Data Files

Data files provide structured data that is available to every template on your site. Place YAML, JSON, TOML, or CSV files in the data/ directory and access them through site.data.*.

# data/navigation.yaml
main:
  - label: "Home"
    url: "/"
  - label: "Blog"
    url: "/blog/"
  - label: "About"
    url: "/about/"
<nav>
  {% for item in site.data.navigation.main %}
    <a href="{{ item.url }}">{{ item.label }}</a>
  {% endfor %}
</nav>

Supported formats

Extension Parser Result type
.yaml, .yml YAML map[string]any
.json JSON Ordered map (preserves key insertion order)
.toml TOML map[string]any
.csv CSV Array of maps (header row = keys)

Each file is keyed by its filename without the extension. data/team.yaml becomes site.data.team, data/products.json becomes site.data.products.

Directory structure

Organize data files in any structure inside data/:

data/
├── navigation.yaml        # site.data.navigation
├── team.yaml              # site.data.team
├── products.json          # site.data.products
└── authors.csv            # site.data.authors

JSON key order preservation

JSON files preserve key insertion order using an ordered map. This matters when you need deterministic iteration order:

{
  "intro": { "title": "Introduction", "weight": 1 },
  "setup": { "title": "Setup", "weight": 2 },
  "usage": { "title": "Usage", "weight": 3 }
}

Iterating site.data.sections in a template produces keys in the order intro, setup, usage – matching the file. YAML and TOML files use standard Go maps, which do not guarantee key order. Use JSON when order matters, or add an explicit weight field and sort in the template.

CSV files

CSV files are parsed with the first row as headers. Each subsequent row becomes a map keyed by the header values:

name,role,github
Alice,Engineering Lead,alice
Bob,Designer,bob-designs
Carol,PM,carol-pm

Access in templates:

{% for person in site.data.authors %}
  <p>{{ person.name }} -- {{ person.role }}</p>
{% endfor %}

Name collision detection

Data files are keyed by stem name (filename without extension). If two files share a stem, the build fails:

[alloy] ERROR Data file conflict in data/:
        "team" is claimed by:
          1. team.csv
          2. team.yaml
        Resolve by renaming one file.
        Build aborted.

No silent overwrites, no priority system. Rename one file to resolve the collision.

External data files

Files outside the data/ directory can be mapped into the data namespace via config:

# alloy.config.yaml
data:
  files:
    cem: "../custom-elements.json"
    tokens: "node_modules/@rhds/tokens/json/rhds.tokens.json"

Each key becomes a site.data.* entry. Paths are resolved relative to the project root:

<p>Schema version: {{ site.data.cem.schemaVersion }}</p>

{% for token in site.data.tokens.color %}
  <div style="background: {{ token.value }}">{{ token.name }}</div>
{% endfor %}

External data files use the same parsers as data/ directory files. They share the same site.data.* namespace – moving a file between data/ and the external config is a config change, not a template change.

Collision handling

If an external file key matches a data/ directory file stem (e.g., cem key in config and data/cem.json on disk), the build fails with the same collision error. Choose external keys that do not conflict with filenames in data/.

External file not found is a build error – not a warning, not silently skipped.

External data sources

Alloy can fetch data from REST APIs and GraphQL endpoints at build time. Fetched data is injected into site.data.*, making it indistinguishable from local files in templates.

# alloy.config.yaml
sources:
  posts:
    type: "rest"
    url: "https://api.example.com/posts.json"
    cache: 3600
    as: "posts"

  products:
    type: "graphql"
    endpoint: "https://api.example.com/graphql"
    query: |
      { products { id, name, price, slug } }
    cache: 1800
    as: "products"

Access fetched data the same way as local files:

{% for post in site.data.posts %}
  <h2><a href="/blog/{{ post.slug }}/">{{ post.title }}</a></h2>
{% endfor %}

Caching

All fetched data is cached to .alloy/fetch-cache/ on disk. The cache value sets the TTL in seconds. Cached data survives process restarts. If the TTL has not expired, the cached data is used without fetching.

Combined with virtual pages

Fetched data feeds directly into pagination for page generation:

# content/products.md
---
pagination:
  data: site.data.products
  as: product
permalink: "/products/{{ product.slug }}/"
---
<h1>{{ product.name }}</h1>
<p>{{ product.price }}</p>

One template plus an external data source generates pages at build time with no individual content files.

Data in the cascade

Data files sit at the bottom of the Data Cascade. Global data provides site-wide defaults that directory data (_data.yaml) and front matter can override.

1. Global data       ← data files (lowest priority)
2. Directory data    ← _data.yaml
3. Front matter      ← per-page (highest priority)

Custom data directory

Override the default data/ path in config:

# alloy.config.yaml
structure:
  data: "./shared/data/"