# Post Metadata Schema # # Canonical contract for any content item (blog, article, page, etc.). # Consumed by: # - ContentIndexer: validates *.ncl files, builds in-memory index # - TypeDialog: generates web/CLI/TUI forms from this schema # - DB migration: struct maps directly to posts table # # Usage — preferred (make_post factory): # # let schema = import "site/nickel/content/metadata/post_metadata.ncl" in # schema.make_post { # title = "My Rust Post", # slug = "my-rust-post", # date = "2024-01-15", # category = "rust", # } # # The factory merges site defaults (Layer 1) with the provided overrides # (Layer 2) then applies PostMetadata as a contract for type checking. let c = import "contracts.ncl" in let d = import "defaults.ncl" in let g = import "graph_contracts.ncl" in { PostMetadata = { # --- Identity --- id | String | c.NonEmptyString | doc "Unique stable identifier. Inferred from filename stem when absent. Never changes after publication." | optional, title | String | c.NonEmptyString | doc "Post title shown in listings, headings, and browser tab", slug | String | c.SlugContract | doc "URL path segment: lowercase, hyphens only. E.g. 'my-rust-post-2024'", subtitle | String | doc "Secondary heading displayed under the title in the post view" | optional, excerpt | String | doc "Short summary (1-3 sentences) for index listings and SEO meta description" | optional, # --- Content file reference --- content_file | String | doc "Filename of the content body relative to this *.ncl file. Inferred from slug when absent." | optional, # --- Publication --- author | String | c.NonEmptyString | doc "Author display name shown in post header and listings" | default = "Jesús Pérez Lorenzo", date | String | c.DateContract | doc "Original publication date in YYYY-MM-DD format", updated_at | String | c.DateContract | doc "Date of last significant update in YYYY-MM-DD format" | optional, published | Bool | doc "False hides the post from all index listings and the API" | default = true, featured | Bool | doc "Pins to the top of the category listing and enables featured styling" | default = false, draft | Bool | doc "Draft posts are excluded from the index and not served by /api/content-render" | default = false, # --- Taxonomy --- category | String | c.NonEmptyString | doc "Primary category slug — must match the parent directory name (e.g. 'rust', 'devops')", tags | Array String | doc "Secondary classification tags for filtering and related-content discovery" | default = [], # --- Display hints --- read_time | String | c.ReadTimeContract | doc "Estimated reading time for the listing card. E.g. '8 min read'. Auto-derived from word count when absent." | optional, sort_order | Number | doc "Manual position within the category listing. Lower values appear first. 0 = no preference." | default = 0, # --- SEO --- image_url | String | doc "Open Graph and social share image. Absolute URL or root-relative path." | optional, thumbnail | String | doc "Card thumbnail image. Absolute URL or root-relative path. Falls back to image_url when absent." | optional, # --- Multilingual --- translations | { _ : c.TranslationEntry } | doc "Map of language-code → { id, slug } for the translation. E.g. { es = { id = 'my-post', slug = 'mi-post-en-rust' } }." | default = {}, # --- Knowledge graph --- graph | g.GraphMeta | doc "Explicit relationships to other content items and on+re ontology nodes. Consumed by the content-graph feature. Ignored when the feature is not active." | default = {}, }, # Factory function — Layer 1 (defaults) & Layer 2 (overrides) | contract. # Not exported to JSON; accessible only via Nickel import. make_post | not_exported | doc "Create a validated post metadata record. Provide at minimum: title, slug, date, category." = fun overrides => d & overrides | PostMetadata, }