✨feature: Set up templated views and blog support
This commit is contained in:
230
main.go
230
main.go
@@ -1,3 +1,4 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -7,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,26 +16,27 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// NavItem represents a navigation link
|
||||
type NavItem struct {
|
||||
Title string
|
||||
URL string
|
||||
}
|
||||
|
||||
// PageMeta holds front matter metadata
|
||||
type PageMeta struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Date string `yaml:"date"`
|
||||
Slug string // we'll set this ourselves from filename
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Date string `yaml:"date"`
|
||||
Categories []string `yaml:"categories"`
|
||||
Slug string
|
||||
}
|
||||
|
||||
var slugRegex = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
func main() {
|
||||
contentDir := "./content"
|
||||
outputDir := "./public"
|
||||
templateFile := "./templates/layout.html"
|
||||
|
||||
tpl, err := template.ParseFiles(templateFile)
|
||||
// Load all templates in the templates folder
|
||||
tpl, err := template.New("").Funcs(templateFuncs()).ParseGlob("templates/*.html")
|
||||
if err != nil {
|
||||
fmt.Printf("Template parsing error: %v\n", err)
|
||||
return
|
||||
@@ -45,7 +48,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect nav info early
|
||||
var nav []NavItem
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) == ".md" {
|
||||
@@ -54,77 +56,63 @@ func main() {
|
||||
if name != "index" {
|
||||
url = "/" + name + "/"
|
||||
}
|
||||
nav = append(nav, NavItem{
|
||||
Title: strings.Title(name),
|
||||
URL: url,
|
||||
})
|
||||
nav = append(nav, NavItem{Title: strings.Title(name), URL: url})
|
||||
}
|
||||
}
|
||||
|
||||
// Add blog to the main nav
|
||||
nav = append(nav, NavItem{
|
||||
Title: "Blog",
|
||||
URL: "/blog/",
|
||||
})
|
||||
// Add blog to main nav
|
||||
nav = append(nav, NavItem{Title: "Blog", URL: "/blog/"})
|
||||
|
||||
// Generate index.html for each static page markdown file
|
||||
// Static page rendering
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".md" {
|
||||
continue
|
||||
if filepath.Ext(file.Name()) == ".md" {
|
||||
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
||||
title := strings.Title(name)
|
||||
mdPath := filepath.Join(contentDir, file.Name())
|
||||
rawContent, err := ioutil.ReadFile(mdPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read %s: %v\n", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
meta, content := parseFrontMatter(rawContent)
|
||||
htmlContent := blackfriday.Run(content)
|
||||
if meta.Title == "" {
|
||||
meta.Title = title
|
||||
}
|
||||
|
||||
var outPath string
|
||||
if name == "index" {
|
||||
outPath = filepath.Join(outputDir, "index.html")
|
||||
} else {
|
||||
subDir := filepath.Join(outputDir, name)
|
||||
os.MkdirAll(subDir, os.ModePerm)
|
||||
outPath = filepath.Join(subDir, "index.html")
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create %s: %v\n", outPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
tpl.ExecuteTemplate(outFile, "static_page", map[string]interface{}{
|
||||
"Title": meta.Title,
|
||||
"Description": meta.Description,
|
||||
"Date": meta.Date,
|
||||
"Categories": meta.Categories,
|
||||
"Content": template.HTML(htmlContent),
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
"PageTemplate": "static",
|
||||
})
|
||||
|
||||
fmt.Printf("Generated: %s\n", outPath)
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
||||
mdPath := filepath.Join(contentDir, file.Name())
|
||||
rawContent, err := ioutil.ReadFile(mdPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read %s: %v\n", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
meta, content := parseFrontMatter(rawContent)
|
||||
htmlContent := blackfriday.Run(content)
|
||||
|
||||
// Determine output path
|
||||
var outPath string
|
||||
if name == "index" {
|
||||
outPath = filepath.Join(outputDir, "index.html")
|
||||
} else {
|
||||
subDir := filepath.Join(outputDir, name)
|
||||
os.MkdirAll(subDir, os.ModePerm)
|
||||
outPath = filepath.Join(subDir, "index.html")
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create %s: %v\n", outPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Fall back to filename for title if none provided
|
||||
if meta.Title == "" {
|
||||
meta.Title = strings.Title(name)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Title": meta.Title,
|
||||
"Description": meta.Description,
|
||||
"Date": meta.Date,
|
||||
"Content": template.HTML(htmlContent),
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
}
|
||||
|
||||
err = tpl.Execute(outFile, data)
|
||||
if err != nil {
|
||||
fmt.Printf("Template execution failed for %s: %v\n", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Generated: %s\n", outPath)
|
||||
}
|
||||
|
||||
// Generate blog index
|
||||
var blogPosts []PageMeta
|
||||
categoryMap := make(map[string][]PageMeta)
|
||||
|
||||
blogDir := filepath.Join(contentDir, "blog")
|
||||
blogFiles, _ := ioutil.ReadDir(blogDir)
|
||||
@@ -144,12 +132,16 @@ func main() {
|
||||
|
||||
meta, content := parseFrontMatter(rawContent)
|
||||
html := blackfriday.Run(content)
|
||||
|
||||
meta.Slug = slug
|
||||
if meta.Title == "" {
|
||||
meta.Title = strings.Title(slug)
|
||||
}
|
||||
|
||||
for _, cat := range meta.Categories {
|
||||
slug := slugify(cat)
|
||||
categoryMap[slug] = append(categoryMap[slug], meta)
|
||||
}
|
||||
|
||||
outPath := filepath.Join(outputDir, "blog", slug, "index.html")
|
||||
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
|
||||
|
||||
@@ -159,47 +151,54 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
err = tpl.Execute(outFile, map[string]interface{}{
|
||||
"Title": meta.Title,
|
||||
"Description": meta.Description,
|
||||
"Date": meta.Date,
|
||||
"Content": template.HTML(html),
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
tpl.ExecuteTemplate(outFile, "blog_post_page", map[string]interface{}{
|
||||
"Title": meta.Title,
|
||||
"Description": meta.Description,
|
||||
"Date": meta.Date,
|
||||
"Categories": meta.Categories,
|
||||
"Content": template.HTML(html),
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
"PageTemplate": "blog_post",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Template error for blog post %s: %v\n", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
blogPosts = append(blogPosts, meta)
|
||||
}
|
||||
|
||||
err = buildBlogIndex(blogPosts, tpl, outputDir, nav)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to build blog index:", err)
|
||||
buildBlogIndex(blogPosts, tpl, outputDir, nav)
|
||||
|
||||
for catSlug, posts := range categoryMap {
|
||||
outDir := filepath.Join(outputDir, "blog", "category", catSlug)
|
||||
os.MkdirAll(outDir, os.ModePerm)
|
||||
|
||||
outPath := filepath.Join(outDir, "index.html")
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create category page for %s: %v\n", catSlug, err)
|
||||
continue
|
||||
}
|
||||
|
||||
tpl.ExecuteTemplate(outFile, "category_page", map[string]interface{}{
|
||||
"Title": "Category: " + strings.Title(strings.ReplaceAll(catSlug, "-", " ")),
|
||||
"Posts": posts,
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
"PageTemplate": "category_page",
|
||||
})
|
||||
|
||||
fmt.Printf("Generated category: /blog/category/%s/\n", catSlug)
|
||||
}
|
||||
|
||||
fmt.Println("✅ Site generation complete.")
|
||||
}
|
||||
|
||||
// parseFrontMatter splits raw markdown into meta + content
|
||||
func parseFrontMatter(raw []byte) (PageMeta, []byte) {
|
||||
var meta PageMeta
|
||||
|
||||
// Normalize line endings
|
||||
raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n"))
|
||||
|
||||
// Only try to parse if the file starts with "---\n"
|
||||
if bytes.HasPrefix(raw, []byte("---\n")) {
|
||||
// Split after the first two "---\n" lines
|
||||
parts := bytes.SplitN(raw, []byte("---\n"), 3)
|
||||
|
||||
if len(parts) >= 3 {
|
||||
// parts[0] = empty (before first ---)
|
||||
// parts[1] = YAML content
|
||||
// parts[2] = remaining markdown content
|
||||
if len(parts) == 3 {
|
||||
err := yaml.Unmarshal(parts[1], &meta)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
|
||||
@@ -208,26 +207,39 @@ func parseFrontMatter(raw []byte) (PageMeta, []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no front matter, return raw as-is
|
||||
return meta, raw
|
||||
}
|
||||
|
||||
// buildBlogIndex generates the blog index page
|
||||
func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) error {
|
||||
func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) {
|
||||
outPath := filepath.Join(outputDir, "blog", "index.html")
|
||||
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
|
||||
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Println("Failed to create blog index:", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Title": "Go SSG - Blog",
|
||||
"Posts": posts,
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
}
|
||||
|
||||
return tpl.Execute(outFile, data)
|
||||
fmt.Printf("Rendering blog index: %d posts\n", len(posts))
|
||||
tpl.ExecuteTemplate(outFile, "blog_index_page", map[string]interface{}{
|
||||
"Title": "Blog Index",
|
||||
"Posts": posts,
|
||||
"Nav": nav,
|
||||
"Year": time.Now().Year(),
|
||||
"PageTemplate": "blog_index",
|
||||
})
|
||||
}
|
||||
|
||||
func slugify(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = slugRegex.ReplaceAllString(s, "-")
|
||||
return strings.Trim(s, "-")
|
||||
}
|
||||
|
||||
func templateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"lower": strings.ToLower,
|
||||
"slugify": slugify,
|
||||
"urlquery": template.URLQueryEscaper,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user