Skip to content

Content Model

pageflare can read structured markdown content from your repo and use it as the source of truth for what each page should expose to search engines, AI agents, and humans. Frontmatter on every page declares the title, description, canonical URL, schema.org type, publish dates, and more. A single site.yaml at the root carries brand defaults that apply everywhere.

The same content drives multiple outputs in one pipeline pass:

  • HTML <head> injection — title, meta description, canonical, robots, Open Graph, Twitter cards.
  • JSON-LD blocks — Organization, page-type (Article / BlogPosting / WebPage / Product / ProfilePage), BreadcrumbList, plus any custom overrides.
  • sitemap.xml — drafts excluded, lastmod filled from publish.updated.
  • llms.txt / llms-full.txt — drafts excluded.

You author content in markdown once, and every downstream output stays in sync.

pageflare looks for content under a content/ directory at the project root. Each subdirectory is a content type:

content/
├── site.yaml # global brand + defaults + route patterns
├── pages/ # standalone pages → WebPage by default
│ ├── index.md # → /
│ ├── about.md # → /about/
│ └── legal/
│ └── privacy.md # → /legal/privacy/
├── posts/ # blog posts → BlogPosting by default
│ ├── hello-world.md # → /blog/hello-world/
│ └── deep-dive.md # → /blog/deep-dive/
└── docs/ # documentation → WebPage by default
└── getting-started/
└── installation.md # → /docs/getting-started/installation/
TypeDefault URL patternDefault schema.org page type
pages/{rel-path}/WebPage
posts/blog/{slug}/BlogPosting
docs/docs/{rel-path}/WebPage

index.md collapses to the directory’s URL (so pages/index.md/, docs/getting-started/index.md/docs/getting-started/).

Custom content types are allowed — register them under content.types in pageflare.jsonc with a URL pattern.

  1. slug: in frontmatter wins absolutely. slug: "/totally-custom/" overrides any pattern.
  2. Route override in site.yamlroutes.<type>.pattern overrides the built-in default.
  3. Built-in default pattern for the content type.

Trailing slashes are always normalized.

The content/site.yaml file declares site-wide brand identity and defaults. Most fields are optional, but site.name, site.url, and site.locale are required.

site:
name: "Acme Docs"
url: "https://acme.dev"
locale: "en-US"
default_og_image: "/content/assets/og-default.png"
twitter_handle: "@acme"
brand:
organization:
name: "Acme Inc."
url: "https://acme.dev"
logo: "/content/assets/logos/acme-mark.svg"
same_as:
- "https://github.com/acme"
- "https://x.com/acme"
defaults:
seo:
robots: "index,follow"
twitter:
card: "summary_large_image"
routes:
posts:
pattern: "/articles/{slug}/"
FieldRequiredDescription
nameYesDisplay name of the site — used in og:site_name and as the default BlogPosting.publisher.name.
urlYesSite base URL (no trailing slash). Used to absolutize canonical URLs and BreadcrumbList items.
localeYesBCP-47 locale, e.g. en-US, de-DE. Used in og:locale.
default_og_imageNoPath or absolute URL — fallback for any page that doesn’t set seo.og.image.
twitter_handleNoDefault twitter:site value.

When present, pageflare emits an Organization JSON-LD block on every page. This is what tells AI crawlers and Google’s Knowledge Graph what entity the site belongs to.

FieldRequiredDescription
nameYesOrganization legal/display name.
urlYesOrganization homepage. Usually the same as site.url.
logoNoLogo URL — must be a square image per schema.org guidance.
same_asNoArray of canonical profile URLs (GitHub, X/Twitter, LinkedIn, Crunchbase). Strengthens entity linking.

Defaults fill in any field a page doesn’t explicitly set. They cascade — a page can override one field while inheriting the rest.

  • defaults.seo — same shape as the per-page seo: block. Common use: a site-wide robots: "index,follow" and a default Twitter card style.

Override the built-in URL pattern for a content type. Patterns understand two placeholders:

  • {slug} — the file stem (filename without .md).
  • {rel-path} — the file’s path relative to the type directory, without extension.

Example: routes.posts.pattern: "/articles/{slug}/" makes content/posts/hello.md resolve to /articles/hello/ instead of /blog/hello/.

The content pipeline runs once per page:

  1. Scancontent/<type>/**/*.md files are discovered, frontmatter parsed, URLs resolved.
  2. Match — each HTML page found by the optimizer is matched against a content entry by URL.
  3. Inject — frontmatter drives <head> SEO tags, JSON-LD blocks, and the canonical URL.
  4. Filter — pages with draft: true are removed from sitemap.xml and llms.txt.
  5. Datepublish.updated (falling back to publish.date) fills <lastmod> in sitemap.xml.

If a page has no matching content entry, it’s still optimized — pageflare just doesn’t inject anything that isn’t already in the HTML.