✨feature: Wire up blog section
This commit is contained in:
@@ -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!
|
||||||
|
|||||||
9
content/blog/my-first-blog-post.md
Normal file
9
content/blog/my-first-blog-post.md
Normal 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!
|
||||||
@@ -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!
|
||||||
|
|||||||
201
main.go
201
main.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -10,81 +11,80 @@ 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
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if filepath.Ext(file.Name()) == ".md" {
|
|
||||||
err := generateHTML(file, contentDir, outputDir, tpl, nav)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error generating HTML for %s: %v\n", file.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("✅ Site generation complete.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTemplate(templateFile string) (*template.Template, error) {
|
|
||||||
return template.ParseFiles(templateFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readContentFiles(contentDir string) ([]os.FileInfo, error) {
|
|
||||||
return ioutil.ReadDir(contentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildNavigation(files []os.FileInfo) []NavItem {
|
|
||||||
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" {
|
||||||
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
|
||||||
title := strings.Title(name)
|
|
||||||
url := "/"
|
url := "/"
|
||||||
if name != "index" {
|
if name != "index" {
|
||||||
url = "/" + name + "/"
|
url = "/" + name + "/"
|
||||||
}
|
}
|
||||||
nav = append(nav, NavItem{Title: title, URL: url})
|
nav = append(nav, NavItem{
|
||||||
|
Title: strings.Title(name),
|
||||||
|
URL: url,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nav
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateHTML(file os.FileInfo, contentDir, outputDir string, tpl *template.Template, nav []NavItem) error {
|
// 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()))
|
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())
|
||||||
mdContent, err := ioutil.ReadFile(mdPath)
|
rawContent, err := ioutil.ReadFile(mdPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read %s: %w", file.Name(), err)
|
fmt.Printf("Failed to read %s: %v\n", file.Name(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlContent := blackfriday.Run(mdContent)
|
meta, content := parseFrontMatter(rawContent)
|
||||||
|
htmlContent := blackfriday.Run(content)
|
||||||
|
|
||||||
|
// 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")
|
||||||
@@ -96,16 +96,137 @@ func generateHTML(file os.FileInfo, contentDir, outputDir string, tpl *template.
|
|||||||
|
|
||||||
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)
|
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)
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
|
"Title": meta.Title,
|
||||||
|
"Description": meta.Description,
|
||||||
|
"Date": meta.Date,
|
||||||
"Content": template.HTML(htmlContent),
|
"Content": template.HTML(htmlContent),
|
||||||
"Title": title,
|
|
||||||
"Nav": nav,
|
"Nav": nav,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
"Date": time.Now().Format("January 2, 2006"),
|
}
|
||||||
|
|
||||||
|
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)
|
return tpl.Execute(outFile, data)
|
||||||
|
|||||||
@@ -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
50
public/blog/index.html
Normal 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>© 2025 Keith Solomon</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
40
public/blog/my-first-blog-post/index.html
Normal file
40
public/blog/my-first-blog-post/index.html
Normal 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>© 2025 Keith Solomon</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user