gitblog1
This commit is contained in:
294
internal/templates/manager.go
Normal file
294
internal/templates/manager.go
Normal file
@@ -0,0 +1,294 @@
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user