This commit is contained in:
Dev
2025-09-15 04:02:11 +03:00
commit fc86288f06
24 changed files with 2938 additions and 0 deletions

View 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>&copy; {{.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)
}