Files
go-SSG/main.go
2025-04-19 15:48:56 -05:00

234 lines
5.8 KiB
Go

package main
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/russross/blackfriday/v2"
"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
}
func main() {
contentDir := "./content"
outputDir := "./public"
templateFile := "./templates/layout.html"
tpl, err := template.ParseFiles(templateFile)
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
}
// Collect nav info early
var nav []NavItem
for _, file := range files {
if filepath.Ext(file.Name()) == ".md" {
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
url := "/"
if name != "index" {
url = "/" + name + "/"
}
nav = append(nav, NavItem{
Title: strings.Title(name),
URL: url,
})
}
}
// Add blog to the main nav
nav = append(nav, NavItem{
Title: "Blog",
URL: "/blog/",
})
// Generate index.html for each static page markdown file
for _, file := range files {
if filepath.Ext(file.Name()) != ".md" {
continue
}
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
blogDir := filepath.Join(contentDir, "blog")
blogFiles, _ := ioutil.ReadDir(blogDir)
for _, file := range blogFiles {
if filepath.Ext(file.Name()) != ".md" {
continue
}
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)
continue
}
meta, content := parseFrontMatter(rawContent)
html := blackfriday.Run(content)
meta.Slug = slug
if meta.Title == "" {
meta.Title = strings.Title(slug)
}
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)
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(),
})
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)
}
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
err := yaml.Unmarshal(parts[1], &meta)
if err != nil {
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
}
return meta, parts[2]
}
}
// 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 {
outPath := filepath.Join(outputDir, "blog", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath)
if err != nil {
return err
}
data := map[string]interface{}{
"Title": "Go SSG - Blog",
"Posts": posts,
"Nav": nav,
"Year": time.Now().Year(),
}
return tpl.Execute(outFile, data)
}