refactor: move tenant templates to internal/tenant
- Move internal/build/ to internal/tenant/ - Rename assets for clarity - Add tenant-blog.js for shared blog functionality - Update style.css with improved code block styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bef5dd4437
commit
6e2959f619
11 changed files with 153 additions and 358 deletions
63
internal/tenant/templates/base.html
Normal file
63
internal/tenant/templates/base.html
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!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">
|
||||
|
||||
{{if .StructuredData}}
|
||||
<script type="application/ld+json">{{.StructuredData}}</script>
|
||||
{{end}}
|
||||
</head>
|
||||
<body>
|
||||
<div id="page" class="layout-{{with index .Settings "layout"}}{{.}}{{else}}default{{end}} compactness-{{with index .Settings "compactness"}}{{.}}{{else}}cozy{{end}}">
|
||||
<style>
|
||||
:root {
|
||||
--accent: {{with index .Settings "accent_color"}}{{.}}{{else}}#2563eb{{end}};
|
||||
--font-body: {{or .FontFamily "system-ui, -apple-system, sans-serif"}};
|
||||
}
|
||||
{{.CodeThemeCSS}}
|
||||
</style>
|
||||
<header class="site-header">
|
||||
<a href="/" class="site-name">{{.SiteName}}</a>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>© {{.Year}} {{.SiteName}}</span>
|
||||
{{if .ShowBadge}}<a href="https://writekit.dev" class="powered-by" target="_blank" rel="noopener">Powered by WriteKit</a>{{end}}
|
||||
</footer>
|
||||
|
||||
<script src="/static/js/tenant-blog.js"></script>
|
||||
{{block "scripts" .}}{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
34
internal/tenant/templates/blog.html
Normal file
34
internal/tenant/templates/blog.html
Normal 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">← Newer</a>
|
||||
{{end}}
|
||||
{{if .NextPage}}
|
||||
<a href="{{.NextPage}}" class="pagination-next">Older →</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
34
internal/tenant/templates/home.html
Normal file
34
internal/tenant/templates/home.html
Normal 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}}
|
||||
57
internal/tenant/templates/post.html
Normal file
57
internal/tenant/templates/post.html
Normal 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/tenant-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}}
|
||||
140
internal/tenant/templates/templates.go
Normal file
140
internal/tenant/templates/templates.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
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
|
||||
CodeThemeCSS template.CSS
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue