package content import ( "errors" "fmt" "io/fs" "strings" "gopkg.in/yaml.v3" ) // SettingsFile is where a site declares itself, at the site root. const SettingsFile = "site.yaml" // Settings are the site's own declarations (ADR-0039). // // Declared keys only: this is not a bag for arbitrary engine internals, which is how a settings file turns // into unbounded configuration. Section-level and bundle-level settings are not here — a bundle's own // frontmatter covers it, and the rest of the cascade is still parked. type Settings struct { // Base is the site's canonical origin, without a trailing slash: "https://khosra.example". Empty when // the site declares none, in which case the engine emits paths and nothing absolute. Base string `yaml:"base"` // Title names the site. A theme may put it in a document title or a feed; the engine only carries it. Title string `yaml:"title"` } // LoadSettings reads site.yaml from the site root. // // A missing file is not an error, because a bare site root must still serve (ADR-0026). A malformed one is, // and the caller treats it as fatal: unlike a single bad bundle, which is skipped loudly (ADR-0029), a // broken declaration misconfigures every page on the site. func LoadSettings(fsys fs.FS) (Settings, error) { data, err := fs.ReadFile(fsys, SettingsFile) if errors.Is(err, fs.ErrNotExist) { return Settings{}, nil } if err != nil { return Settings{}, fmt.Errorf("read %s: %w", SettingsFile, err) } var s Settings if err := yaml.Unmarshal(data, &s); err != nil { return Settings{}, fmt.Errorf("parse %s: %w", SettingsFile, err) } s.Base = strings.TrimSuffix(strings.TrimSpace(s.Base), "/") s.Title = strings.TrimSpace(s.Title) return s, nil } // Absolute turns a path the engine emitted into a full URL. // // With no declared base it returns the path unchanged, so a site that has not said where it lives still // links correctly to itself — every internal path is root-relative already. func Absolute(base, path string) string { if base == "" { return path } return base + path }