390 lines
10 KiB
Go
390 lines
10 KiB
Go
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"sort"
|
|
"io/fs"
|
|
|
|
"github.com/russross/blackfriday/v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type NavItem struct {
|
|
Title string
|
|
URL string
|
|
Position int
|
|
}
|
|
|
|
type PageMeta struct {
|
|
Title string `yaml:"title"`
|
|
NavTitle string `yaml:"navTitle"`
|
|
ShowInNav *bool `yaml:"showInNav"`
|
|
NavPosition *int `yaml:"navPosition"`
|
|
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"
|
|
|
|
tpl, err := loadTemplates()
|
|
if err != nil {
|
|
fmt.Printf("Template parsing error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
files, err := ioutil.ReadDir(contentDir)
|
|
if err != nil {
|
|
fmt.Printf("Failed to read content directory: %v\n", err)
|
|
return
|
|
}
|
|
|
|
entries, _ := os.ReadDir(contentDir)
|
|
nav := buildNav(entries, contentDir)
|
|
renderStaticPages(files, contentDir, outputDir, tpl, nav)
|
|
blogPosts, categoryMap := processBlogPosts(contentDir, outputDir, tpl, nav)
|
|
renderContactPage(contentDir, outputDir, tpl, nav)
|
|
renderThanksPage(contentDir, outputDir, tpl, nav)
|
|
buildBlogIndex(blogPosts, tpl, outputDir, nav)
|
|
buildCategoryPages(categoryMap, tpl, outputDir, nav)
|
|
|
|
fmt.Println("✅ Site generation complete.")
|
|
}
|
|
|
|
func loadTemplates() (*template.Template, error) {
|
|
return template.New("").Funcs(templateFuncs()).ParseGlob("templates/*.html")
|
|
}
|
|
|
|
func buildNav(entries []fs.DirEntry, contentDir string) []NavItem {
|
|
var nav []NavItem
|
|
|
|
for _, entry := range entries {
|
|
if entry.Type().IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
|
path := filepath.Join(contentDir, entry.Name())
|
|
|
|
rawContent, err := os.ReadFile(path)
|
|
if err != nil {
|
|
fmt.Printf("Failed to read %s: %v\n", entry.Name(), err)
|
|
continue
|
|
}
|
|
|
|
meta, _ := parseFrontMatter(rawContent)
|
|
|
|
// Skip pages with showInNav: false
|
|
if meta.ShowInNav != nil && !*meta.ShowInNav {
|
|
continue
|
|
}
|
|
|
|
// Pull title info
|
|
title := strings.TrimSpace(meta.NavTitle)
|
|
if title == "" {
|
|
title = meta.Title
|
|
}
|
|
if title == "" && name == "index" {
|
|
title = "Home"
|
|
} else if title == "" {
|
|
title = strings.Title(name)
|
|
}
|
|
|
|
// Build URL
|
|
url := "/"
|
|
if name != "index" {
|
|
url = "/" + name + "/"
|
|
}
|
|
|
|
// Pull nav position regardless of title logic
|
|
position := 999
|
|
if meta.NavPosition != nil {
|
|
position = *meta.NavPosition
|
|
}
|
|
|
|
nav = append(nav, NavItem{
|
|
Title: title,
|
|
URL: url,
|
|
Position: position,
|
|
})
|
|
}
|
|
|
|
// Optional: add Blog manually (or skip if you handle it as a page)
|
|
nav = append(nav, NavItem{
|
|
Title: "Blog",
|
|
URL: "/blog/",
|
|
Position: 3,
|
|
})
|
|
|
|
// Sort by position
|
|
sort.SliceStable(nav, func(i, j int) bool {
|
|
return nav[i].Position < nav[j].Position
|
|
})
|
|
|
|
return nav
|
|
}
|
|
|
|
func renderStaticPages(files []os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
|
|
for _, file := range files {
|
|
if filepath.Ext(file.Name()) == ".md" {
|
|
renderStaticPage(file, contentDir, outputDir, tpl, nav)
|
|
}
|
|
}
|
|
}
|
|
|
|
func renderStaticPage(file os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
|
|
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)
|
|
return
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func renderContactPage(contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
|
|
contactPath := filepath.Join(contentDir, "contact.md")
|
|
rawContent, err := os.ReadFile(contactPath)
|
|
if err != nil {
|
|
fmt.Printf("Contact page not found: %v\n", err)
|
|
return
|
|
}
|
|
|
|
meta, _ := parseFrontMatter(rawContent)
|
|
|
|
outPath := filepath.Join(outputDir, "contact", "index.html")
|
|
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
|
|
|
|
outFile, err := os.Create(outPath)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create contact page: %v\n", err)
|
|
return
|
|
}
|
|
|
|
tpl.ExecuteTemplate(outFile, "contact_page", map[string]interface{}{
|
|
"Title": meta.Title,
|
|
"Description": meta.Description,
|
|
"Nav": nav,
|
|
"Year": time.Now().Year(),
|
|
"PageTemplate": "contact_page",
|
|
})
|
|
|
|
fmt.Println("Generated: /contact/")
|
|
}
|
|
|
|
func renderThanksPage(contentDir, outputDir string, tpl *template.Template, nav []NavItem) {
|
|
path := filepath.Join(contentDir, "thanks.md")
|
|
rawContent, err := os.ReadFile(path)
|
|
if err != nil {
|
|
fmt.Printf("Thanks page not found: %v\n", err)
|
|
return
|
|
}
|
|
|
|
meta, _ := parseFrontMatter(rawContent)
|
|
|
|
outPath := filepath.Join(outputDir, "thanks", "index.html")
|
|
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
|
|
|
|
outFile, err := os.Create(outPath)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create thanks page: %v\n", err)
|
|
return
|
|
}
|
|
|
|
tpl.ExecuteTemplate(outFile, "thanks_page", map[string]interface{}{
|
|
"Title": meta.Title,
|
|
"Description": meta.Description,
|
|
"Nav": nav,
|
|
"Year": time.Now().Year(),
|
|
"PageTemplate": "thanks_page",
|
|
})
|
|
|
|
fmt.Println("Generated: /thanks/")
|
|
}
|
|
|
|
func processBlogPosts(contentDir, outputDir string, tpl *template.Template, nav []NavItem) ([]PageMeta, map[string][]PageMeta) {
|
|
var blogPosts []PageMeta
|
|
categoryMap := make(map[string][]PageMeta)
|
|
|
|
blogDir := filepath.Join(contentDir, "blog")
|
|
blogFiles, _ := ioutil.ReadDir(blogDir)
|
|
|
|
for _, file := range blogFiles {
|
|
if filepath.Ext(file.Name()) != ".md" {
|
|
continue
|
|
}
|
|
|
|
processBlogPost(file, blogDir, outputDir, tpl, nav, &blogPosts, categoryMap)
|
|
}
|
|
|
|
return blogPosts, categoryMap
|
|
}
|
|
|
|
func processBlogPost(file os.FileInfo, blogDir, outputDir string, tpl *template.Template, nav []NavItem, blogPosts *[]PageMeta, categoryMap map[string][]PageMeta) {
|
|
slug := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
|
blogPath := filepath.Join(blogDir, file.Name())
|
|
rawContent, err := ioutil.ReadFile(blogPath)
|
|
if err != nil {
|
|
fmt.Printf("Failed to read blog post %s: %v\n", file.Name(), err)
|
|
return
|
|
}
|
|
|
|
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)
|
|
|
|
outFile, err := os.Create(outPath)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create blog file %s: %v\n", outPath, err)
|
|
return
|
|
}
|
|
|
|
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",
|
|
})
|
|
|
|
*blogPosts = append(*blogPosts, meta)
|
|
}
|
|
|
|
func buildCategoryPages(categoryMap map[string][]PageMeta, tpl *template.Template, outputDir string, nav []NavItem) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
func parseFrontMatter(raw []byte) (PageMeta, []byte) {
|
|
var meta PageMeta
|
|
raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n"))
|
|
|
|
if bytes.HasPrefix(raw, []byte("---\n")) {
|
|
parts := bytes.SplitN(raw, []byte("---\n"), 3)
|
|
if len(parts) == 3 {
|
|
err := yaml.Unmarshal(parts[1], &meta)
|
|
if err != nil {
|
|
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
|
|
}
|
|
return meta, parts[2]
|
|
}
|
|
}
|
|
|
|
return meta, raw
|
|
}
|
|
|
|
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 {
|
|
fmt.Println("Failed to create blog index:", err)
|
|
return
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|