{"id":28829648,"url":"https://github.com/client9/ssg","last_synced_at":"2026-06-29T21:02:16.311Z","repository":{"id":261484554,"uuid":"884448892","full_name":"client9/ssg","owner":"client9","description":"A toolkit for Static Site Generators","archived":false,"fork":false,"pushed_at":"2026-06-28T01:46:19.000Z","size":231,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-28T03:13:40.200Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/client9.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-06T19:14:42.000Z","updated_at":"2026-06-28T01:45:46.000Z","dependencies_parsed_at":"2025-03-27T01:27:21.483Z","dependency_job_id":"4b0fe27b-b2f4-4141-9719-f39a70d5bf34","html_url":"https://github.com/client9/ssg","commit_stats":null,"previous_names":["npg70/ssg","client9/ssg"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/client9/ssg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/client9%2Fssg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/client9%2Fssg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/client9%2Fssg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/client9%2Fssg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/client9","download_url":"https://codeload.github.com/client9/ssg/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/client9%2Fssg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34942665,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-19T05:14:24.267Z","updated_at":"2026-06-29T21:02:16.303Z","avatar_url":"https://github.com/client9.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ssg\n\nA document transformation pipeline and composable static site generator toolkit for Go.\n\n`ssg` provides the building blocks for a content pipeline in three phases:\n\n```\nload inputs into memory\n  ↓\nenrich / expand / contract\n  add or remove artifacts, derive metadata\n  ↓\nmaterialize\n  each artifact runs its own pipeline → emit outputs\n```\n\n## Why? And Alternatives\n\nThere are many static site generators!\n\nMost focus on a \"no programming required\" model which limits extensibility.\n\nThe closest model is [metalsmith](https://metalsmith.io).\n\n## Core concepts\n\n**`Context`** — site-wide state available to every Plugin and pipeline stage:\n\n```go\ntype Context struct {\n    Globals   map[string]any\n    OutputDir string\n    Logger    *log.Logger\n}\n```\n\n**`Plugin`** — the single interface for all pipeline phases:\n\n```go\ntype Plugin func(ctx *Context, artifacts *[]Artifact) error\n```\n\nLoad, filter, expand, and materialize are all Plugins operating on the same\nartifact set. No structural boundary between phases.\n\n**`Artifact`** — one unit of work: metadata plus the pipeline that produces it:\n\n```go\ntype Artifact struct {\n    Meta     ContentSourceConfig // map[string]any with typed accessors\n    Pipeline Pipeline\n}\n```\n\n**`Pipeline`** — a named sequence of stages. Construct with `NewPipeline`:\n\n```go\nfunc NewPipeline(name string, stages ...Stage) Pipeline\n```\n\n**`Stage`** — a single named pipeline step. Each step receives both the current\ncontent value and the page metadata, and can transform either or both:\n\n```go\ntype Stage interface {\n    Name() string\n    Run(ctx *Context, cfg ContentSourceConfig, in any) (any, error)\n}\n```\n\nUse `Step[I, O]` to create a `Stage` from a typed function:\n\n```go\nfunc Step[I, O any](name string, fn func(*Context, ContentSourceConfig, I) (O, error)) Stage\n```\n\nThe pipeline carries **content and metadata together**. A step can:\n- Transform content only — `[]byte → []byte`, ignore `cfg`\n- Mutate metadata only — `any → any` pass-through, write to `cfg`\n- Read metadata to transform content — e.g. pick a MIME type from `cfg.OutputFile()`\n- Read and write metadata while transforming content — e.g. wrap rendered HTML in a layout template\n\n**`MetaLoader`** — parses raw file bytes into frontmatter metadata and body:\n\n```go\ntype MetaLoader func(raw []byte) (map[string]any, []byte, error)\n```\n\nReturning a nil map signals skip. The return type is `map[string]any` so loader\nimplementations have no dependency on this module.\n\n**`Rule`** — pairs a [doublestar](https://github.com/bmatcuk/doublestar) glob pattern\nwith a loader and a pipeline:\n\n```go\ntype Rule struct {\n    Pattern  string\n    Loader   MetaLoader // nil or ssg.Skip = skip without reading\n    Pipeline Pipeline\n}\n```\n\n## Usage\n\n```go\nctx := \u0026ssg.Context{\n    Globals:   map[string]any{\"Site\": siteConfig},\n    OutputDir: \"public\",\n    Logger:    log.Default(),\n}\n\nrules := []ssg.Rule{\n    {\n        Pattern: \"**/*.md\",\n        Loader:  metayaml.Loader,\n        Pipeline: ssg.NewPipeline(\"post\",\n            ssg.SetOutputFile(ssg.CleanURLs(\".md\", \".html\")), // metadata only\n            ssg.SetTemplateName(\"post.html\"),                  // metadata only\n            markdown.New(),                                    // []byte → []byte\n            ssg.Must(ssg.NewPageRender(\"layout\", fns)),        // reads+writes cfg, []byte → []byte\n            ssg.WriteOutput,                                   // reads cfg, terminal sink\n        ),\n    },\n    {Pattern: \"**/_*\"}, // nil Loader: skip draft files\n}\n\nvar artifacts []ssg.Artifact\nfor _, p := range []ssg.Plugin{\n    ssg.FileWalker(\"content\", rules), // Phase 1: load\n    removeDrafts,                     // Phase 2: contract\n    addTaxonomy,                      // Phase 2: expand\n    ssg.Render,                       // Phase 3: materialize\n} {\n    if err := p(ctx, \u0026artifacts); err != nil {\n        log.Fatal(err)\n    }\n}\n```\n\n### One-to-many outputs\n\nUse `FanOut` inside a pipeline to produce multiple output files from one source.\nEach branch is a full Pipeline; all branches receive the same input:\n\n```go\nPipeline: ssg.NewPipeline(\"post\",\n    ssg.FanOut(\"outputs\",\n        ssg.NewPipeline(\"html\", ssg.SetOutputFile(ssg.CleanURLs(\".md\", \".html\")), markdown.New(), ssg.WriteOutput),\n        ssg.NewPipeline(\"txt\",  ssg.SetOutputFile(ssg.UglyURLs(\".md\", \".txt\")),  plaintext.New(), ssg.WriteOutput),\n    ),\n),\n```\n\n### Writing a pipeline step\n\nImplement a typed function and wrap it with `Step`. The function receives both the\ncurrent content value and the mutable metadata map:\n\n```go\n// Content-transforming step (metadata ignored):\nvar UpperCase = ssg.Step(\"uppercase\", func(_ *ssg.Context, _ ssg.ContentSourceConfig, in []byte) ([]byte, error) {\n    return bytes.ToUpper(in), nil\n})\n\n// Metadata-only step (content passed through unchanged):\nfunc SetCanonical(base string) ssg.Stage {\n    return ssg.Step(\"set-canonical\", func(_ *ssg.Context, cfg ssg.ContentSourceConfig, in any) (any, error) {\n        cfg[\"Canonical\"] = base + cfg.OutputFile()\n        return in, nil\n    })\n}\n\n// Step that reads metadata to transform content:\nvar AddTitle = ssg.Step(\"add-title\", func(_ *ssg.Context, cfg ssg.ContentSourceConfig, in []byte) ([]byte, error) {\n    title := cfg.Get(\"Title\")\n    return append([]byte(\"\u003ch1\u003e\"+title+\"\u003c/h1\u003e\\n\"), in...), nil\n})\n```\n\nUse `ssg.Must(ssg.NewPageRender(\"layout\", fns))` to inline constructors that return\n`(Stage, error)`.\n\n### Filtering\n\n```go\nssg.FilterArtifacts(func(meta ssg.ContentSourceConfig) bool {\n    draft, _ := meta[\"draft\"].(bool)\n    return !draft\n})\n```\n\n### Taxonomy pages\n\n```go\nbyTag := ssg.GroupByStrings(artifacts, \"Tags\")\nfor tag, tagArtifacts := range byTag {\n    artifacts = append(artifacts, ssg.NewPage(\n        \"tags/\"+slug(tag)+\"/index.html\", \"tag-list/index.html\",\n        map[string]any{\"Tag\": tag, \"Pages\": metaSlice(tagArtifacts)},\n        tagPipeline,\n    ))\n}\n```\n\n### Built-in metadata steps\n\n| Step | What it does |\n|---|---|\n| `SetOutputFile(transform)` | Applies a `PathTransformer` to `SourcePath`, writes `OutputFile` to cfg |\n| `SetTemplateName(name)` | Writes `TemplateName` to cfg if not already set by frontmatter |\n\nBoth pass content through unchanged (`any → any`).\n\n### Path transformers\n\n| Function | Example |\n|---|---|\n| `CleanURLs(\".md\", \".html\")` | `posts/foo.md` → `posts/foo/index.html` |\n| `UglyURLs(\".md\", \".html\")` | `posts/foo.md` → `posts/foo.html` |\n| `SlugNormalize(next)` | lowercases and hyphenates before applying next |\n\n## Sub-modules\n\nEach sub-module is a separate Go module and can be imported independently.\nMeta sub-modules have no dependency on `github.com/client9/ssg`.\n\n### Pipeline stages (`render/`)\n\nEach package returns a `ssg.Stage` (or a constructor for one).\n\n| Module | Import path | Description |\n|---|---|---|\n| **htmlclean** | `github.com/client9/ssg/render/htmlclean` | Normalizes HTML fragments via `golang.org/x/net/html` |\n| **markdown** | `github.com/client9/ssg/render/markdown` | Markdown → HTML via Goldmark with GFM and auto heading IDs |\n| **minify** | `github.com/client9/ssg/render/minify` | Minifies HTML/CSS/JS/SVG; MIME type from `cfg.OutputFile()` |\n| **shortcode** | `github.com/client9/ssg/render/shortcode` | Embedded `$cmd[args]{body}` macro engine |\n\nThe shortcode syntax:\n\n```\n$cmd\n$cmd[arg1 arg2]\n$cmd[name=value key=\"val\"]\n$cmd{body}\n$cmd[args]{body}\n$$   →  literal $\n```\n\n### Metadata loaders (`meta/`)\n\nEach package exports a single `var Loader` of type `func([]byte) (map[string]any, []byte, error)`.\n\n| Module | Import path | Description |\n|---|---|---|\n| **json** | `github.com/client9/ssg/meta/json` | JSON object frontmatter (`{\\n...\\n}\\n`) |\n| **yaml** | `github.com/client9/ssg/meta/yaml` | YAML frontmatter (`---\\n...\\n---\\n`) via `go.yaml.in/yaml/v4` |\n| **toml** | `github.com/client9/ssg/meta/toml` | TOML frontmatter (`+++\\n...\\n+++\\n`) via `github.com/BurntSushi/toml` |\n| **email** | `github.com/client9/ssg/meta/email` | Email-style `Key: Value` headers; `email.NewLoader(transformers...)` for type coercion |\n\nThe root module also provides two built-in loaders:\n- `ssg.Passthrough` — returns raw bytes as body with empty metadata; use for assets\n- `ssg.Skip` — unconditionally skips the file; explicit alternative to a nil `Rule.Loader`\n\n### Template functions (`tmpl/`)\n\n| Module | Import path | Description |\n|---|---|---|\n| **stdfuncs** | `github.com/client9/ssg/tmpl/stdfuncs` | Stdlib-only `template.FuncMap`; covers strings, math, collections, path, time, encoding, and URL helpers |\n\n```go\nt := template.New(\"page\").Funcs(stdfuncs.FuncMap())\n\n// Combine with your own:\nfns := stdfuncs.Merge(stdfuncs.FuncMap(), template.FuncMap{\"myFunc\": myFunc})\n```\n\n## Sample\n\n`sample/` is a complete working site: JSON frontmatter, HTML content with\n`text/template` macros, page templates, tag taxonomy, and HTML pretty-printing.\n\n```bash\ncd sample \u0026\u0026 make run   # renders to sample/public/\n```\n\n## Development\n\n```bash\nmake test    # go test ./...\nmake lint    # go mod tidy, gofmt, golangci-lint\nmake env     # install golangci-lint, goimports\n```\n\nSub-modules each have their own `go.mod`. Run `go test ./...` from their directory,\nor use the workspace: `go work sync` at the repo root.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclient9%2Fssg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclient9%2Fssg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclient9%2Fssg/lists"}