Skip to content
internal/blog/sitedirectives
On this page

The site-level directives that keep the home project's authored front page current: the curated project cards with live versions, and the recent posts.

#internal/blog/sitedirectives

#internal/blog/sitedirectives

Package sitedirectives carries the site-level directives: the generated parts of the home project's authored pages.

The front page is authored -- prose, structure and design belong to whoever writes it -- but two of its parts are mechanical: the curated project cards with each project's live version, and the recent posts. Those arrive through directives so the authored page can never go stale.

#Where resolution happens

Two moments, and both are the same code:

- Build time, when the home project is built for the assembly. The build needs the assembly's manifests to know any project's live version, so it is run as "selfdoc build --target home --site-manifests

", which is [BuildHomeProject]. Without that context the command refuses to build at all, naming what is missing. A plain build of the home project refuses too: the catalog has no "projects-cards", so it stops at an unknown directive. Neither path ever emits an empty region.

- Assembly time, on every deploy, inside the shared-file generator. Each resolved region is left in the emitted HTML inside a wrapper element, so the assembly can re-render it from the manifests it holds without going back to the source. This is what keeps the front page's version badges current when another project deploys: the home project's own build may be months old, the region is not.

Both moments render a region's links against the rendering page's hop back to the site root, never against the site's base URL. A region on the front page and the same region on a page one level down need different hrefs, and an absolute one would need neither -- it would work on the deployed host and walk a reader off any preview or mirror. Directive resolution never learns which page it is writing into, so the build resolves regions first and then re-addresses them per page; [PageContext] is the one place that turns a page path into that hop.

#The region wrapper

The region wrapper is the whole mechanism. It is a custom element, "", and not an HTML comment: the build minifies its output and strips every comment, so a comment-delimited region would survive the markdown conversion and then vanish on the way to disk. A custom element survives both, carries its directive's attributes as data attributes so a re-render uses the same ones, and is nothing a browser has to be told about. A region that opens and never closes is a hard error, never a half-rendered page.

#How a build reaches this package

The Python shipped one shim script per directive and handed them to the build under the "directives" config key, with the live context passed through the same config document. One binary needs neither: both directives are compiled in, and [Directives] hands the build a [resolver.BuiltinDirective] per name, each bound to the [SiteContext] the command assembled. The context reaches the resolver inside those functions rather than as a value in the config map, which stays a document.

#RegionTag

Go go
const RegionTag = "selfdoc-region"

RegionTag is the element one region is wrapped in.

#SiteDirectives

Go go
var SiteDirectives = []string{"projects-cards", "blog-highlights"}

SiteDirectives is the directives this package resolves, in one place so the CLI, the build and the verifier all name the same set.

#Error

Go go
type Error struct

Error is the failure every operation here reports: a directive that cannot be rendered, and a page whose region opens and never closes.

It is the Go counterpart of the RuntimeError the Python surface raised, and the one error type a caller needs to recognize with errors.As to render a refusal distinctly from an unexpected internal failure.

#SiteContext

Go go
type SiteContext struct

SiteContext is everything a site-level directive reads.

#Region

Go go
type Region struct

Region is one region a page carries, as the page states it.

It replaces the raw regular-expression match the Python surface handed its callers: the verifier reads Name to say which directive a page lost and Body to notice that it holds nothing, and the refresh pass reads Attrs to re-render with the attributes the directive was written with.

#RenderBlogHighlights

Go go
func RenderBlogHighlights(manifests []map[string]any, siteHop string, limit int) (string, error)

RenderBlogHighlights returns the limit most recent posts across every project.

siteHop is the rendering page's hop back to the site root; every post link is written against it rather than against the site's base URL, so the highlights lead into the tree the reader is on.

#ResolveProjectsCards

Go go
func ResolveProjectsCards(attrs map[string]string, context SiteContext) (string, error)

ResolveProjectsCards returns the curated project cards.

The directive takes no attributes at all: what the listing holds is declared in the home project's docs/projects.toml, so a marker that states anything is stating it in the wrong place. A context with no listing is refused too, naming that file -- the alternative is a front page publishing a listing nobody curated.

#ResolveBlogHighlights

Go go
func ResolveBlogHighlights(attrs map[string]string, context SiteContext) (string, error)

ResolveBlogHighlights returns the newest posts across every project.

limit is required and has no default: how many recent posts the front page shows is an editorial decision, and a number invented here would be one nobody made.

#RenderDirectiveBody

Go go
func RenderDirectiveBody(name string, attrs map[string]string, context SiteContext) (string, error)

RenderDirectiveBody returns the HTML a site-level directive's region holds.

It reports an error for an unknown name, a missing required attribute, or a context that cannot answer the directive (no curated listing).

#ResolveForBuild

Go go
func ResolveForBuild(name string, attrs map[string]string, context *SiteContext) (string, error)

ResolveForBuild resolves a site-level directive during a build of the home project.

This is what [Directives] binds, and the refusal below is what a registration with no context renders: the manifests are what a version badge and a post highlight are read from, and no project's own repository holds them, so a build that never received them cannot resolve the directive and says which command supplies them.

#Directives

Go go
func Directives(context *SiteContext) map[string]resolver.BuiltinDirective

Directives returns the resolver registration for the site-level directives, each bound to context.

A build reaches this package through the returned functions and through nothing else: the context is captured here rather than passed through the config document, which stays a document. A nil context registers the names with the refusal [ResolveForBuild] renders, which is what a caller that has no assembly data gets instead of a silently empty region.

#LazyDirectives

Go go
func LazyDirectives(

LazyDirectives returns the site-level directive registration bound to a context resolved on first use.

A caller that would have to reach the network to answer these directives registers through here rather than through [Directives]: the read happens when a page actually carries one of the markers, and a project whose pages carry none pays nothing. resolve is called at most once, and its answer -- value or error -- is what every marker on every page of that run sees.

A resolve that fails names the directive it failed to answer, because the reader of the diagnostic is looking at a page that carries a marker, not at a command that fetched something.

#HomeListingPath

Go go
func HomeListingPath(dirPath string, cfg map[string]any) string

HomeListingPath returns where the home project declares its curated listing.

#BuildHomeProject

Go go
func BuildHomeProject(

BuildHomeProject builds the home project with the assembly's data in scope.

This is the only build that can resolve a site-level directive, and the refusals below are why: the manifests are what a version badge and a post highlight are read from, and no project's own repository holds them. A missing context stops the build before a page is written -- there is no rendering of an empty region and no placeholder.

Directive resolution happens per markdown source and never learns which emitted page it is writing into, so the regions come out of the build addressed from the output root. [RefreshOutputRegions] then re-renders each one against its own page's hop -- the same pass the assembly runs on every deploy, run here so the home project's own build output is correct on its own.

The project's selfdoc.json is loaded here rather than taken as an argument, because this entry point's whole input is a directory: the checkout and the assembly's manifests directory. A caller that already holds both the config and the assembly's manifests calls [BuildHome].

#HomeContext

Go go
func HomeContext(

HomeContext is the site context the home project's directives resolve against: the assembly's manifests, and the listing this project curates.

The listing is read from the project's own docs/projects.toml rather than from the copy the assembly keeps, because that file is the authored source and the copy is what a deploy wrote from it. A build and a check of the same working tree therefore answer from the same document.

#BuildHome

Go go
func BuildHome(

BuildHome builds the home project with an already-resolved site context.

This is [BuildHomeProject] without the directory read: a caller that got the assembly's manifests from somewhere other than a checkout -- the Git Data API, say -- has the context already and builds through here.

The site-level directives are registered into the config the build runs on rather than into the document on disk, which stays a document.

#RegisterSiteDirectives

Go go
func RegisterSiteDirectives(

RegisterSiteDirectives returns cfg with registered added to its "directives" mapping, leaving cfg itself alone.

A name the project declares for itself wins: the config document is the project's own declaration, and a registration that overwrote it would silently replace a script the author wrote.

#RenderRegion

Go go
func RenderRegion(name string, attrs map[string]string, context SiteContext) (string, error)

RenderRegion returns a resolved region: the body between its two tags.

#FindRegions

Go go
func FindRegions(pageHTML string) []Region

FindRegions returns every region the page carries, in document order.

#RegionNames

Go go
func RegionNames(pageHTML string) []string

RegionNames returns every site-level directive region the page carries.

#FindUnclosedRegions

Go go
func FindUnclosedRegions(pageHTML string) []string

FindUnclosedRegions returns the directives whose region opens and never closes.

#RefreshRegions

Go go
func RefreshRegions(pageHTML string, context SiteContext, source string) (string, error)

RefreshRegions re-renders every site-level region in pageHTML from context.

Idempotent by construction: the tags stay in the output, so the next deploy finds the same regions and rewrites their bodies again. A page with no region comes back unchanged.

It returns an error naming source when a region opens and never closes, or when a region cannot be re-rendered. An empty source names nothing.

#PageContext

Go go
func PageContext(context SiteContext, pageRel string) SiteContext

PageContext is context addressed from the page at pageRel.

pageRel is the page's path relative to the root it is served from, and the hop back to that root is how many directories deep it sits. Every caller that renders a region into a known page goes through here, so the build-time pass and the deploy-time pass cannot disagree about where a region's links point.

#RefreshOutputRegions

Go go
func RefreshOutputRegions(outputDir string, context SiteContext, h *effects.Handle) ([]string, error)

RefreshOutputRegions re-renders every region in every HTML page under outputDir.

It returns the output-relative paths that changed. The paths are also the addresses: the home project's output root is the site root, both in its own build and after the graft, so a page's depth in this tree is the hop its links need.

#Error.Error

Go go
func (e *Error) Error() string { return e.Message }

Error returns the diagnostic.

Search