feature: Set up templated views and blog support

This commit is contained in:
Keith Solomon
2025-04-20 15:39:22 -05:00
parent 463250eb7b
commit 3fe75e9ad5
20 changed files with 580 additions and 255 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
bak/

View File

@@ -0,0 +1,20 @@
---
title: Go SSG - Another Blog Post
description: A longer summary of the post. Lorem ipsum dolor set amit.
date: 2025-04-19
categories:
- go
- anger
---
# Why isn't this working?! 😭
This is my first Go SSG blog post.
This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.
It supports features like:
- Markdown content
- Customizable templates
- Simple configuration

View File

@@ -2,8 +2,19 @@
title: Go SSG - My First Blog Post title: Go SSG - My First Blog Post
description: A short summary of the post. description: A short summary of the post.
date: 2025-04-19 date: 2025-04-19
categories:
- progrmming
- go
--- ---
# *Tap Tap Tap* Is this thing on? # *Tap Tap Tap* Is this thing on?
This is my first blog post! This is my first Go SSG blog post.
This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.
It supports features like:
- Markdown content
- Customizable templates
- Simple configuration

138
main.go
View File

@@ -1,3 +1,4 @@
package main package main
import ( import (
@@ -7,6 +8,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@@ -14,26 +16,27 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// 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 { type PageMeta struct {
Title string `yaml:"title"` Title string `yaml:"title"`
Description string `yaml:"description"` Description string `yaml:"description"`
Date string `yaml:"date"` Date string `yaml:"date"`
Slug string // we'll set this ourselves from filename Categories []string `yaml:"categories"`
Slug string
} }
var slugRegex = regexp.MustCompile(`[^a-z0-9]+`)
func main() { func main() {
contentDir := "./content" contentDir := "./content"
outputDir := "./public" outputDir := "./public"
templateFile := "./templates/layout.html"
tpl, err := template.ParseFiles(templateFile) // Load all templates in the templates folder
tpl, err := template.New("").Funcs(templateFuncs()).ParseGlob("templates/*.html")
if err != nil { if err != nil {
fmt.Printf("Template parsing error: %v\n", err) fmt.Printf("Template parsing error: %v\n", err)
return return
@@ -45,7 +48,6 @@ func main() {
return return
} }
// Collect nav info early
var nav []NavItem var nav []NavItem
for _, file := range files { for _, file := range files {
if filepath.Ext(file.Name()) == ".md" { if filepath.Ext(file.Name()) == ".md" {
@@ -54,26 +56,18 @@ func main() {
if name != "index" { if name != "index" {
url = "/" + name + "/" url = "/" + name + "/"
} }
nav = append(nav, NavItem{ nav = append(nav, NavItem{Title: strings.Title(name), URL: url})
Title: strings.Title(name),
URL: url,
})
} }
} }
// Add blog to the main nav // Add blog to main nav
nav = append(nav, NavItem{ nav = append(nav, NavItem{Title: "Blog", URL: "/blog/"})
Title: "Blog",
URL: "/blog/",
})
// Generate index.html for each static page markdown file // Static page rendering
for _, file := range files { for _, file := range files {
if filepath.Ext(file.Name()) != ".md" { if filepath.Ext(file.Name()) == ".md" {
continue
}
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())) name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
title := strings.Title(name)
mdPath := filepath.Join(contentDir, file.Name()) mdPath := filepath.Join(contentDir, file.Name())
rawContent, err := ioutil.ReadFile(mdPath) rawContent, err := ioutil.ReadFile(mdPath)
if err != nil { if err != nil {
@@ -83,8 +77,10 @@ func main() {
meta, content := parseFrontMatter(rawContent) meta, content := parseFrontMatter(rawContent)
htmlContent := blackfriday.Run(content) htmlContent := blackfriday.Run(content)
if meta.Title == "" {
meta.Title = title
}
// Determine output path
var outPath string var outPath string
if name == "index" { if name == "index" {
outPath = filepath.Join(outputDir, "index.html") outPath = filepath.Join(outputDir, "index.html")
@@ -100,31 +96,23 @@ func main() {
continue continue
} }
// Fall back to filename for title if none provided tpl.ExecuteTemplate(outFile, "static_page", map[string]interface{}{
if meta.Title == "" {
meta.Title = strings.Title(name)
}
data := map[string]interface{}{
"Title": meta.Title, "Title": meta.Title,
"Description": meta.Description, "Description": meta.Description,
"Date": meta.Date, "Date": meta.Date,
"Categories": meta.Categories,
"Content": template.HTML(htmlContent), "Content": template.HTML(htmlContent),
"Nav": nav, "Nav": nav,
"Year": time.Now().Year(), "Year": time.Now().Year(),
} "PageTemplate": "static",
})
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) fmt.Printf("Generated: %s\n", outPath)
} }
}
// Generate blog index
var blogPosts []PageMeta var blogPosts []PageMeta
categoryMap := make(map[string][]PageMeta)
blogDir := filepath.Join(contentDir, "blog") blogDir := filepath.Join(contentDir, "blog")
blogFiles, _ := ioutil.ReadDir(blogDir) blogFiles, _ := ioutil.ReadDir(blogDir)
@@ -144,12 +132,16 @@ func main() {
meta, content := parseFrontMatter(rawContent) meta, content := parseFrontMatter(rawContent)
html := blackfriday.Run(content) html := blackfriday.Run(content)
meta.Slug = slug meta.Slug = slug
if meta.Title == "" { if meta.Title == "" {
meta.Title = strings.Title(slug) 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") outPath := filepath.Join(outputDir, "blog", slug, "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm) os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
@@ -159,47 +151,54 @@ func main() {
continue continue
} }
err = tpl.Execute(outFile, map[string]interface{}{ tpl.ExecuteTemplate(outFile, "blog_post_page", map[string]interface{}{
"Title": meta.Title, "Title": meta.Title,
"Description": meta.Description, "Description": meta.Description,
"Date": meta.Date, "Date": meta.Date,
"Categories": meta.Categories,
"Content": template.HTML(html), "Content": template.HTML(html),
"Nav": nav, "Nav": nav,
"Year": time.Now().Year(), "Year": time.Now().Year(),
"PageTemplate": "blog_post",
}) })
if err != nil {
fmt.Printf("Template error for blog post %s: %v\n", file.Name(), err)
continue
}
blogPosts = append(blogPosts, meta) blogPosts = append(blogPosts, meta)
} }
err = buildBlogIndex(blogPosts, tpl, outputDir, nav) buildBlogIndex(blogPosts, tpl, outputDir, nav)
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 { if err != nil {
fmt.Println("Failed to build blog index:", err) 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)
} }
fmt.Println("✅ Site generation complete.") fmt.Println("✅ Site generation complete.")
} }
// parseFrontMatter splits raw markdown into meta + content
func parseFrontMatter(raw []byte) (PageMeta, []byte) { func parseFrontMatter(raw []byte) (PageMeta, []byte) {
var meta PageMeta var meta PageMeta
// Normalize line endings
raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n")) 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")) { if bytes.HasPrefix(raw, []byte("---\n")) {
// Split after the first two "---\n" lines
parts := bytes.SplitN(raw, []byte("---\n"), 3) parts := bytes.SplitN(raw, []byte("---\n"), 3)
if len(parts) == 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) err := yaml.Unmarshal(parts[1], &meta)
if err != nil { if err != nil {
fmt.Printf("⚠️ Failed to parse front matter: %v\n", err) fmt.Printf("⚠️ Failed to parse front matter: %v\n", err)
@@ -208,26 +207,39 @@ func parseFrontMatter(raw []byte) (PageMeta, []byte) {
} }
} }
// If there's no front matter, return raw as-is
return meta, raw return meta, raw
} }
// buildBlogIndex generates the blog index page func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) {
func buildBlogIndex(posts []PageMeta, tpl *template.Template, outputDir string, nav []NavItem) error {
outPath := filepath.Join(outputDir, "blog", "index.html") outPath := filepath.Join(outputDir, "blog", "index.html")
os.MkdirAll(filepath.Dir(outPath), os.ModePerm) os.MkdirAll(filepath.Dir(outPath), os.ModePerm)
outFile, err := os.Create(outPath) outFile, err := os.Create(outPath)
if err != nil { if err != nil {
return err fmt.Println("Failed to create blog index:", err)
return
} }
data := map[string]interface{}{ fmt.Printf("Rendering blog index: %d posts\n", len(posts))
"Title": "Go SSG - Blog", tpl.ExecuteTemplate(outFile, "blog_index_page", map[string]interface{}{
"Title": "Blog Index",
"Posts": posts, "Posts": posts,
"Nav": nav, "Nav": nav,
"Year": time.Now().Year(), "Year": time.Now().Year(),
} "PageTemplate": "blog_index",
})
return tpl.Execute(outFile, data) }
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,
}
} }

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Go SSG - About</title> <title>Go SSG - About</title>
<meta name="description" content="More About My Go SSG Site.">
</head> </head>
<body> <body>
<header>
<header>
<nav> <nav>
<ul> <ul>
@@ -20,21 +20,24 @@
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<small>Published: 2025-04-19</small> <article>
<h1>Go SSG - About</h1>
<h1>About</h1> <h1>About</h1>
<p>Experimenting with Go. Seems pretty cool so far!</p> <p>Experimenting with Go. Seems pretty cool so far!</p>
</article>
</main>
</main> <footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go SSG - Another Blog Post</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>
<article>
<h1>Go SSG - Another Blog Post</h1>
<p><small>2025-04-19</small></p>
<p>
Categories:
<a href="/blog/category/go/">go</a>
,
<a href="/blog/category/anger/">anger</a>
</p>
<h1>Why isn&rsquo;t this working?! 😭</h1>
<p>This is my first Go SSG blog post.</p>
<p>This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.</p>
<p>It supports features like:</p>
<ul>
<li>Markdown content</li>
<li>Customizable templates</li>
<li>Simple configuration</li>
</ul>
</article>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Anger</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>
<h1>Category: Anger</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
</ul>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Go</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>
<h1>Category: Go</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Category: Progrmming</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>
<h1>Category: Progrmming</h1>
<ul>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Go SSG - Blog</title> <title>Blog Index</title>
</head> </head>
<body> <body>
<header>
<header>
<nav> <nav>
<ul> <ul>
@@ -20,31 +20,25 @@
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<h1>Blog Index</h1>
<ul>
<li><a href="/blog/another-blog-post/">Go SSG - Another Blog Post</a><br>A longer summary of the post. Lorem ipsum dolor set amit.</li>
<li><a href="/blog/my-first-blog-post/">Go SSG - My First Blog Post</a><br>A short summary of the post.</li>
</ul>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
<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> </body>
</html> </html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Go SSG - My First Blog Post</title> <title>Go SSG - My First Blog Post</title>
<meta name="description" content="A short summary of the post.">
</head> </head>
<body> <body>
<header>
<header>
<nav> <nav>
<ul> <ul>
@@ -20,21 +20,49 @@
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<small>Published: 2025-04-19</small> <article>
<h1>Go SSG - My First Blog Post</h1>
<p><small>2025-04-19</small></p>
<p>
Categories:
<a href="/blog/category/progrmming/">progrmming</a>
,
<a href="/blog/category/go/">go</a>
</p>
<h1><em>Tap Tap Tap</em> Is this thing on?</h1> <h1><em>Tap Tap Tap</em> Is this thing on?</h1>
<p>This is my first blog post!</p> <p>This is my first Go SSG blog post.</p>
<p>This project is a simple static site generator written in Go. It is designed to be easy to use and extend, allowing you to create a static website quickly and efficiently.</p>
<p>It supports features like:</p>
<ul>
<li>Markdown content</li>
<li>Customizable templates</li>
<li>Simple configuration</li>
</ul>
</article>
</main>
<footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
</main>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body> </body>
</html> </html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Go SSG - Home</title> <title>Go SSG - Home</title>
<meta name="description" content="My Go SSG Site.">
</head> </head>
<body> <body>
<header>
<header>
<nav> <nav>
<ul> <ul>
@@ -20,21 +20,24 @@
</ul> </ul>
</nav> </nav>
</header> </header>
<main> <main>
<small>Published: 2025-04-19</small> <article>
<h1>Go SSG - Home</h1>
<h1>Welcome</h1> <h1>Welcome</h1>
<p>This is your first static site built with Go!</p> <p>This is your first static site built with Go!</p>
</article>
</main>
</main> <footer>
<p>&copy; 2025 Keith Solomon - Go SSG</p>
</footer>
<footer>
<p>&copy; 2025 Keith Solomon</p>
</footer>
</body> </body>
</html> </html>

28
templates/base.html Normal file
View File

@@ -0,0 +1,28 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
</head>
<body>
{{ template "header" . }}
<main>
{{- if eq .PageTemplate "blog_index" -}}
{{ template "blog_index_content" . }}
{{- else if eq .PageTemplate "static" -}}
{{ template "static_content" . }}
{{- else if eq .PageTemplate "blog_post" -}}
{{ template "blog_post_content" . }}
{{- else if eq .PageTemplate "category_page" -}}
{{ template "category_content" . }}
{{- else -}}
<p>Unknown PageTemplate: {{ .PageTemplate }}</p>
{{- end -}}
</main>
{{ template "footer" . }}
</body>
</html>
{{ end }}

View File

@@ -0,0 +1,14 @@
{{ define "blog_index_page" }}
{{ template "base" . }}
{{ end }}
{{ define "blog_index_content" }}
<h1>Blog Index</h1>
<ul>
{{ range .Posts }}
<li><a href="/blog/{{ .Slug }}/">{{ .Title }}</a><br>{{ .Description }}</li>
{{ else }}
<li>No posts found.</li>
{{ end }}
</ul>
{{ end }}

View File

@@ -0,0 +1,22 @@
{{ define "blog_post_page" }}
{{ template "base" . }}
{{ end }}
{{ define "blog_post_content" }}
<article>
<h1>{{ .Title }}</h1>
{{ if .Date }}<p><small>{{ .Date }}</small></p>{{ end }}
{{ if .Categories }}
<p>
Categories:
{{ range $i, $cat := .Categories }}
{{ if $i }}, {{ end }}
<a href="/blog/category/{{ $cat | slugify }}/">{{ $cat }}</a>
{{ end }}
</p>
{{ end }}
{{ .Content }}
</article>
{{ end }}

View File

@@ -0,0 +1,12 @@
{{ define "category_page" }}
{{ template "base" . }}
{{ end }}
{{ define "category_content" }}
<h1>{{ .Title }}</h1>
<ul>
{{ range .Posts }}
<li><a href="/blog/{{ .Slug }}/">{{ .Title }}</a><br>{{ .Description }}</li>
{{ end }}
</ul>
{{ end }}

5
templates/footer.html Normal file
View File

@@ -0,0 +1,5 @@
{{ define "footer" }}
<footer>
<p>&copy; {{ .Year }} Keith Solomon - Go SSG</p>
</footer>
{{ end }}

11
templates/header.html Normal file
View File

@@ -0,0 +1,11 @@
{{ define "header" }}
<header>
<nav>
<ul>
{{ range .Nav }}
<li><a href="{{ .URL }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</nav>
</header>
{{ end }}

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Title }}</title>
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
</head>
<body>
<header>
<nav>
<ul>
{{ range .Nav }}
<li><a href="{{ .URL }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</nav>
</header>
<main>
{{ if .Date }}<small>Published: {{ .Date }}</small>{{ end }}
{{ .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>
<footer>
<p>&copy; {{ .Year }} Keith Solomon</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,10 @@
{{ define "static_page" }}
{{ template "base" . }}
{{ end }}
{{ define "static_content" }}
<article>
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
{{ end }}