Project Structure
A standard Alloy project follows this directory layout:
my-site/
├── alloy.config.yaml # Site config (YAML, TOML, or JSON)
├── content/ # Content files (Markdown, HTML)
│ ├── index.md # Site root page -> /
│ ├── about.html # Standalone page -> /about/
│ └── blog/
│ ├── _data.yaml # Directory-level data (cascades down)
│ ├── index.md # Blog landing -> /blog/
│ └── first-post.md # Blog post -> /blog/first-post/
├── layouts/ # Liquid templates
│ ├── default.liquid # Fallback layout
│ ├── post.liquid # Blog post layout
│ └── partials/ # Reusable template fragments
│ ├── header.liquid
│ └── footer.liquid
├── assets/ # Processed assets (CSS, JS, images)
├── static/ # Copied as-is to output
├── data/ # Global data files (YAML, JSON, CSV)
│ └── navigation.yaml
├── plugins/ # Optional plugins
├── components/ # SSR component sources (experimental)
└── .alloy/ # Internal (auto-generated, gitignore)
Config file
Alloy detects config files by extension in this order: alloy.config.yaml, alloy.config.yml, alloy.config.toml, alloy.config.json. The first match wins.
# alloy.config.yaml
title: "My Site"
baseURL: "https://example.com"
language: "en"
build:
output: "_site"
clean: true
content:
formats: ["md", "html"]
markdown:
goldmark:
unsafe: true
typographer: true
templateTags: true # false = treat {{ }} and {% %} as literal text
templates:
engine: "liquid"
taxonomies:
tags:
categories:
Front matter supports three formats, detected by delimiter:
| Delimiter | Format | Example |
|---|---|---|
--- |
YAML | --- title: "My Post" --- |
+++ |
TOML | +++ title = "My Post" +++ |
{ |
JSON | { "title": "My Post" } |
content/
Content files are Markdown or HTML with front matter. Front matter is required on all content files – empty front matter (---\n---) is valid, but missing delimiters is a build error.
Files whose extension does not match content.formats (default: ["md", "html"]) are treated as passthrough – copied directly to output without processing. This lets you colocate assets with content:
content/about/
├── index.md # content (processed)
├── hero.png # passthrough (copied to _site/about/hero.png)
└── diagram.svg # passthrough (copied to _site/about/diagram.svg)
HTML files without front matter are classified based on content: full documents (starting with <!DOCTYPE or <html>) become passthrough; fragments become content pages with empty front matter, inheriting layout from the _data.yaml cascade.
layouts/
Template files rendered by the configured engine. The Liquid engine looks for .liquid files first, falling back to bare extensions (e.g., .html) when no .liquid file is found. The Go template engine (engine: "gotemplate" or "go") resolves .html files only.
Layout lookup follows an explicit chain:
layout:from front matter or_data.yamlcascade- Convention match (e.g.,
post.liquidfor blog children, section name for index pages) default.liquidfallback- Build error if nothing matches
Layouts can chain via front matter layout: directives, enabling multi-level composition:
<!-- layouts/has-toc.liquid -->
---
layout: "base"
---
<div class="with-toc">
<aside>{% include "partials/toc" %}</aside>
<main>{{ content }}</main>
</div>
<!-- layouts/has-toc.html -->
---
layout: "base"
---
<div class="with-toc">
<aside>{{ include "partials/toc" }}</aside>
<main>{{ .content }}</main>
</div>
data/
Global data files accessible in templates as site.data.*. Supports YAML, TOML, JSON, and CSV. The filename (without extension) becomes the key:
data/
├── navigation.yaml # -> site.data.navigation
├── authors.json # -> site.data.authors
└── team.csv # -> site.data.team (array of maps)
Two files with the same stem name (e.g., team.csv and team.yaml) cause a build error – no silent overwrites.
External data files can be mapped into the data namespace via config:
data:
files:
tokens: "node_modules/@rhds/tokens/json/rhds.tokens.json"
cem: "../custom-elements.json"
These are accessible as site.data.tokens and site.data.cem, identical to local data files.
_data.yaml
Directory-level data files that cascade to all descendant content. Each _data.yaml deep-merges with its parent’s data. Common uses include setting a shared layout, permalink pattern, or default tags for an entire section:
# content/blog/_data.yaml
layout: post
permalink: "/blog/:year/:month/:slug/"
tags: ["blog"]
Every page in content/blog/ and its subdirectories inherits these values unless overridden in front matter.
static/
Files copied verbatim to the output root with no processing:
static/
├── favicon.ico # -> _site/favicon.ico
├── robots.txt # -> _site/robots.txt
└── images/
└── logo.png # -> _site/images/logo.png
assets/
Processed assets. Copied to output and available for plugin hooks like onAssetProcess.
plugins/
Optional directory for plugin files. Alloy detects the plugin tier from the file:
.jsfiles withoutruntime: "node"run on embedded QuickJS (Tier 2).wasmfiles run as compiled WASM via wazero (Tier 2).jsfiles withruntime: "node"run as Node subprocess (Tier 3)
The Node bridge is only spawned when the project has Tier 3 plugins.
.alloy/
Alloy creates this directory at runtime for caches, build artifacts, and runtime state. Add it to .gitignore – its contents are safe to delete and regenerated as needed.
| Path | Created by | Purpose |
|---|---|---|
.alloy/bridge.mjs |
alloy build, alloy dev, alloy serve (when Node plugins exist) |
Node plugin IPC bridge script |
.alloy/workers.pid |
alloy build, alloy dev, alloy serve (when Node plugins exist) |
Tracks Node worker PIDs for stale process cleanup |
.alloy/fetch-cache/ |
alloy build, alloy dev, alloy serve (when sources: configured) |
Data source response cache |
.alloy/wasm-cache/ |
alloy build, alloy dev, alloy serve (when WASM plugins exist) |
Compiled WASM module cache |
.alloy/profiles/ |
alloy build --profile |
pprof CPU and memory profiles |
.alloy/server.lock |
alloy dev, alloy serve |
Dev server lockfile for concurrent process detection |
Subdirectories are created on demand – only the ones relevant to your project appear.
Custom directory structure
Override default paths with the structure: config key. All paths are relative to the project root:
# alloy.config.yaml -- monorepo example
structure:
content: "./docs/pages/"
layouts: "./docs/layouts/"
assets: "./docs/assets/"
static: "./docs/static/"
data: "./data/"
plugins: "./tools/plugins/"
The pipeline, file watcher, and dev server all use the configured paths. When a key is omitted, it defaults to its standard name.
Passthrough copy
Map external directories into the output via config:
passthrough:
- from: "../design-system/dist/elements"
to: "elements"
- from: "../shared-assets/fonts"
to: "assets/fonts"
Passthrough supports glob patterns and exclude filters:
passthrough:
- from: "elements/**/*.{js,css}"
to: "assets/packages/elements"
exclude:
- "*.html"
- "demo/"
- "**/*.map"
See also CLI Reference for commands and flags.