diff options
| author | ivar <i@oiee.no> | 2026-04-03 14:46:05 +0200 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-04-03 14:46:05 +0200 |
| commit | a46309ac64261814d931f538fad373ea8a4f0e95 (patch) | |
| tree | 6aeb1333071e433fc9e3bc7779b8b41d9e270544 /internal/admin | |
| parent | a21fa502891a6d2f0600485b1b76762ecc178fd0 (diff) | |
| download | nebbet.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>
Diffstat (limited to 'internal/admin')
| -rw-r--r-- | internal/admin/server.go | 10 | ||||
| -rw-r--r-- | internal/admin/templates/base.html | 34 | ||||
| -rw-r--r-- | internal/admin/templates/error.html | 7 | ||||
| -rw-r--r-- | internal/admin/templates/form.html | 62 | ||||
| -rw-r--r-- | internal/admin/templates/list.html | 40 |
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}} |
