Rustelo/resources/nickel/content/metadata/post_metadata.ncl
Jesús Pérez f1010e9d07
chore: update
2026-07-18 20:16:16 +01:00

156 lines
4.3 KiB
Text

# 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,
}