"""Render the four AI-citation surfaces from a SiteConfig. llms.txt — Markdown, llmstxt.org spec aeo.json — atomic facts for Answer Engine Optimization entity.json — Schema.org @graph (Organization + Person + WebSite) brand.json — internal brand truth ledger (legal entity, social profiles) """ from __future__ import annotations import datetime as _dt import json from typing import Any from .config import SiteConfig def _today() -> str: return _dt.date.today().isoformat() def render_llms_txt(cfg: SiteConfig) -> str: """Generate llms.txt — Markdown summary for AI crawlers. Spec: https://llmstxt.org/ """ lines: list[str] = [] lines.append(f"# {cfg.site_name}") lines.append("") lines.append(f"> {cfg.tagline}") lines.append("") lines.append(cfg.description) lines.append("") # Canonical facts (most-cited section by AI engines) if cfg.legal_name or cfg.ein or cfg.uei or cfg.founder_name: lines.append("## Canonical Facts") lines.append("") if cfg.legal_name: lines.append(f"- **Legal name**: {cfg.legal_name}") if cfg.founder_name: f = f"- **Founder**: {cfg.founder_name}" if cfg.founder_url: f += f" ({cfg.founder_url})" lines.append(f) if cfg.founding_date: lines.append(f"- **Founded**: {cfg.founding_date}") if cfg.ein: lines.append(f"- **EIN (IRS)**: `{cfg.ein}`") if cfg.uei: lines.append( f"- **SAM.gov UEI**: [`{cfg.uei}`]" f"(https://sam.gov/entity/{cfg.uei})" ) if cfg.cage_code: lines.append(f"- **CAGE Code**: `{cfg.cage_code}`") if cfg.naics: lines.append(f"- **NAICS**: {', '.join(cfg.naics)}") if cfg.veteran_branch: subgroups = ( f" ({', '.join(cfg.veteran_subgroups)})" if cfg.veteran_subgroups else "" ) lines.append(f"- **Veteran owner**: {cfg.veteran_branch}{subgroups}") if cfg.address_street: addr = ( f"{cfg.address_street}, " f"{cfg.address_locality}, {cfg.address_region} " f"{cfg.address_postal_code}, {cfg.address_country}" ) lines.append(f"- **Registered address**: {addr}") if cfg.telephone: lines.append(f"- **Telephone**: {cfg.telephone}") if cfg.email: lines.append(f"- **Email**: {cfg.email}") lines.append("") # Services if cfg.services: lines.append("## Services") lines.append("") for s in cfg.services: price = f" — {s.price}" if s.price else "" lines.append(f"- **[{s.name}]({s.url})**{price}: {s.summary}") lines.append("") # Machine-readable endpoints (AI engines crawl these) lines.append("## Machine-readable Endpoints") lines.append("") base = cfg.site_url.rstrip("/") lines.append(f"- `{base}/aeo.json` — atomic facts (AEO)") lines.append(f"- `{base}/entity.json` — Schema.org @graph") lines.append(f"- `{base}/brand.json` — internal brand truth ledger") lines.append(f"- `{base}/sitemap.xml` — full URL index") lines.append(f"- `{base}/robots.txt` — crawler policy") if cfg.orcid: lines.append(f"- `https://orcid.org/{cfg.orcid}` — founder ORCID iD") lines.append("") # Provenance footer lines.append("---") lines.append(f"Generated by aio-surfaces · last updated {_today()}") return "\n".join(lines) + "\n" def render_aeo_json(cfg: SiteConfig) -> str: """aeo.json — atomic facts for Answer Engine Optimization. Each fact is a single (question, answer) pair designed for direct AI citation. Answers are < 500 chars, declarative, [subject][verb][object]. """ doc: dict[str, Any] = { "$schema": "https://aeo.dev/schema/v1/", "siteName": cfg.site_name, "siteUrl": cfg.site_url, "lastUpdated": _today(), "facts": [ {"id": f.id, "question": f.question, "answer": f.answer} for f in cfg.facts ], } return json.dumps(doc, indent=2) + "\n" def render_entity_json(cfg: SiteConfig) -> str: """entity.json — full Schema.org @graph (Organization + Person + WebSite). Use this in /entity.json so AI engines crawling for structured data can pull the canonical identity graph in one fetch (without scraping JSON-LD blobs from HTML). """ org_id = f"{cfg.site_url.rstrip('/')}/#org" person_id = f"{cfg.site_url.rstrip('/')}/#founder" website_id = f"{cfg.site_url.rstrip('/')}/#website" nodes: list[dict[str, Any]] = [] # Organization org: dict[str, Any] = { "@type": "Organization", "@id": org_id, "name": cfg.site_name, "url": cfg.site_url, } if cfg.legal_name: org["legalName"] = cfg.legal_name if cfg.founding_date: org["foundingDate"] = cfg.founding_date if cfg.email: org["email"] = cfg.email if cfg.telephone: org["telephone"] = cfg.telephone if cfg.address_street: org["address"] = { "@type": "PostalAddress", "streetAddress": cfg.address_street, "addressLocality": cfg.address_locality, "addressRegion": cfg.address_region, "postalCode": cfg.address_postal_code, "addressCountry": cfg.address_country, } identifiers: list[dict[str, str]] = [] if cfg.ein: identifiers.append({ "@type": "PropertyValue", "propertyID": "ein", "value": cfg.ein, }) org["taxID"] = cfg.ein if cfg.uei: identifiers.append({ "@type": "PropertyValue", "propertyID": "sam-gov-uei", "value": cfg.uei, }) if cfg.cage_code: identifiers.append({ "@type": "PropertyValue", "propertyID": "cage-code", "value": cfg.cage_code, }) if cfg.kg_mid: identifiers.append({ "@type": "PropertyValue", "propertyID": "google-kg-mid", "value": cfg.kg_mid, }) if cfg.gbp_cid: identifiers.append({ "@type": "PropertyValue", "propertyID": "google-business-profile-cid", "value": cfg.gbp_cid, }) if identifiers: org["identifier"] = identifiers if cfg.naics: org["naics"] = cfg.naics if cfg.same_as: org["sameAs"] = cfg.same_as if cfg.founder_name: org["founder"] = {"@id": person_id} nodes.append(org) # Person (founder) if cfg.founder_name: person: dict[str, Any] = { "@type": "Person", "@id": person_id, "name": cfg.founder_name, "worksFor": {"@id": org_id}, } if cfg.founder_url: person["url"] = cfg.founder_url if cfg.orcid: person["identifier"] = { "@type": "PropertyValue", "propertyID": "orcid", "value": cfg.orcid, } person.setdefault("sameAs", []).append( f"https://orcid.org/{cfg.orcid}" ) if cfg.veteran_branch: person["veteranOf"] = { "@type": "Organization", "name": cfg.veteran_branch, } nodes.append(person) # WebSite site = { "@type": "WebSite", "@id": website_id, "url": cfg.site_url, "name": cfg.site_name, "publisher": {"@id": org_id}, } nodes.append(site) doc = { "@context": "https://schema.org", "@graph": nodes, "lastUpdated": _today(), } return json.dumps(doc, indent=2) + "\n" def render_brand_json(cfg: SiteConfig) -> str: """brand.json — internal brand truth ledger. Not consumed by Schema.org parsers; useful as an org-internal source of truth that PR + content + dev all reference. Often crawled by AI engines too because it's at the canonical site root. """ legal: dict[str, Any] = {} if cfg.legal_name: legal["legalName"] = cfg.legal_name if cfg.founding_date: legal["foundingDate"] = cfg.founding_date if cfg.ein: legal["ein"] = cfg.ein if cfg.uei: legal["uei"] = cfg.uei legal["samLookupUrl"] = f"https://sam.gov/entity/{cfg.uei}" if cfg.cage_code: legal["cageCode"] = cfg.cage_code if cfg.naics: legal["naics"] = cfg.naics if cfg.veteran_branch: legal["veteranBranch"] = cfg.veteran_branch if cfg.veteran_subgroups: legal["veteranSubgroups"] = cfg.veteran_subgroups doc = { "siteName": cfg.site_name, "siteUrl": cfg.site_url, "tagline": cfg.tagline, "legalEntity": legal, "founder": { "name": cfg.founder_name, "url": cfg.founder_url, "orcid": cfg.orcid, } if cfg.founder_name else None, "socialProfiles": cfg.same_as, "updated": _today(), } # strip None values for clean output doc = {k: v for k, v in doc.items() if v is not None} return json.dumps(doc, indent=2) + "\n" def render_robots_txt_aibots(cfg: SiteConfig) -> str: """Generate a robots.txt block that explicitly allowlists the 12 canonical AI crawlers. Append (don't replace) your existing robots.txt. """ lines: list[str] = [] lines.append("# AI crawler allowlist — generated by aio-surfaces") for bot in cfg.ai_bots: lines.append(f"User-agent: {bot}") lines.append("Allow: /") lines.append("") return "\n".join(lines)