"""Loads structured SSR copy (glossary, static-page SEO meta) from content/ — see docs/README.md. Validated at load time so a malformed content file fails loudly at import rather than silently rendering a blank/broken page. Separate from cities_flavor.json (backend/cities_flavor.json, machine-generated from Wikipedia by gen_flavor.py) and the Jinja templates themselves (backend/templates/*.html.j2, which still own page structure/markup) — this covers only the two content classes that are pure static data with no embedded template logic: the glossary, and each static page's SEO title/description. """ import os import yaml import paths _GLOSSARY_PATH = os.path.join(paths.CONTENT_DIR, "glossary.yaml") _PAGES_PATH = os.path.join(paths.CONTENT_DIR, "pages.yaml") _REQUIRED_GLOSSARY_FIELDS = ("term", "short", "body") _REQUIRED_PAGE_FIELDS = ("title", "description") def _load_yaml(path: str) -> dict: with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) if not isinstance(data, dict): raise ValueError(f"{path}: expected a top-level mapping, got {type(data).__name__}") return data def load_glossary(path: str = _GLOSSARY_PATH) -> "dict[str, dict]": """slug -> {term, short, body}, in file order. Raises ValueError if any entry is missing a required field, has the wrong shape, or repeats a slug — a bad content edit fails loudly (at content.py's module-level import, normally) rather than rendering a blank glossary card.""" data = _load_yaml(path) terms = data.get("terms") if not isinstance(terms, list) or not terms: raise ValueError(f"{path}: 'terms' must be a non-empty list") glossary: "dict[str, dict]" = {} for i, entry in enumerate(terms): if not isinstance(entry, dict) or "slug" not in entry: raise ValueError(f"{path}: terms[{i}] must be a mapping with a 'slug' key") slug = entry["slug"] missing = [f for f in _REQUIRED_GLOSSARY_FIELDS if not entry.get(f)] if missing: raise ValueError(f"{path}: term '{slug}' is missing {missing}") if slug in glossary: raise ValueError(f"{path}: duplicate slug '{slug}'") glossary[slug] = {f: entry[f] for f in _REQUIRED_GLOSSARY_FIELDS} return glossary def load_pages(path: str = _PAGES_PATH) -> "dict[str, dict]": """page key -> {title, description}. Same fail-loud contract as load_glossary.""" data = _load_yaml(path) pages = data.get("pages") if not isinstance(pages, dict) or not pages: raise ValueError(f"{path}: 'pages' must be a non-empty mapping") result: "dict[str, dict]" = {} for key, entry in pages.items(): if not isinstance(entry, dict): raise ValueError(f"{path}: page '{key}' must be a mapping") missing = [f for f in _REQUIRED_PAGE_FIELDS if not entry.get(f)] if missing: raise ValueError(f"{path}: page '{key}' is missing {missing}") result[key] = {f: entry[f] for f in _REQUIRED_PAGE_FIELDS} return result