summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-03 14:46:05 +0200
committerivar <i@oiee.no>2026-04-03 14:46:05 +0200
commita46309ac64261814d931f538fad373ea8a4f0e95 (patch)
tree6aeb1333071e433fc9e3bc7779b8b41d9e270544
parenta21fa502891a6d2f0600485b1b76762ecc178fd0 (diff)
downloadnebbet.no-a46309ac64261814d931f538fad373ea8a4f0e95.tar.xz
nebbet.no-a46309ac64261814d931f538fad373ea8a4f0e95.zip
feat: embed admin templates into binary with //go:embed
Move admin templates from templates/admin/*.html to internal/admin/templates/*.html and embed them using //go:embed directive. This removes the runtime dependency on having template files on disk, allowing the templates to be compiled into the binary. Changes: - Add embed import and //go:embed directive for templates - Change ParseGlob() to ParseFS() to load from embedded filesystem - Copy templates to internal/admin/templates/ for embedding Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
-rw-r--r--internal/admin/server.go10
-rw-r--r--internal/admin/templates/base.html34
-rw-r--r--internal/admin/templates/error.html7
-rw-r--r--internal/admin/templates/form.html62
-rw-r--r--internal/admin/templates/list.html40
5 files changed, 150 insertions, 3 deletions
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 33413ee..b36c598 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -4,6 +4,7 @@
package admin
import (
+ "embed"
"fmt"
"html/template"
"net/http"
@@ -19,6 +20,9 @@ import (
"nebbet.no/internal/builder"
)
+//go:embed templates/*.html
+var adminTemplates embed.FS
+
// Server is an http.Handler that serves the admin post-management UI.
type Server struct {
// PostsDir is the directory where post markdown files are stored,
@@ -353,7 +357,7 @@ func slugify(title string) string {
return s
}
-// mustParseTemplates loads admin templates from the templates/admin/ directory.
+// mustParseTemplates loads admin templates from the embedded filesystem.
func mustParseTemplates() *template.Template {
funcs := template.FuncMap{
"splitTags": func(s string) []string {
@@ -367,8 +371,8 @@ func mustParseTemplates() *template.Template {
return tags
},
}
- // Load all .html files from templates/admin/ directory
- tmpl, err := template.New("admin").Funcs(funcs).ParseGlob("templates/admin/*.html")
+ // Parse templates from embedded filesystem
+ tmpl, err := template.New("admin").Funcs(funcs).ParseFS(adminTemplates, "templates/*.html")
if err != nil {
panic(fmt.Sprintf("failed to parse admin templates: %v", err))
}
diff --git a/internal/admin/templates/base.html b/internal/admin/templates/base.html
new file mode 100644
index 0000000..f88bba6
--- /dev/null
+++ b/internal/admin/templates/base.html
@@ -0,0 +1,34 @@
+{{define "base"}}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Admin — {{.Title}}</title>
+ <link rel="stylesheet" href="/styles/admin.css">
+ <link rel="stylesheet" href="/lib/milkdown-crepe/style.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "@milkdown/crepe": "/lib/node_modules/@milkdown/crepe"
+ }
+ }
+ </script>
+</head>
+
+<body>
+ <nav>
+ <span class="brand">Admin</span>
+ <a href="/admin/">Posts</a>
+ <a href="/admin/new">New Post</a>
+ </nav>
+ <main>
+ {{if eq .ContentTemplate "list-content"}}{{template "list-content" .}}{{end}}
+ {{if eq .ContentTemplate "form-content"}}{{template "form-content" .}}{{end}}
+ {{if eq .ContentTemplate "error-content"}}{{template "error-content" .}}{{end}}
+ </main>
+</body>
+
+</html>
+{{end}} \ No newline at end of file
diff --git a/internal/admin/templates/error.html b/internal/admin/templates/error.html
new file mode 100644
index 0000000..36a4a8a
--- /dev/null
+++ b/internal/admin/templates/error.html
@@ -0,0 +1,7 @@
+{{define "error-content"}}
+ <div class="alert">
+ <h2>Error</h2>
+ <p>{{.Message}}</p>
+ <p><a href="javascript:history.back()">Go back</a></p>
+ </div>
+{{end}}
diff --git a/internal/admin/templates/form.html b/internal/admin/templates/form.html
new file mode 100644
index 0000000..1b577c9
--- /dev/null
+++ b/internal/admin/templates/form.html
@@ -0,0 +1,62 @@
+{{define "form-content"}}
+ <h1>{{.Title}}</h1>
+ <form method="POST" action="{{.Action}}">
+ <label for="title">Title</label>
+ <input type="text" id="title" name="title" value="{{.Post.Title}}" required autofocus>
+
+ <label for="date">Date</label>
+ <input type="date" id="date" name="date" value="{{.Post.Date}}">
+
+ <label for="tags">Tags</label>
+ <input type="text" id="tags" name="tags" value="{{.Post.Tags}}" placeholder="tag1, tag2, tag3">
+ <p class="hint">Comma-separated list of tags.</p>
+
+ <label for="content">Content (Markdown)</label>
+ <textarea id="content" name="content" style="display: none;">{{.Post.Content}}</textarea>
+ <div id="editor" style="border: 1px solid #ccc; border-radius: 4px; min-height: 340px;"></div>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary">
+ {{if .IsNew}}Create Post{{else}}Save Changes{{end}}
+ </button>
+ <a href="/admin/" class="btn btn-secondary">Cancel</a>
+ </div>
+ </form>
+
+ <script type="module">
+ import { Crepe } from '@milkdown/crepe';
+
+ const contentField = document.getElementById('content');
+ const editorContainer = document.getElementById('editor');
+ const form = contentField.closest('form');
+
+ // Initialize Crepe with the textarea content
+ const crepe = new Crepe({
+ root: editorContainer,
+ defaultValue: contentField.value,
+ });
+
+ // Sync editor content back to textarea before form submission
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ try {
+ // Get the markdown content from Crepe
+ const markdown = await crepe.getMarkdown();
+ contentField.value = markdown;
+ } catch (err) {
+ console.error('Failed to get markdown from editor:', err);
+ }
+
+ // Submit the form
+ form.submit();
+ });
+ </script>
+
+ <noscript>
+ <style>
+ #editor { display: none; }
+ #content { display: block !important; }
+ </style>
+ </noscript>
+{{end}}
diff --git a/internal/admin/templates/list.html b/internal/admin/templates/list.html
new file mode 100644
index 0000000..561e317
--- /dev/null
+++ b/internal/admin/templates/list.html
@@ -0,0 +1,40 @@
+{{define "list-content"}}
+ <h1>Posts</h1>
+ {{if .Posts}}
+ <table>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Date</th>
+ <th>Tags</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Posts}}
+ <tr>
+ <td>{{.Title}}</td>
+ <td>{{.Date}}</td>
+ <td>
+ {{if .Tags}}
+ <div class="tags">
+ {{range (splitTags .Tags)}}<span class="tag">{{.}}</span>{{end}}
+ </div>
+ {{end}}
+ </td>
+ <td>
+ <div class="actions">
+ <a href="/admin/{{.Slug}}/edit" class="btn btn-secondary">Edit</a>
+ <form method="POST" action="/admin/{{.Slug}}/delete" onsubmit="return confirm('Delete {{.Title}}?')">
+ <button type="submit" class="btn btn-danger">Delete</button>
+ </form>
+ </div>
+ </td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{else}}
+ <div class="empty">No posts yet. <a href="/admin/new">Create one.</a></div>
+ {{end}}
+{{end}}