295 lines
7.6 KiB
Go
295 lines
7.6 KiB
Go
package templates
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitblog/internal/content"
|
|
)
|
|
|
|
type Manager struct {
|
|
templates map[string]*template.Template
|
|
funcMap template.FuncMap
|
|
}
|
|
|
|
type PageData struct {
|
|
Title string
|
|
Content template.HTML
|
|
Posts []*content.Post
|
|
Post *content.Post
|
|
Page *content.Page
|
|
Navigation []content.Navigation
|
|
Categories []string
|
|
Theme string
|
|
CurrentYear int
|
|
Meta map[string]string
|
|
}
|
|
|
|
func NewManager() *Manager {
|
|
funcMap := template.FuncMap{
|
|
"formatDate": formatDate,
|
|
"formatDateTime": formatDateTime,
|
|
"truncate": truncate,
|
|
"safeHTML": safeHTML,
|
|
"join": strings.Join,
|
|
"contains": strings.Contains,
|
|
"hasPrefix": strings.HasPrefix,
|
|
"hasSuffix": strings.HasSuffix,
|
|
"toLower": strings.ToLower,
|
|
"toUpper": strings.ToUpper,
|
|
"title": strings.Title,
|
|
"now": time.Now,
|
|
}
|
|
|
|
return &Manager{
|
|
templates: make(map[string]*template.Template),
|
|
funcMap: funcMap,
|
|
}
|
|
}
|
|
|
|
func (m *Manager) LoadTemplates() error {
|
|
templates := []string{
|
|
"base",
|
|
"home",
|
|
"post",
|
|
"page",
|
|
"category",
|
|
"404",
|
|
}
|
|
|
|
for _, name := range templates {
|
|
if err := m.loadTemplate(name); err != nil {
|
|
return fmt.Errorf("failed to load template %s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) loadTemplate(name string) error {
|
|
baseTemplate := m.getBaseTemplate()
|
|
pageTemplate := m.getPageTemplate(name)
|
|
|
|
tmpl, err := template.New(name).Funcs(m.funcMap).Parse(baseTemplate + pageTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.templates[name] = tmpl
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Render(templateName string, data PageData) (string, error) {
|
|
tmpl, exists := m.templates[templateName]
|
|
if !exists {
|
|
return "", fmt.Errorf("template %s not found", templateName)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (m *Manager) getBaseTemplate() string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="en" data-theme="{{.Theme}}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{if .Title}}{{.Title}} - {{end}}GitBlog</title>
|
|
<meta name="description" content="{{if .Meta.description}}{{.Meta.description}}{{else}}A modern blog powered by GitHub{{end}}">
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header class="header">
|
|
<nav class="nav">
|
|
<div class="nav-brand">
|
|
<a href="/" class="nav-logo">GitBlog</a>
|
|
</div>
|
|
<div class="nav-menu">
|
|
{{range .Navigation}}
|
|
<a href="{{.URL}}" class="nav-link">{{.Title}}</a>
|
|
{{end}}
|
|
</div>
|
|
<div class="nav-actions">
|
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
<span class="theme-icon">🌙</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="main">
|
|
{{template "content" .}}
|
|
</main>
|
|
|
|
<footer class="footer">
|
|
<div class="footer-content">
|
|
<p>© {{.CurrentYear}} GitBlog. Powered by GitHub.</p>
|
|
<div class="footer-links">
|
|
<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>
|
|
<a href="/rss.xml" target="_blank" rel="noopener">RSS</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<script src="/static/js/theme.js"></script>
|
|
<script src="/static/js/main.js"></script>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
func (m *Manager) getPageTemplate(name string) string {
|
|
templates := map[string]string{
|
|
"home": `{{define "content"}}
|
|
<div class="hero">
|
|
<h1 class="hero-title">Welcome to GitBlog</h1>
|
|
<p class="hero-subtitle">A modern blog powered by GitHub</p>
|
|
</div>
|
|
|
|
<div class="posts-grid">
|
|
{{range .Posts}}
|
|
<article class="post-card">
|
|
<div class="post-meta">
|
|
<time class="post-date">{{.Date | formatDate}}</time>
|
|
{{if .Categories}}
|
|
<span class="post-categories">
|
|
{{range .Categories}}
|
|
<span class="category-tag">{{.}}</span>
|
|
{{end}}
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
<h2 class="post-title">
|
|
<a href="/post/{{.Slug}}">{{.Title}}</a>
|
|
</h2>
|
|
<div class="post-excerpt">{{.Excerpt | safeHTML}}</div>
|
|
<div class="post-tags">
|
|
{{range .Tags}}
|
|
<span class="tag">{{.}}</span>
|
|
{{end}}
|
|
</div>
|
|
</article>
|
|
{{end}}
|
|
</div>
|
|
{{end}}`,
|
|
|
|
"post": `{{define "content"}}
|
|
<article class="post">
|
|
<header class="post-header">
|
|
<h1 class="post-title">{{.Post.Title}}</h1>
|
|
<div class="post-meta">
|
|
<time class="post-date">{{.Post.Date | formatDateTime}}</time>
|
|
{{if .Post.Categories}}
|
|
<div class="post-categories">
|
|
{{range .Post.Categories}}
|
|
<a href="/category/{{. | toLower}}" class="category-link">{{.}}</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</header>
|
|
|
|
<div class="post-content">
|
|
{{.Post.HTML | safeHTML}}
|
|
</div>
|
|
|
|
{{if .Post.Tags}}
|
|
<footer class="post-footer">
|
|
<div class="post-tags">
|
|
{{range .Post.Tags}}
|
|
<span class="tag">{{.}}</span>
|
|
{{end}}
|
|
</div>
|
|
</footer>
|
|
{{end}}
|
|
</article>
|
|
{{end}}`,
|
|
|
|
"page": `{{define "content"}}
|
|
<article class="page">
|
|
<header class="page-header">
|
|
<h1 class="page-title">{{.Page.Title}}</h1>
|
|
</header>
|
|
|
|
<div class="page-content">
|
|
{{.Page.HTML | safeHTML}}
|
|
</div>
|
|
</article>
|
|
{{end}}`,
|
|
|
|
"category": `{{define "content"}}
|
|
<div class="category-header">
|
|
<h1 class="category-title">Category: {{.Title}}</h1>
|
|
<p class="category-description">Posts in this category</p>
|
|
</div>
|
|
|
|
<div class="posts-grid">
|
|
{{range .Posts}}
|
|
<article class="post-card">
|
|
<div class="post-meta">
|
|
<time class="post-date">{{.Date | formatDate}}</time>
|
|
{{if .Categories}}
|
|
<span class="post-categories">
|
|
{{range .Categories}}
|
|
<span class="category-tag">{{.}}</span>
|
|
{{end}}
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
<h2 class="post-title">
|
|
<a href="/post/{{.Slug}}">{{.Title}}</a>
|
|
</h2>
|
|
<div class="post-excerpt">{{.Excerpt | safeHTML}}</div>
|
|
<div class="post-tags">
|
|
{{range .Tags}}
|
|
<span class="tag">{{.}}</span>
|
|
{{end}}
|
|
</div>
|
|
</article>
|
|
{{end}}
|
|
</div>
|
|
{{end}}`,
|
|
|
|
"404": `{{define "content"}}
|
|
<div class="error-page">
|
|
<h1 class="error-title">404</h1>
|
|
<p class="error-message">Page not found</p>
|
|
<a href="/" class="error-link">Go back home</a>
|
|
</div>
|
|
{{end}}`,
|
|
}
|
|
|
|
return templates[name]
|
|
}
|
|
|
|
func formatDate(t time.Time) string {
|
|
return t.Format("January 2, 2006")
|
|
}
|
|
|
|
func formatDateTime(t time.Time) string {
|
|
return t.Format("January 2, 2006 at 3:04 PM")
|
|
}
|
|
|
|
func truncate(s string, length int) string {
|
|
if len(s) <= length {
|
|
return s
|
|
}
|
|
return s[:length] + "..."
|
|
}
|
|
|
|
func safeHTML(s string) template.HTML {
|
|
return template.HTML(s)
|
|
}
|