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
- 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, "
#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
const RegionTag = "selfdoc-region"RegionTag is the element one region is wrapped in.
#SiteDirectives
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
type Error structError 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
type SiteContext structSiteContext is everything a site-level directive reads.
#Region
type Region structRegion 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
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
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
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
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
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
func Directives(context *SiteContext) map[string]resolver.BuiltinDirectiveDirectives 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
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
func HomeListingPath(dirPath string, cfg map[string]any) stringHomeListingPath returns where the home project declares its curated listing.
#BuildHomeProject
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
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
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
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
func RenderRegion(name string, attrs map[string]string, context SiteContext) (string, error)RenderRegion returns a resolved region: the body between its two tags.
#FindRegions
func FindRegions(pageHTML string) []RegionFindRegions returns every region the page carries, in document order.
#RegionNames
func RegionNames(pageHTML string) []stringRegionNames returns every site-level directive region the page carries.
#FindUnclosedRegions
func FindUnclosedRegions(pageHTML string) []stringFindUnclosedRegions returns the directives whose region opens and never closes.
#RefreshRegions
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
func PageContext(context SiteContext, pageRel string) SiteContextPageContext 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
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
func (e *Error) Error() string { return e.Message }Error returns the diagnostic.