Skip to content

janmarkuslanger/ssgo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

54 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Logo

SSGO

Simple, fast and extendable static site generator.

Code coverage Latest Release Build Status Download ZIP


✨ What is SSGO?

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

πŸ“¦ Installation

Install via:

go get github.com/janmarkuslanger/ssgo

Or download directly:
⬇ Download latest ZIP


πŸš€ Minimal Example

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)

main.go

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)
	}
}

templates/layout.html

{{ define "root" }}
<!DOCTYPE html>
<html>
  <head><title>{{ .Title }}</title></head>
  <body>
  	<h1>{{ upper .Title }}</h1>
    {{ template "content" . }}
  </body>
</html>
{{ end }}

templates/blog.html

{{ define "content" }}
<h1>{{ .Title }}</h1>
<p>{{ .Content }}</p>
{{ end }}

Run it

go run main.go

β†’ Two files will be generated:

public/
└── blog/
    β”œβ”€β”€ hello-world
    └── second-post

🧱 Concepts

πŸ”¨ Builder

Orchestrates the generation of pages and writes them to disk:

type Builder struct {
	OutputDir  string
	Writer     Writer
	Generators []page.Generator
}

πŸ“„ Generator / Config

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.

πŸ“¦ PagePayload

Passed to GetData so you can access dynamic URL parameters:

type PagePayload struct {
	Path   string
	Params map[string]string
}

πŸ–‹ Writer

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

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

βš™οΈ Tasks

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.

πŸ§ͺ Example

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 (built-in)

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 {}.

βœ… Example: Copy static assets

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)

πŸ”Œ Registering as a BeforeTask

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).

πŸ“¦ Constructor

func NewCopyTask(sourceDir string, outputSubDir string, pathResolver PathResolver) CopyTask
  • sourceDir: Path to the source folder
  • outputSubDir: Optional subfolder inside OutputDir (use "" to copy directly into OutputDir)
  • pathResolver: Optional path resolver (if nil, a default implementation will be used)

πŸ“– License

MIT Β© Jan Markus Langer

About

Simple, fast and extendable static site generator

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages