This commit is contained in:
Josh 2026-01-09 00:16:46 +02:00
commit d69342b2e9
160 changed files with 28681 additions and 0 deletions

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<meta name="description" content="{{.Description}}">
<link rel="canonical" href="{{.CanonicalURL}}">
{{if .NoIndex}}<meta name="robots" content="noindex">{{end}}
<!-- Open Graph -->
<meta property="og:title" content="{{.Title}}">
<meta property="og:description" content="{{.Description}}">
<meta property="og:type" content="{{.OGType}}">
<meta property="og:url" content="{{.CanonicalURL}}">
{{if .OGImage}}<meta property="og:image" content="{{.OGImage}}">{{end}}
<meta property="og:site_name" content="{{.SiteName}}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{.Title}}">
<meta name="twitter:description" content="{{.Description}}">
{{if .OGImage}}<meta name="twitter:image" content="{{.OGImage}}">{{end}}
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
{{if .FontURL}}<link rel="stylesheet" href="{{.FontURL}}">{{end}}
<link rel="stylesheet" href="/static/css/style.css">
<style>
:root {
--accent: {{with index .Settings "accent_color"}}{{.}}{{else}}#2563eb{{end}};
--font-body: {{or .FontFamily "system-ui, -apple-system, sans-serif"}};
}
</style>
{{if .StructuredData}}
<script type="application/ld+json">{{.StructuredData}}</script>
{{end}}
</head>
<body class="layout-{{with index .Settings "layout"}}{{.}}{{else}}default{{end}} compactness-{{with index .Settings "compactness"}}{{.}}{{else}}cozy{{end}}">
<header class="site-header">
<a href="/" class="site-name">{{.SiteName}}</a>
<nav class="site-nav">
<button type="button" id="search-trigger" class="search-trigger">
<span>Search</span>
<kbd>/</kbd>
</button>
</nav>
</header>
<main>
{{template "content" .}}
</main>
<footer class="site-footer">
<span>&copy; {{.Year}} {{.SiteName}}</span>
{{if .ShowBadge}}<a href="https://writekit.dev" class="powered-by" target="_blank" rel="noopener">Powered by WriteKit</a>{{end}}
</footer>
<div id="search-modal" class="search-modal">
<div class="search-modal-backdrop"></div>
<div class="search-modal-content">
<input type="text" id="search-input" placeholder="Search..." autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-hint">Press <kbd>ESC</kbd> to close</div>
</div>
</div>
<script src="/static/js/main.js"></script>
{{block "scripts" .}}{{end}}
</body>
</html>

View file

@ -0,0 +1,34 @@
{{define "content"}}
<div class="blog">
<header class="blog-header">
<h1>Posts</h1>
</header>
<section class="posts-list">
{{range .Posts}}
<article class="post-card">
<a href="/posts/{{.Slug}}">
<h2 class="post-card-title">{{.Title}}</h2>
<time class="post-card-date" datetime="{{.Date.Format "2006-01-02"}}">{{.Date.Format "January 2, 2006"}}</time>
{{if .Description}}
<p class="post-card-description">{{.Description}}</p>
{{end}}
</a>
</article>
{{else}}
<p class="no-posts">No posts yet.</p>
{{end}}
</section>
{{if or .PrevPage .NextPage}}
<nav class="pagination">
{{if .PrevPage}}
<a href="{{.PrevPage}}" class="pagination-prev">&larr; Newer</a>
{{end}}
{{if .NextPage}}
<a href="{{.NextPage}}" class="pagination-next">Older &rarr;</a>
{{end}}
</nav>
{{end}}
</div>
{{end}}

View file

@ -0,0 +1,34 @@
{{define "content"}}
<div class="home">
{{with index .Settings "author_bio"}}
<section class="profile">
{{with index $.Settings "author_avatar"}}
<img src="{{.}}" alt="{{$.SiteName}}" class="profile-avatar">
{{end}}
<p class="profile-bio">{{.}}</p>
</section>
{{end}}
<section class="posts-list">
{{range .Posts}}
<article class="post-card">
<a href="/posts/{{.Slug}}">
<h2 class="post-card-title">{{.Title}}</h2>
<time class="post-card-date" datetime="{{.Date.Format "2006-01-02"}}">{{.Date.Format "January 2, 2006"}}</time>
{{if .Description}}
<p class="post-card-description">{{.Description}}</p>
{{end}}
</a>
</article>
{{else}}
<p class="no-posts">No posts yet.</p>
{{end}}
</section>
{{if .HasMore}}
<nav class="pagination">
<a href="/posts" class="view-all">View all posts</a>
</nav>
{{end}}
</div>
{{end}}

View file

@ -0,0 +1,57 @@
{{define "content"}}
<article class="post">
<header class="post-header">
<time class="post-date" datetime="{{.Post.Date.Format "2006-01-02"}}">{{.Post.Date.Format "January 2, 2006"}}</time>
<h1 class="post-title">{{.Post.Title}}</h1>
{{if .Post.Description}}
<p class="post-description">{{.Post.Description}}</p>
{{end}}
{{if .Post.Tags}}
<div class="post-tags">
{{range .Post.Tags}}
<a href="/tags/{{.}}" class="tag">#{{.}}</a>
{{end}}
</div>
{{end}}
{{if .Post.CoverImage}}
<figure class="post-cover">
<img src="{{.Post.CoverImage}}" alt="{{.Post.Title}}" loading="eager" />
</figure>
{{end}}
</header>
<div class="post-content prose">
{{.ContentHTML}}
</div>
{{if .InteractionConfig.ReactionsEnabled}}
<section id="reactions" class="reactions" data-slug="{{.Post.Slug}}">
<div class="reactions-container"></div>
</section>
{{end}}
{{if .InteractionConfig.CommentsEnabled}}
<section id="comments" class="comments" data-slug="{{.Post.Slug}}">
<h3 class="comments-title">Comments</h3>
<div class="comments-list"></div>
<div class="comment-form-container"></div>
</section>
{{end}}
</article>
{{end}}
{{define "scripts"}}
{{if or .InteractionConfig.ReactionsEnabled .InteractionConfig.CommentsEnabled}}
<script src="/static/js/post.js"></script>
<script>
WriteKit.init({
slug: "{{.Post.Slug}}",
reactions: {{.InteractionConfig.ReactionsEnabled}},
comments: {{.InteractionConfig.CommentsEnabled}},
reactionMode: "{{.InteractionConfig.ReactionMode}}",
reactionEmojis: "{{.InteractionConfig.ReactionEmojis}}".split(","),
requireAuth: {{.InteractionConfig.ReactionsRequireAuth}}
});
</script>
{{end}}
{{end}}

View file

@ -0,0 +1,139 @@
package templates
import (
"bytes"
"embed"
"encoding/json"
"html/template"
"time"
)
//go:embed *.html
var templateFS embed.FS
var funcMap = template.FuncMap{
"safeHTML": func(s string) template.HTML { return template.HTML(s) },
"json": func(v any) string { b, _ := json.Marshal(v); return string(b) },
"or": func(a, b any) any {
if a != nil && a != "" {
return a
}
return b
},
}
var fontURLs = map[string]string{
"system": "",
"inter": "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"georgia": "",
"merriweather": "https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap",
"source-serif": "https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600;700&display=swap",
"jetbrains-mono": "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap",
}
var fontFamilies = map[string]string{
"system": "system-ui, -apple-system, sans-serif",
"inter": "'Inter', system-ui, sans-serif",
"georgia": "Georgia, 'Times New Roman', serif",
"merriweather": "'Merriweather', Georgia, serif",
"source-serif": "'Source Serif 4', Georgia, serif",
"jetbrains-mono": "'JetBrains Mono', 'Fira Code', monospace",
}
func GetFontURL(fontKey string) string {
if url, ok := fontURLs[fontKey]; ok {
return url
}
return ""
}
func GetFontFamily(fontKey string) string {
if family, ok := fontFamilies[fontKey]; ok {
return family
}
return fontFamilies["system"]
}
var homeTemplate, blogTemplate, postTemplate *template.Template
func init() {
homeTemplate = template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "base.html", "home.html"))
blogTemplate = template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "base.html", "blog.html"))
postTemplate = template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "base.html", "post.html"))
}
type PageData struct {
Title string
Description string
CanonicalURL string
OGType string
OGImage string
NoIndex bool
SiteName string
Year int
FontURL string
FontFamily string
StructuredData template.JS
Settings map[string]any
ShowBadge bool
}
type HomeData struct {
PageData
Posts []PostSummary
HasMore bool
}
type BlogData struct {
PageData
Posts []PostSummary
PrevPage string
NextPage string
}
type PostData struct {
PageData
Post PostDetail
ContentHTML template.HTML
InteractionConfig map[string]any
}
type PostSummary struct {
Slug string
Title string
Description string
Date time.Time
}
type PostDetail struct {
Slug string
Title string
Description string
CoverImage string
Date time.Time
Tags []string
}
func RenderHome(data HomeData) ([]byte, error) {
var buf bytes.Buffer
if err := homeTemplate.ExecuteTemplate(&buf, "base.html", data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func RenderBlog(data BlogData) ([]byte, error) {
var buf bytes.Buffer
if err := blogTemplate.ExecuteTemplate(&buf, "base.html", data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func RenderPost(data PostData) ([]byte, error) {
var buf bytes.Buffer
if err := postTemplate.ExecuteTemplate(&buf, "base.html", data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}