✨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
|
||||
|
||||
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
|
||||
|
||||
This is your first static site built with Go!
|
||||
|
||||
201
main.go
201
main.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
@@ -10,81 +11,80 @@ import (
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
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 := parseTemplate(templateFile)
|
||||
tpl, err := template.ParseFiles(templateFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Template parsing error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
files, err := readContentFiles(contentDir)
|
||||
files, err := ioutil.ReadDir(contentDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read content directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
nav := buildNavigation(files)
|
||||
|
||||
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 {
|
||||
// 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()))
|
||||
title := strings.Title(name)
|
||||
url := "/"
|
||||
if name != "index" {
|
||||
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()))
|
||||
title := strings.Title(name)
|
||||
|
||||
mdPath := filepath.Join(contentDir, file.Name())
|
||||
mdContent, err := ioutil.ReadFile(mdPath)
|
||||
rawContent, err := ioutil.ReadFile(mdPath)
|
||||
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
|
||||
if name == "index" {
|
||||
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)
|
||||
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{}{
|
||||
"Title": meta.Title,
|
||||
"Description": meta.Description,
|
||||
"Date": meta.Date,
|
||||
"Content": template.HTML(htmlContent),
|
||||
"Title": title,
|
||||
"Nav": nav,
|
||||
"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)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>About</title>
|
||||
<title>Go SSG - About</title>
|
||||
<meta name="description" content="More About My Go SSG Site.">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -15,15 +16,19 @@
|
||||
|
||||
<li><a href="/">Index</a></li>
|
||||
|
||||
<li><a href="/blog/">Blog</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h1>About
|
||||
<small>Published: 2025-04-19</small>
|
||||
<h1>About</h1>
|
||||
|
||||
<p>Experimenting with Go. Seems pretty cool so far!</p>
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
|
||||
</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>
|
||||
<meta charset="UTF-8">
|
||||
<title>Index</title>
|
||||
<title>Go SSG - Home</title>
|
||||
<meta name="description" content="My Go SSG Site.">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -15,15 +16,19 @@
|
||||
|
||||
<li><a href="/">Index</a></li>
|
||||
|
||||
<li><a href="/blog/">Blog</a></li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ .Title }}</title>
|
||||
{{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -18,7 +19,23 @@
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user