feat: static sites feature with Gitea/GitHub/GitLab support and Deno backend
Deploy static content from Git repository folders with optional server-side
API endpoints. Supports Gitea/Forgejo/Gogs, GitHub, and GitLab with provider
autodetection.
- New Sites entity with CRUD, encrypted secrets, and manual/push/tag sync triggers
- Pluggable GitProvider interface with three implementations
- Deno container mode: auto-generates router from API_{method}_{name} exports
- Static container mode: nginx serving files with optional markdown rendering
- Wizard UI with provider selector, repo picker, branch/folder tree pickers
- Deploy pipeline builds fresh image, starts container, configures NPM proxy
- Stop/Start buttons, force redeploy on manual trigger
- Periodic health checker detects crashed containers
- Proxy route existence check during auto-sync
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package staticsite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
// RenderMarkdownFiles walks the directory and converts all .md files to .html.
|
||||
// The original .md file is kept alongside the generated .html file.
|
||||
func RenderMarkdownFiles(dir string) error {
|
||||
md := goldmark.New()
|
||||
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".md" && ext != ".markdown" {
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert(src, &buf); err != nil {
|
||||
return fmt.Errorf("render %s: %w", path, err)
|
||||
}
|
||||
|
||||
html := wrapHTML(extractTitle(src), buf.String())
|
||||
|
||||
htmlPath := strings.TrimSuffix(path, filepath.Ext(path)) + ".html"
|
||||
if err := os.WriteFile(htmlPath, []byte(html), 0o644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", htmlPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// extractTitle finds the first # heading in markdown content.
|
||||
func extractTitle(src []byte) string {
|
||||
for _, line := range strings.Split(string(src), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "# ") {
|
||||
return strings.TrimPrefix(trimmed, "# ")
|
||||
}
|
||||
}
|
||||
return "Page"
|
||||
}
|
||||
|
||||
// wrapHTML wraps rendered markdown in a minimal HTML document.
|
||||
func wrapHTML(title, body string) string {
|
||||
return fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%s</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #333; }
|
||||
pre { background: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
|
||||
pre code { background: none; padding: 0; }
|
||||
img { max-width: 100%%; height: auto; }
|
||||
a { color: #0366d6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
%s
|
||||
</body>
|
||||
</html>`, title, body)
|
||||
}
|
||||
Reference in New Issue
Block a user