feature: Wire up blog section

This commit is contained in:
Keith Solomon
2025-04-19 15:48:56 -05:00
parent 472aad6bc6
commit 463250eb7b
9 changed files with 379 additions and 120 deletions

View File

@@ -1,3 +1,9 @@
---
title: Go SSG - About
description: More About My Go SSG Site.
date: 2025-04-19
---
# About # About
Experimenting with Go. Seems pretty cool so far! Experimenting with Go. Seems pretty cool so far!

View File

@@ -0,0 +1,9 @@
---
title: Go SSG - My First Blog Post
description: A short summary of the post.
date: 2025-04-19
---
# *Tap Tap Tap* Is this thing on?
This is my first blog post!

View File

@@ -1,3 +1,9 @@
---
title: Go SSG - Home
description: My Go SSG Site.
date: 2025-04-19
---
# Welcome # Welcome
This is your first static site built with Go! This is your first static site built with Go!

225
main.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
@@ -10,102 +11,222 @@ import (
"time" "time"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
"gopkg.in/yaml.v3"
) )
// NavItem represents a link in the navigation menu // NavItem represents a navigation link
type NavItem struct { type NavItem struct {
Title string Title string
URL 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() { func main() {
contentDir := "./content" contentDir := "./content"
outputDir := "./public" outputDir := "./public"
templateFile := "./templates/layout.html" templateFile := "./templates/layout.html"
tpl, err := parseTemplate(templateFile) tpl, err := template.ParseFiles(templateFile)
if err != nil { if err != nil {
fmt.Printf("Template parsing error: %v\n", err) fmt.Printf("Template parsing error: %v\n", err)
return return
} }
files, err := readContentFiles(contentDir) files, err := ioutil.ReadDir(contentDir)
if err != nil { if err != nil {
fmt.Printf("Failed to read content directory: %v\n", err) fmt.Printf("Failed to read content directory: %v\n", err)
return return
} }
nav := buildNavigation(files) // Collect nav info early
var nav []NavItem
for _, file := range files { for _, file := range files {
if filepath.Ext(file.Name()) == ".md" { if filepath.Ext(file.Name()) == ".md" {
err := generateHTML(file, contentDir, outputDir, tpl, nav) name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
if err != nil { url := "/"
fmt.Printf("Error generating HTML for %s: %v\n", file.Name(), err) 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.") fmt.Println("✅ Site generation complete.")
} }
func parseTemplate(templateFile string) (*template.Template, error) { // parseFrontMatter splits raw markdown into meta + content
return template.ParseFiles(templateFile) func parseFrontMatter(raw []byte) (PageMeta, []byte) {
} var meta PageMeta
func readContentFiles(contentDir string) ([]os.FileInfo, error) { // Normalize line endings
return ioutil.ReadDir(contentDir) raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n"))
}
func buildNavigation(files []os.FileInfo) []NavItem { // Only try to parse if the file starts with "---\n"
var nav []NavItem if bytes.HasPrefix(raw, []byte("---\n")) {
for _, file := range files { // Split after the first two "---\n" lines
if filepath.Ext(file.Name()) == ".md" { parts := bytes.SplitN(raw, []byte("---\n"), 3)
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
title := strings.Title(name) if len(parts) >= 3 {
url := "/" // parts[0] = empty (before first ---)
if name != "index" { // parts[1] = YAML content
url = "/" + name + "/" // parts[2] = remaining markdown content
err := yaml.Unmarshal(parts[1], &meta)
if err != nil {
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
} }
nav = append(nav, NavItem{Title: title, URL: url}) return meta, parts[2]
} }
} }
return nav
// If there's no front matter, return raw as-is
return meta, raw
} }
func generateHTML(file os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) error { // buildBlogIndex generates the blog index page
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())) func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) error {
title := strings.Title(name) outPath := filepath.Join(outputDir, "blog", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
mdPath := filepath.Join(contentDir, file.Name())
mdContent, err := ioutil.ReadFile(mdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", file.Name(), err)
}
htmlContent := blackfriday.Run(mdContent)
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) outFile, err := os.Create(outPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create %s: %w", outPath, err) return err
} }
defer outFile.Close()
data := map[string]interface{}{ data := map[string]interface{}{
"Content": template.HTML(htmlContent), "Title": "Go SSG - Blog",
"Title": title, "Posts": posts,
"Nav": nav, "Nav": nav,
"Year": time.Now().Year(), "Year": time.Now().Year(),
"Date": time.Now().Format("January 2, 2006"),
} }
return tpl.Execute(outFile, data) return tpl.Execute(outFile, data)

View File

@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>About</title> <title>Go SSG - About</title>
<meta name="description" content="More About My Go SSG Site.">
</head> </head>
<body> <body>
@@ -15,15 +16,19 @@
<li><a href="/">Index</a></li> <li><a href="/">Index</a></li>
<li><a href="/blog/">Blog</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<h1>About <small>Published: 2025-04-19</small>
<h1>About</h1>
<p>Experimenting with Go. Seems pretty cool so far!</p>
<p>
</main> </main>

50
public/blog/index.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - Blog</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/about/">About</a></li>
<li><a href="/">Index</a></li>
<li><a href="/blog/">Blog</a></li>
</ul>
</nav>
</header>
<main>
<section>
<h2>Blog Posts</h2>
<ul>
<li>
<a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a>
<small>2025-04-19</small><br>
A short summary of the post.
</li>
</ul>
</section>
</main>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - My First Blog Post</title>
<meta name="description" content="A short summary of the post.">
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/about/">About</a></li>
<li><a href="/">Index</a></li>
<li><a href="/blog/">Blog</a></li>
</ul>
</nav>
</header>
<main>
<small>Published: 2025-04-19</small>
<h1><em>Tap Tap Tap</em> Is this thing on?</h1>
<p>This is my first blog post!</p>
</main>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body>
</html>

View File

@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Index</title> <title>Go SSG - Home</title>
<meta name="description" content="My Go SSG Site.">
</head> </head>
<body> <body>
@@ -15,15 +16,19 @@
<li><a href="/">Index</a></li> <li><a href="/">Index</a></li>
<li><a href="/blog/">Blog</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<h1>Welcome <small>Published: 2025-04-19</small>
<h1>Welcome</h1>
<p>This is your first static site built with Go!</p>
<p>
</main> </main>

View File

@@ -4,6 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
</head> </head>
<body> <body>
@@ -18,7 +19,23 @@
</header> </header>
<main> <main>
{{ if .Date }}<small>Published: {{ .Date }}</small>{{ end }}
{{ .Content }} {{ .Content }}
{{ if .Posts }}
<section>
<h2>Blog Posts</h2>
<ul>
{{ range .Posts }}
<li>
<a href="/blog/{{ .Slug }}/">{{ .Title }}</a>
{{ if .Date }} <small>{{ .Date }}</small>{{ end }}<br>
{{ .Description }}
</li>
{{ end }}
</ul>
</section>
{{ end }}
</main> </main>
<footer> <footer>