garden

package module
v0.0.0-...-f407762 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 32 Imported by: 0

README

garden

Go Reference CI

Turn a folder of markdown notes into a knowledge garden: a static site where links open notes in stacked panes, side by side, in the style of Andy Matuschak's working notes. Every note lists its backlinks, and hovering a link shows a preview of the linked note, scrolled to the linked section.

  • A single Go binary, also usable as a library.
  • Reads Obsidian vaults ([[wikilinks]]) and MkDocs sites ([text](note.md), admonitions, mkdocs.yml nav).
  • Renders LaTeX math with a bundled copy of KaTeX, so it works offline.
  • Parses and renders notes in parallel: 636 notes in about 0.3 s.
  • Outputs plain static files that you can host on GitHub Pages or anywhere else.
  • Only rewrites output files that changed.

Install

go install github.com/zoravur/garden/cmd/garden@latest

Requires Go 1.22 or newer.

Use

garden init my-garden      # garden.yaml and a starter note
cd my-garden
garden serve               # http://localhost:8080, rebuilds when a note changes
garden build               # writes the site to dist/

To publish on GitHub Pages:

  1. Copy examples/github-pages/publish.yml into your notes repo as .github/workflows/publish.yml.
  2. Turn on Pages under Settings → Pages → Source: GitHub Actions.

Every push to main then rebuilds and publishes the site.

Configuration

garden.yaml sits next to your notes. Relative paths resolve from the file's folder.

title: My Garden
description: Working notes, linked together.
vault: vault                  # folder of .md files
out: dist
home: ""                      # note opened first (path without .md); blank = index.md, or a generated index
nav: folders                  # contents tree from folders, or mkdocs:path/to/mkdocs.yml
exclude: [templates/**, "*.excalidraw.md"]
public_only: false            # true: publish only notes with `public: true`
heading_shift: 0              # 1 turns "# X" into <h2> (MkDocs toc baselevel: 2)
site_prefix: /my-site/        # strip this absolute prefix from links in raw HTML
source_url: https://github.com/me/notes/blob/main/   # adds a "Source" link to each note
static_pages: true            # also write notes/<id>.html for search engines and no-JS readers
max_asset_mb: 0               # skip larger files (0 = no limit)...
asset_fallback_url: ""        # ...and link them from here instead
style: wiki                   # built-in look: "" (default) or wiki
css: [overrides.css]          # your stylesheets, loaded last
theme: theme                  # folder of whole-file replacements (garden.css, garden.js, index.html, note.html)

If home is blank and the vault has no index.md at its root, which is common in Obsidian vaults, garden generates an index page. It lists the ten most-linked notes with their opening lines, followed by the whole contents tree. The generated page doesn't appear in any note's backlinks. If home names a note that doesn't exist, the build stops with an error.

With public_only: true, private notes never reach the output. A link to a private note becomes plain text, so neither the note's name nor its path appears anywhere on the site.

What it understands

Markdown Notes
[label](other-note.md#heading) Resolved relative to the note, then from the vault root, then by file name anywhere in the vault. Notes can move between folders without breaking links.
[[Note]], [[Note|label]], [[Note#Heading]], ![[image.png]] Obsidian wikilinks. Links inside code are left alone.
!!! note "Title", ??? warning, ???+ tip MkDocs Material admonitions. The ??? forms are collapsible.
Frontmatter title, date, public Without a title, the leading # heading is used, then the file name (underscores become spaces; hyphens are kept, so 2026-10-22 stays a date).
GFM tables, task lists, footnotes, definition lists Rendered by goldmark.
Fenced code with a language Highlighted in the browser.
$x$, $$x$$, \(x\), \[x\], \begin{align}…\end{align} Math, rendered with KaTeX. KaTeX ships inside garden and is added to the site only if a note uses math. Prices like $5 and $10, shell variables, and anything in code stay plain text.

Heading IDs follow MkDocs' slug rules, so existing #section links keep working. garden build -report broken.txt lists links that point nowhere.

As a library

The garden package does everything the command does, and exposes the link graph for use in larger systems: search indexes, crawlers, a graph database, or a server that renders notes on demand.

import "github.com/zoravur/garden"

// One call, like `garden build`:
cfg, _ := garden.LoadConfig("garden.yaml")
g, stats, err := garden.Build(ctx, cfg)

// Or load into memory and work with the graph:
g, err := garden.Load(ctx, &garden.Config{FS: os.DirFS("notes")})
for _, n := range g.Notes {
	fmt.Println(n.ID, n.Title, len(n.Links), len(g.Backlinks(n.ID)))
}
id, anchor, ok := g.Resolve("index", "backlinks.md#why")  // same rules as rendering
stats, err := g.Write(ctx, "dist")                          // write the site later, or never
  • Notes come from any fs.FS: a directory, an embed.FS, a zip archive, or an in-memory fstest.MapFS.
  • Load and Write are separate. You can index notes or validate links without producing a site, and loading honours context cancellation.
  • What each note carries: a Note has its rendered HTML, frontmatter, excerpt and outgoing Links. Each link records the heading it sits under and the sentence around it.
  • Backlinks: Garden.Backlinks returns the inverted edges, sorted.

Full API docs are at pkg.go.dev.

Styling

There are three layers, applied in order, and each can override the ones before it.

  1. style: picks a built-in look. style: wiki gives an encyclopedia style: light only, sans-serif text under ruled serif headings, blue links, and red links for notes that are missing or unpublished.

  2. css: adds your own stylesheets. They load last, so a few lines of CSS are enough to change anything:

    style: wiki
    css: [garden-overrides.css]
    
    /* garden-overrides.css */
    :root { --accent: #0b6e4f; --pane-w: 680px; }
    .pane-title { font-size: 34px; }
    

    Colours, fonts and sizes are CSS variables (--accent, --ink, --paper, --sheet, --f-body, --pane-w, ...), listed at the top of web/garden.css. The wiki style adds --redlink.

  3. theme: replaces whole files. Put garden.css, garden.js, index.html or note.html in a folder to use it instead of the built-in version. garden.DefaultTheme() returns the built-in files to start from.

Links to notes that don't exist get the class missing. Links to unpublished notes (with public_only) are reduced to <span class="private">, which keeps the text but has no target, so a stylesheet can colour both without the site revealing anything.

Output

dist/
  index.html, garden.css, garden.js   the app shell
  data/index.json                     titles, excerpts, contents tree
  data/shard-NNN.json                 note bodies and backlinks, fetched on demand
  notes/<id>.html                     one plain page per note
  <assets>                            images and files the notes use

The URL fragment stores the open panes, separated by ~: index.html#ideas.2Fbacklinks~index.

Examples

  • examples/starter: a four-note Obsidian-style vault.
  • examples/blue-book: config for Lyz's Blue Book, a 636-note MkDocs garden. It reads the MkDocs navigation and leaves out the auto-generated newsletter, which links to nearly every note and would bury the real backlinks.

License

MIT

Documentation

Overview

Package garden turns a folder of markdown notes into a static knowledge garden: a site where links open notes in stacked panes, side by side, and every note lists the notes that link to it.

The package can be used in two ways. Build does everything in one call, the same as the garden command:

cfg, err := garden.LoadConfig("garden.yaml")
if err != nil { ... }
_, stats, err := garden.Build(ctx, cfg)

Or the steps can be separated, so a larger system can work with the link graph before (or instead of) writing a site. Load reads and renders every note in parallel and computes backlinks, all in memory:

g, err := garden.Load(ctx, cfg)
for _, n := range g.Notes {
	fmt.Println(n.ID, len(n.Links), len(g.Backlinks(n.ID)))
}
stats, err := g.Write(ctx, "dist")

Notes are read from Config.FS when it is set (an embed.FS, an fstest.MapFS, a zip archive), otherwise from the Config.Vault directory.

Math

$inline$, $$display$$, \(inline\), \[display\] and \begin{env} blocks are set aside before markdown parsing, so the parser can't alter them, and rendered in the browser with a bundled copy of KaTeX. A dollar sign is only treated as math when it pairs up the way Pandoc requires, so prices, shell variables and code are left alone.

Output

Garden.Write produces plain static files: an app shell (index.html, garden.css, garden.js), the note data split into shards that the browser loads on demand, optional per-note HTML pages, and the assets notes reference. Files whose content hasn't changed are not rewritten.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Build

func Build(ctx context.Context, cfg *Config) (*Garden, *Stats, error)

Build loads the vault and writes the site to cfg.Out.

Example

Build a site from a garden.yaml, as the garden command does.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/zoravur/garden"
)

func main() {
	cfg, err := garden.LoadConfig("garden.yaml")
	if err != nil {
		log.Fatal(err)
	}
	_, stats, err := garden.Build(context.Background(), cfg)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d notes written to %s\n", stats.Notes, cfg.Out)
}

func DefaultTheme

func DefaultTheme() fs.FS

DefaultTheme returns the built-in theme files: index.html (the app shell), note.html (static note pages), garden.css and garden.js.

func Styles

func Styles() []string

Styles lists the built-in styles that Config.Style accepts.

Types

type Backlink struct {
	Source string // ID of the linking note
	Refs   []Link // the links, with Target set to the linked note
}

Backlink is a note that links to another, with each place it does so.

type Config

type Config struct {
	// Title and Description are used for the site's <title> and metadata.
	Title       string `yaml:"title"`
	Description string `yaml:"description"`

	// Vault is the folder of markdown notes. Ignored when FS is set.
	Vault string `yaml:"vault"`

	// FS, when set, is read instead of the Vault directory. Paths in it are
	// vault-relative, e.g. "ideas/backlinks.md".
	FS fs.FS `yaml:"-"`

	// Out is the folder Write and Build write the site to. Default "dist".
	Out string `yaml:"out"`

	// Home is the ID of the note opened first (its path without ".md").
	// When blank, a note at the vault root named index.md is used if there
	// is one; otherwise an index page is generated: the most-linked notes
	// and a contents tree. When set, the note must exist.
	Home string `yaml:"home"`

	// Exclude lists vault-relative patterns to skip. "dir/**" skips a whole
	// folder; other patterns use path.Match against the full path and the
	// file name.
	Exclude []string `yaml:"exclude"`

	// Nav selects the contents tree: "folders" (default) mirrors the vault's
	// folders; "mkdocs:path/to/mkdocs.yml" reads an MkDocs nav.
	Nav string `yaml:"nav"`

	// PublicOnly publishes only notes whose frontmatter has `public: true`.
	// Links to other notes render as plain text, without their names.
	PublicOnly bool `yaml:"public_only"`

	// SitePrefix is an absolute URL prefix (e.g. "/blue-book/") to strip
	// from links and raw HTML, for vaults written for a site in a subpath.
	SitePrefix string `yaml:"site_prefix"`

	// HeadingShift is added to every heading level. 1 turns "# X" into <h2>,
	// matching MkDocs' toc baselevel: 2.
	HeadingShift int `yaml:"heading_shift"`

	// SourceURL, if set, adds a "Source" link to each note: SourceURL + the
	// note's vault path.
	SourceURL string `yaml:"source_url"`

	// StaticPages also writes notes/<id>.html, a plain page per note for
	// search engines and readers without JavaScript. Default true.
	StaticPages *bool `yaml:"static_pages"`

	// ShardKB is the target size of each note data file. Default 512.
	ShardKB int `yaml:"shard_kb"`

	// MaxAssetMB skips copying assets larger than this (0 means no limit).
	// References to skipped assets point at AssetFallbackURL instead.
	MaxAssetMB       float64 `yaml:"max_asset_mb"`
	AssetFallbackURL string  `yaml:"asset_fallback_url"`

	// Style picks a built-in look layered over the default theme: "" (the
	// default) or "wiki" (light, blue links, red links for missing or
	// unpublished notes). See Styles.
	Style string `yaml:"style"`

	// CSS lists extra stylesheets, applied after the theme and Style, so
	// they can override any rule or CSS variable.
	CSS []string `yaml:"css"`

	// Theme is a folder whose garden.css, garden.js, index.html or note.html
	// replace the built-in ones. Default "theme" next to the config file,
	// used only if it exists.
	Theme string `yaml:"theme"`

	// Workers is how many notes are processed at once. Default: CPU count.
	Workers int `yaml:"workers"`
	// contains filtered or unexported fields
}

Config describes a garden: where its notes live and how to publish them. It is usually read from a garden.yaml file with LoadConfig, but can be built in code. Relative paths in a config file resolve against the file's directory; relative paths set in code resolve against the working directory.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads a garden.yaml file.

type Garden

type Garden struct {
	Config *Config    // normalized copy of the config it was loaded with
	Notes  []*Note    // published notes, sorted by ID
	Nav    []*NavNode // contents tree

	// Broken maps link targets that match nothing to the IDs of notes that
	// link to them.
	Broken map[string][]string
	// contains filtered or unexported fields
}

Garden is a vault loaded into memory: every published note rendered, plus the link graph. It is safe for concurrent reads.

func Load

func Load(ctx context.Context, config *Config) (*Garden, error)

Load scans the vault, renders every published note in parallel and computes backlinks. Nothing is written to disk.

Example

Load a vault into memory and walk its link graph without writing a site.

package main

import (
	"context"
	"fmt"
	"log"
	"testing/fstest"

	"github.com/zoravur/garden"
)

func main() {
	vault := fstest.MapFS{
		"index.md":           {Data: []byte("Start with [[Evergreen notes]].")},
		"Evergreen notes.md": {Data: []byte("They grow through [[ideas/Backlinks|backlinks]].")},
		"ideas/Backlinks.md": {Data: []byte("A link seen from the other end. Back to [[index]].")},
	}
	g, err := garden.Load(context.Background(), &garden.Config{FS: vault})
	if err != nil {
		log.Fatal(err)
	}
	for _, n := range g.Notes {
		fmt.Printf("%-16s links out: %d, linked from: %d\n", n.Title, len(n.Links), len(g.Backlinks(n.ID)))
	}
}
Output:
Evergreen notes  links out: 1, linked from: 1
Backlinks        links out: 1, linked from: 1
Index            links out: 1, linked from: 1

func (*Garden) Assets

func (g *Garden) Assets() []string

Assets returns the vault paths of every file a published note uses, sorted.

func (g *Garden) Backlinks(id string) []Backlink

Backlinks returns the notes that link to id, sorted by title.

func (*Garden) Note

func (g *Garden) Note(id string) (*Note, bool)

Note returns the published note with the given ID.

func (*Garden) Report

func (g *Garden) Report() string

Report lists broken links in a plain-text form suitable for a log or file.

func (*Garden) Resolve

func (g *Garden) Resolve(from, dest string) (id, anchor string, ok bool)

Resolve reports which note a link destination written in note `from` points at, using the same rules as rendering. ok is false if it doesn't point at a published note.

func (*Garden) Write

func (g *Garden) Write(ctx context.Context, out string) (*Stats, error)

Write writes the site to out (cfg.Out if out is empty). Files whose content is unchanged are not rewritten, so repeated writes are cheap.

type Link struct {
	Target  string // ID of the linked note
	Anchor  string // heading ID within the target, if any
	Label   string // the link's text
	Section string // heading in the source note the link sits under
	Context string // the sentence or list item around the link, trimmed
}

Link is an edge from one note to another, with the text around it.

type NavNode struct {
	Title    string     `json:"t"`
	ID       string     `json:"id,omitempty"`
	Children []*NavNode `json:"c,omitempty"`
}

NavNode is an entry in the contents tree. A section can also be a note (its index page), so ID and Children can both be set.

type Note

type Note struct {
	ID          string         // vault path without ".md", e.g. "ideas/backlinks"
	Path        string         // vault path, e.g. "ideas/backlinks.md"
	Title       string         // frontmatter title, else the leading # heading, else the file name
	Date        string         // frontmatter date, as written
	Frontmatter map[string]any // parsed YAML frontmatter; nil if there is none
	HTML        string         // rendered body; links are relative to the site root
	Excerpt     string         // plain text of the first paragraph, trimmed
	Links       []Link         // outgoing links to other published notes, in document order
	Assets      []string       // vault paths of images and files the note uses
	Broken      []string       // link targets that match no note or file
	HasCode     bool           // contains a code block
	HasMath     bool           // contains math, rendered in the browser with KaTeX
	Generated   bool           // made by garden (the index page), not read from the vault
}

Note is one rendered note and the links found in it.

type Phase

type Phase struct {
	Name string
	Took time.Duration
}

Phase is how long one step of a load or write took.

type Stats

type Stats struct {
	Notes         int     // published notes
	Links         int     // note-to-note links
	Broken        int     // links that match nothing
	Written       int     // output files created or changed
	Unchanged     int     // output files left as they were
	AssetsCopied  int     // assets in the output
	AssetsSkipped int     // assets over MaxAssetMB, served from AssetFallbackURL
	Phases        []Phase // time per step, load included
	Took          time.Duration
}

Stats summarizes a build.

Directories

Path Synopsis
cmd
garden command
Command garden builds a stacked-pane knowledge garden from a folder of markdown notes.
Command garden builds a stacked-pane knowledge garden from a folder of markdown notes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL