GoBlog

GoBlog is a flexible blog generation and serving system for creating static blog feeds from Markdown files. It provides a powerful parser for Markdown content with YAML frontmatter, as well as multiple deployment options including a CLI tool, Docker image, and embeddable Go package.
The project is designed for developers who want a simple, Go-based solution for blog generation with support for modern Markdown features like syntax highlighting, footnotes, and custom templates.
Installation
go get github.com/harrydayexe/GoBlog/v2
Quick Start
Using the Parser Package
The parser package reads Markdown files with YAML frontmatter and converts them to structured Post objects:
package main
import (
"fmt"
"log"
"github.com/harrydayexe/GoBlog/v2/pkg/parser"
)
func main() {
// Create a new parser with syntax highlighting enabled
p := parser.New(
parser.WithCodeHighlighting(true),
parser.WithCodeHighlightingStyle("monokai"),
)
// Parse a single markdown file
post, err := p.ParseFile("posts/my-post.md")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Title: %s\n", post.Title)
fmt.Printf("Date: %s\n", post.FormattedDate())
fmt.Printf("Tags: %v\n", post.Tags)
}
Parsing Multiple Posts
// Parse all markdown files in a directory
posts, err := p.ParseDirectory("posts/")
if err != nil {
log.Fatal(err)
}
// Sort by date and filter by tag
posts.SortByDate()
goPosts := posts.FilterByTag("go")
fmt.Printf("Found %d posts about Go\n", len(goPosts))
Documentation
Full API documentation is available at:
Markdown Post Format
Posts are written in Markdown with YAML frontmatter:
---
title: "My Blog Post Title"
date: 2024-01-15
description: "A brief description of the post"
tags: ["go", "blogging", "markdown"]
---
# Post Content
Your blog post content goes here with **full Markdown support**.
\`\`\`go
func main() {
fmt.Println("Code highlighting included!")
}
\`\`\`
CLI Usage
# Generate static blog
goblog gen posts/ output/
# Serve blog locally
goblog serve posts/ --port 8080
Advanced Features
Syntax Highlighting CSS
The parser renders code blocks using CSS classes (via chroma's html.WithClasses option) rather than inline styles. This keeps the generated HTML clean but means you must include a matching stylesheet in your templates.
Generate the stylesheet for any chroma style at startup and embed it in a <style> tag:
import (
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"strings"
)
formatter := chromahtml.New(chromahtml.WithClasses(true))
style := styles.Get("monokai") // must match WithCodeHighlightingStyle
var sb strings.Builder
formatter.WriteCSS(&sb, style)
chromaCSS := sb.String() // embed in a <style> tag in your template
The default style is "monokai". Change it with parser.WithCodeHighlightingStyle("dracula") — just make sure to use the same name when generating the stylesheet.
The CSS class names follow the Pygments short-name convention (.k for keyword, .s for string, .nf for function name, etc.). A full reference is in chroma's types.go.
Raw Output Mode
GoBlog supports raw HTML output mode for advanced use cases where you need direct access to the generated HTML without template wrappers. This is useful when:
- Integrating GoBlog output into an existing site or CMS
- Building custom templates in your own application
- Embedding blog content in other HTML frameworks
- Processing HTML content programmatically before display
Using Raw Output with the CLI
# Generate raw HTML files without templates
goblog gen posts/ output/ --raw
# Or use the short flag
goblog gen posts/ output/ -r
When raw output mode is enabled:
- Individual post files contain only the parsed Markdown converted to HTML
- No template wrapping is applied to the content
- The
tags/ directory is not created (tag pages are skipped)
- The
index.html file is still generated but contains raw HTML
Using Raw Output with the Go API
package main
import (
"context"
"os"
"github.com/harrydayexe/GoBlog/v2/pkg/config"
"github.com/harrydayexe/GoBlog/v2/pkg/generator"
"github.com/harrydayexe/GoBlog/v2/pkg/outputter"
)
func main() {
// Create generator with raw output enabled (no renderer needed in raw mode)
fsys := os.DirFS("posts/")
gen := generator.New(fsys, nil, config.WithRawOutput())
// Generate the blog
blog, err := gen.Generate(context.Background())
if err != nil {
panic(err)
}
// Access raw HTML bytes directly
for slug, htmlContent := range blog.Posts {
// Process or wrap htmlContent as needed
// htmlContent contains only the parsed Markdown as HTML
}
// Write to disk with raw output mode
writer := outputter.NewDirectoryWriter("output/", config.WithRawOutput())
writer.HandleGeneratedBlog(context.Background(), blog)
}
What to Expect from Raw Output
When RawOutput is enabled, the GeneratedBlog structure contains:
Posts map: Keys are post slugs (derived from filenames), values are raw HTML byte slices containing only the Markdown content converted to HTML
Index field: Contains raw HTML bytes for the index page (currently empty in raw mode)
Tags map: Will be empty - tag pages are not generated in raw output mode
The HTML content is clean, semantic HTML generated from your Markdown without any surrounding structure like <html>, <head>, or <body> tags. This gives you complete control over how to integrate the content into your site.
Blog Root Configuration
When deploying your blog at a subdirectory rather than the root of your domain (e.g., example.com/blog/ instead of example.com/), you need to configure the blog root path. This ensures all generated links in templates use the correct base path.
Using Blog Root with the CLI
# Generate blog for deployment at /blog/ subdirectory
goblog gen posts/ output/ --root-path /blog/
# Or use the short flag
goblog gen posts/ output/ -p /blog/
# Serve blog locally with custom root path
goblog serve posts/ --root-path /blog/ --port 8080
When blog root is configured:
- All internal links (navigation, post links, tag links) will use the specified root path
- Links are generated as
/blog/posts/my-post.html instead of /posts/my-post.html
- Home links point to
/blog/ instead of /
- Tag links use
/blog/tags/tag-name.html instead of /tags/tag-name.html
Using Blog Root with the Go API
package main
import (
"context"
"os"
"github.com/harrydayexe/GoBlog/v2/pkg/config"
"github.com/harrydayexe/GoBlog/v2/pkg/generator"
"github.com/harrydayexe/GoBlog/v2/pkg/outputter"
)
func main() {
// Create generator with blog root for subdirectory deployment
fsys := os.DirFS("posts/")
renderer, _ := generator.NewTemplateRenderer(templates.Default)
gen := generator.New(fsys, renderer, config.WithBlogRoot("/blog/"))
// Generate the blog
blog, err := gen.Generate(context.Background())
if err != nil {
panic(err)
}
// Write to disk - all links will use /blog/ prefix
writer := outputter.NewDirectoryWriter("output/")
writer.HandleGeneratedBlog(context.Background(), blog)
}
Common Use Cases
Root Deployment (Default):
# Blog at example.com/
goblog gen posts/ output/
# Links: /posts/slug.html, /tags/tag.html, /
Subdirectory Deployment:
# Blog at example.com/blog/
goblog gen posts/ output/ --root-path /blog/
# Links: /blog/posts/slug.html, /blog/tags/tag.html, /blog/
Nested Subdirectory:
# Blog at example.com/docs/blog/
goblog gen posts/ output/ --root-path /docs/blog/
# Links: /docs/blog/posts/slug.html, /docs/blog/tags/tag.html, /docs/blog/
Custom Templates
GoBlog ships with built-in templates (templates.Default) but you can supply
any fs.FS — e.g. os.DirFS("./mytheme") — to generator.NewTemplateRenderer
for a fully custom theme.
Required directory layout
Your template root must contain:
mytheme/
pages/
post.tmpl receives models.PostPageData
index.tmpl receives models.IndexPageData
tag.tmpl receives models.TagPageData
tags-index.tmpl receives models.TagsIndexPageData
partials/
head.tmpl must {{define "head"}}
header.tmpl must {{define "header"}}
footer.tmpl must {{define "footer"}}
post-card.tmpl must {{define "post-card"}}
layouts/ optional — loaded but not executed by default
Each page template is a complete HTML document. It pulls in partials with:
{{template "head" .}}
{{template "header" .}}
{{template "footer" .}}
The post-card partial is referenced inside index.tmpl and tag.tmpl when
ranging over a list of posts.
Template data
Every page template receives a struct that embeds
models.BaseData
(SiteTitle, PageTitle, Description, Year, BlogRoot), plus
page-specific fields:
| Template |
Data struct |
Extra fields |
pages/post.tmpl |
PostPageData |
.Post *Post |
pages/index.tmpl |
IndexPageData |
.Posts PostList, .TotalPosts int |
pages/tag.tmpl |
TagPageData |
.Tag string, .Posts []*Post, .PostCount int |
pages/tags-index.tmpl |
TagsIndexPageData |
.Tags []TagInfo, .TotalTags int |
Template helpers (FuncMap)
| Name |
Signature |
Example output |
formatDate |
func(t time.Time) string |
"January 2, 2006" |
shortDate |
func(t time.Time) string |
"Jan 2, 2006" |
year |
func() int |
2025 |
Go API example
package main
import (
"context"
"os"
"github.com/harrydayexe/GoBlog/v2/pkg/config"
"github.com/harrydayexe/GoBlog/v2/pkg/generator"
"github.com/harrydayexe/GoBlog/v2/pkg/outputter"
)
func main() {
// Use a custom template directory
renderer, err := generator.NewTemplateRenderer(os.DirFS("./mytheme"))
if err != nil {
panic(err)
}
fsys := os.DirFS("posts/")
gen := generator.New(fsys, renderer, config.WithBlogRoot("/blog/"))
blog, err := gen.Generate(context.Background())
if err != nil {
panic(err)
}
writer := outputter.NewDirectoryWriter("output/")
writer.HandleGeneratedBlog(context.Background(), blog)
}
To use the built-in templates instead, replace os.DirFS("./mytheme") with
templates.Default (from github.com/harrydayexe/GoBlog/v2/pkg/templates).
Docker Usage
# Run blog server in container
docker run -v ./posts:/posts -p 8080:8080 goblog/goblog serve /posts
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
License
GNU General Public License v3.0 - see LICENSE file for details.