Simple, fast and extendable static site generator.
SSGO is a minimal, Go-native static site generator focused on:
- π§© Composability β every page is generated via a clear data + template pipeline
- β‘ Simplicity β no magic or custom config formats
- π§ Extensibility β plug in your own rendering or writing logic
Install via:
go get github.com/janmarkuslanger/ssgo
Or download directly:
β¬ Download latest ZIP
In this example we use the default implementation of the renderer (https://pkg.go.dev/html/template) and the writer.
If you use the default html renderer. Your root template must start with a {{ define "root" }}
.
Create the following structure:
your-project/
βββ main.go
βββ templates/
β βββ layout.html
β βββ blog.html
βββ public/ (generated after running)
package main
import (
"log"
"github.com/janmarkuslanger/ssgo/builder"
"github.com/janmarkuslanger/ssgo/page"
"github.com/janmarkuslanger/ssgo/rendering"
"github.com/janmarkuslanger/ssgo/writer"
)
func main() {
renderer := rendering.HTMLRenderer{
Layout: []string{"templates/layout.html"},
CustomFuncs: template.FuncMap{
"upper": strings.ToUpper,
},
}
posts := map[string]map[string]any{
"hello-world": {
"Title": "Hello World",
"Content": "Welcome to my blog!",
},
"second-post": {
"Title": "Second Post",
"Content": "Another blog entry.",
},
}
generator := page.Generator{
Config: page.Config{
Pattern: "/blog/:slug",
Template: "templates/blog.html",
GetPaths: func() []string {
return []string{"/blog/hello-world", "/blog/second-post"}
},
GetData: func(p page.PagePayload) map[string]any {
return posts[p.Params["slug"]]
},
Renderer: renderer,
},
}
b := builder.Builder{
OutputDir: "public",
Writer: &writer.FileWriter{},
Pages: []page.Generator{
generator,
},
}
if err := b.Build(); err != nil {
log.Fatal(err)
}
}
{{ define "root" }}
<!DOCTYPE html>
<html>
<head><title>{{ .Title }}</title></head>
<body>
<h1>{{ upper .Title }}</h1>
{{ template "content" . }}
</body>
</html>
{{ end }}
{{ define "content" }}
<h1>{{ .Title }}</h1>
<p>{{ .Content }}</p>
{{ end }}
go run main.go
β Two files will be generated:
public/
βββ blog/
βββ hello-world
βββ second-post
Orchestrates the generation of pages and writes them to disk:
type Builder struct {
OutputDir string
Writer Writer
Generators []page.Generator
}
Each page.Generator
is driven by a Config
:
type Config struct {
Template string
Pattern string
GetPaths func() []string
GetData func(PagePayload) map[string]any
Renderer rendering.Renderer
}
This allows dynamic paths with params like /blog/:slug
.
Passed to GetData
so you can access dynamic URL parameters:
type PagePayload struct {
Path string
Params map[string]string
}
The Writer
interface is used to persist rendered output:
type Writer interface {
Write(path string, content string) error
}
Default implementation:
type FileWriter struct{}
func (FileWriter) Write(path string, content string) error {
_ = os.MkdirAll(filepath.Dir(path), 0755)
return os.WriteFile(path, []byte(content), 0644)
}
Swap this out to write to memory, S3, etc.
Rendering is abstracted via this interface:
type Renderer interface {
Render(RenderContext) (string, error)
}
The built-in HTMLRenderer
supports:
- Go templates (
html/template
) - Layouts (via
[]string
) - Custom data injection
- Custom template funcs
You can register custom tasks to be executed before or after the build.
A task must implement the following interface:
type Task interface {
Run(ctx TaskContext) error
IsCritical() bool
}
type TaskContext struct {
OutputDir string
}
This allows you to perform custom logic like preparing directories, copying assets, or generating extra files.
type PrintTask struct{}
func (PrintTask) Run(ctx task.TaskContext) error {
fmt.Println("Building to:", ctx.OutputDir)
return nil
}
func (PrintTask) IsCritical() bool {
return false
}
Register the task:
builder := builder.Builder{
OutputDir: "public",
Writer: &writer.FileWriter{},
Pages: []page.Generator{...},
BeforeTasks: []task.Task{
PrintTask{},
},
}
CopyTask
is a built-in task in the taskutil
package that copies all files from a given SourceDir
into the builder's OutputDir
.
Optionally, you can specify an OutputSubDir
to copy into a subfolder.
Always use the constructor NewCopyTask(...)
β do not initialize the struct manually with {}
.
Suppose you have a static/
directory with images or icons you want to copy into the output folder during the build:
import (
"github.com/janmarkuslanger/ssgo/taskutil"
)
copyStatic := taskutil.NewCopyTask("static", "", nil)
To copy into a subfolder like public/assets/
:
copyStatic := taskutil.NewCopyTask("static", "assets", nil)
You can also inject a custom PathResolver
(mainly for testing):
mockResolver := myMockResolver{}
copyStatic := taskutil.NewCopyTask("static", "", mockResolver)
builder := builder.Builder{
OutputDir: "public",
Writer: &writer.FileWriter{},
Pages: []page.Generator{...},
BeforeTasks: []task.Task{
copyStatic,
},
}
For example, static/logo.png
will be copied to public/logo.png
(or to public/assets/logo.png
if OutputSubDir
is set to assets
).
func NewCopyTask(sourceDir string, outputSubDir string, pathResolver PathResolver) CopyTask
sourceDir
: Path to the source folderoutputSubDir
: Optional subfolder insideOutputDir
(use""
to copy directly intoOutputDir
)pathResolver
: Optional path resolver (ifnil
, a default implementation will be used)
MIT Β© Jan Markus Langer