On this page
Loading and validating selfdoc.json against one declarative schema table, so every consumer reads a key that is always present with its declared default.
#internal/config
#internal/config
Package config loads and validates a project's selfdoc.json.
The whole schema is one declarative table, [Schema], and one recursive validator walks it. A loaded config is a plain map keyed by every top-level field name in [Schema] -- present unconditionally, carrying the field's default when the document omitted it -- so every consumer reads a key rather than asking whether it exists.
#Value shapes
A raw document is decoded the way Python's json module decodes it, because the validator's type rules were written against those shapes: a JSON object is a map[string]any, an array is a []any, an integer literal is an int64, a number carrying a fraction or an exponent is a float64, a string is a string, true/false is a bool, and null is a nil any. [ValidateConfig] accepts a document assembled in memory under the same rules, and additionally tolerates a plain int where an int64 would appear.
#Nested values are validated in place
The validator rewrites nested lists and objects as it descends -- a transform's result, a coerced float -- and because Go maps and slices are reference types the caller's own document sees those rewrites, exactly as the Python did. A caller that needs its raw document untouched copies it first.
#UnversionedVersion
const UnversionedVersion = "unversioned"UnversionedVersion is what a project declaring "unversioned": true is dispatched and recorded under wherever a version string is required.
The assembly's dispatch payload, its derived membership record and the commit message a deploy writes all carry a version. A project with no public version has nothing to put there, and the empty string is not an answer: the membership record refuses an empty field, because a record that lost its version used to read the same as one that never had one. This literal is the answer -- it is never a number, so nothing can mistake it for a release, and every reader that renders a version treats it as "no version to show".
#FieldStr
const FieldStr FieldType = "str"FieldStr accepts a JSON string.
#FieldBool
const FieldBool FieldType = "bool"FieldBool accepts a JSON boolean.
#FieldInt
const FieldInt FieldType = "int"FieldInt accepts a JSON integer literal, never a boolean.
#FieldFloat
const FieldFloat FieldType = "float"FieldFloat accepts any JSON number, coercing an integer to a float.
#FieldDict
const FieldDict FieldType = "dict"FieldDict accepts a JSON object.
#FieldList
const FieldList FieldType = "list"FieldList accepts a JSON array.
#LintCodeValidator
var LintCodeValidator func(codes []string, source string) errorLintCodeValidator polices a config's "lint_ignore" list: it is called with the declared codes and the name of the source that declared them, and any error it returns becomes a [ConfigError].
It is a seam rather than a direct call because the lint-code registry is the lints package's to own, and this package must stay loadable without it. The command layer installs the registry-backed check at startup; while it is nil the list's codes are accepted as written, which is why installing it is part of wiring the binary and not optional.
#VALIDDeployProviders
var VALIDDeployProviders = []string{"cloudflare-pages", "github-pages"}VALIDDeployProviders is the closed set of hosting providers a deploy block may name.
#VALIDSearchEngines
var VALIDSearchEngines = []string{"pagefind"}VALIDSearchEngines is the closed set of engines that may answer a site's search UI. One member today, and the list is still the enumeration a config is checked against: the key is the extension point, so a second engine is a member added here rather than a new mechanism.
#Schema
var Schema = []FieldSpec{Schema declares every top-level field of selfdoc.json, in the order a loaded config's keys are resolved. It is the single authority: the unknown-key refusal, the resolved config's key set, and the generated configuration tables all read it.
Treat it as immutable. It is a package-level slice only so the generated documentation can walk it.
#Config
type Config = map[string]anyConfig is a loaded selfdoc.json: every top-level name in [Schema] is a key, carrying either the document's value or the field's default.
#ConfigError
type ConfigError structConfigError is returned when selfdoc.json is present but invalid.
#FieldType
type FieldType stringFieldType is the value shape a [FieldSpec] accepts.
#FieldSpec
type FieldSpec structFieldSpec is the declaration of a single configuration field: its type, whether it is required, what it defaults to, and every constraint the validator applies to it.
One spec describes one field, and a spec's children (for an object) or ItemSpec (for an array, or for an object with an open key set) describe what sits inside it, so the whole schema is one value rather than a set of validation functions.
#Load
func Load(dir string) (Config, error)Load reads and validates selfdoc.json from dir.
It returns a nil Config and a nil error when the file does not exist -- "this directory is not a selfdoc project" is an answer, not a failure. A malformed or invalid document is a [ConfigError].
#ValidateConfig
func ValidateConfig(raw any) (Config, error)ValidateConfig validates a raw config document and returns the resolved config.
This is what [Load] runs on the parsed contents of selfdoc.json; it is separate so a config assembled in memory goes through the same rules as one read from disk. The accepted value shapes are the ones [DecodeDocument] produces.
#IsUnversioned
func IsUnversioned(config map[string]any) boolIsUnversioned reports whether config declares the project has no public version.
It reads the declaration, not the rewritten "versions" array [postValidate] derives from it: the array is what the build addresses pages with, and the declaration is what says the project has no version at all.
#DecodeDocument
func DecodeDocument(data []byte) (any, error)DecodeDocument decodes a selfdoc.json document into the value shapes the validator expects, matching Python's json module: an object becomes a map[string]any, an array a []any, an integer literal an int64, a number carrying a fraction or an exponent a float64, a string a string, true/false a bool, and null a nil any.
Trailing content after the first value is an error, as it is for json.load. An integer literal too large for an int64 becomes a float64 -- the one place Python's arbitrary-precision int cannot be reproduced -- so such a value fails an integer field's type check instead of passing it.
#ConfigError.Error
func (e *ConfigError) Error() string { return e.Message }