Files
go-SSG/main.go
Keith Solomon 1a9b58d7d7 Add Contact Page and Update Navigation Links
- Updated navigation links across multiple blog pages to replace "Index" with "Home" and added "Contact" link.
- Created a new contact page with a form for user inquiries.
- Added a new contact template to render the contact page content.
- Updated blog index page to enhance structure and styling.
- Added a contact markdown file for content management.
2025-04-21 07:15:03 -05:00

342 lines
9.0 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
}
type PageMeta struct {
Title string `yaml:"title"`
NavTitle string `yaml:"navTitle"`
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)
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)
title := meta.NavTitle
if title == "" {
title = meta.Title
}
if title == "" && name == "index" {
title = "Home"
} else if title == "" {
title = strings.Title(name)
}
url := "/"
if name != "index" {
url = "/" + name + "/"
}
nav = append(nav, NavItem{Title: title, URL: url})
}
// Add Blog manually
nav = append(nav, NavItem{Title: "Blog", URL: "/blog/"})
// Optional: order the nav explicitly
preferredOrder := map[string]int{
"Home": 0,
"Blog": 1,
"About": 2,
"Contact": 3,
}
sort.SliceStable(nav, func(i, j int) bool {
return preferredOrder[nav[i].Title] < preferredOrder[nav[j].Title]
})
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 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
}
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,
}
}