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) }