Content Mounts

Content mounts let you combine multiple filesystem directories into a single site. Each mount attaches a directory at a specific URL prefix in the content tree – no symlinks, no copy scripts, no build-time workarounds.

When to Use Mounts

  • Shared documentation: Mount a company-wide docs library under /docs/shared/
  • Vendored content: Mount third-party documentation at /docs/vendor/
  • Split editorial and technical content: Keep blog posts in one directory and API docs in another
  • Multi-team sites: Each team maintains their own content directory, merged into one site

Configuration

Add content.mounts to your config.yaml:

# config.yaml
content:
  directory: ./content                  # primary content (always present)

  mounts:
    # Merge shared content into root namespace
    - source: ./shared-content
      mount: /

    # Vendor docs appear under /docs/vendor/
    - source: /opt/docs/vendor
      mount: /docs/vendor

    # API reference with high priority (overrides primary on collision)
    - source: ./api-docs
      mount: /docs/api/reference
      priority: high
      default_template: api-page.html.jinja

The primary content.directory always loads first. Mounts are processed in declaration order after it.

Mount Levels

Mounts can attach at any level of the content tree:

LevelMount PathEffect
0 (root)/Pages merge directly into the top-level URL space
1 (section)/docsPages appear under /docs/page-name
2 (subsection)/docs/api/referencePages nested two levels deep

Root Mounts

A mount at / merges its pages into the same URL space as your primary content directory. A page at getting-started/default.md in the mount source becomes /getting-started – the same as if it were in your primary content directory.

mounts:
  - source: ./shared-content
    mount: /

Section and Subsection Mounts

Mounts at deeper levels prefix all page URLs with the mount path. A page at widgets/default.md in a mount at /products becomes /products/widgets.

mounts:
  - source: ./product-pages
    mount: /products

Mount Options

Each mount supports these fields:

FieldRequiredDefaultDescription
sourceyes–Filesystem path or plugin:<name> identifier
mountyes–URL prefix where content appears (must start with /)
prioritynolowCollision resolution: low or high
default_templateno–Fallback template for pages without template: in frontmatter

Plugin Mounts

Mounts can also source content from WASM plugins using the plugin:<name> syntax. The plugin’s on_content_load hook is called at startup, and the returned pages are merged into the content tree under the mount prefix.

content:
  mounts:
    - source: plugin:accent-notion
      mount: /kb
      default_template: kb-article.html.jinja

The plugin must declare on_content_load = true in its plugin.toml:

[hooks]
on_content_load = true

[config]
api_url = "https://api.notion.com/v1"
api_key = ""
refresh_interval_seconds = 120

[network]
allowed_hosts = ["api.notion.com"]

How Plugin Mounts Work

  1. At startup, Accent calls the plugin’s on_content_load WASM function
  2. The plugin fetches content from its external source (API, database, etc.)
  3. The plugin returns pages as JSON with relative URLs and markdown content
  4. Accent applies the mount prefix to each URL and merges pages into the content tree
  5. Plugin pages participate in taxonomy, collections, navigation, and static builds

Plugin JSON Protocol

The plugin receives:

{
  "config": { "api_url": "...", "api_key": "..." },
  "mount": "/kb",
  "host": {
    "production": false,
    "site_name": "My Site",
    "site_url": "https://example.com",
    "language": "en"
  }
}

And returns:

{
  "pages": [
    {
      "url": "/getting-started",
      "markdown": "# Getting Started\n\nWelcome...",
      "frontmatter": {
        "title": "Getting Started",
        "tags": ["docs"],
        "template": "kb-article.html.jinja"
      }
    }
  ],
  "error": null
}

Plugins return relative URLs (like /getting-started). Accent prepends the mount path, so the page appears at /kb/getting-started. This makes plugins mount-agnostic – the same plugin can be mounted at different paths in different sites.

Plugin Failures

Plugin failures during on_content_load are non-fatal. If a plugin cannot reach its API or returns an error, the server still starts with all other content sources. A warning is logged for each failure.

Collision Resolution

When a mounted page has the same URL as a page in your primary content directory (or another mount), Accent resolves the conflict based on priority:

  • priority: low (default) – The primary content page wins. The mount page is skipped and a warning is logged.
  • priority: high – The mount page wins and replaces the primary content page. A warning is still logged.
mounts:
  # These API docs override anything at /docs/api/ in primary content
  - source: ./generated-api-docs
    mount: /docs/api
    priority: high

Collisions always produce a log warning so you can diagnose unexpected shadowing:

WARN Mount '/docs/vendor' page at '/docs/vendor/setup' collides with existing page from primary content, keeping existing (mount priority: low)

Default Templates

Mounts can specify a default_template that applies to pages without an explicit template: field in their frontmatter. This is useful when mounting content that wasn’t authored for your theme:

mounts:
  - source: ./api-docs
    mount: /docs/api
    default_template: api-page.html.jinja

Pages that explicitly set template: in their frontmatter are unaffected – default_template only fills in the gap when no template is specified.

Hierarchy and Navigation

Mounted pages integrate naturally into Accent’s page hierarchy:

  • Breadcrumbs work across mount boundaries. A page at /docs/vendor/getting-started shows breadcrumbs like Home > Docs > Vendor > Getting Started.
  • Parent/child relationships are resolved by URL structure. The parent of /docs/vendor/setup is /docs/vendor (if an index page exists there), or /docs (the next ancestor with a page).
  • Sibling navigation (previous/next) works within the mount’s URL subtree.

For breadcrumbs to show the mount root as a level, include an index page (e.g., default.md) in the mount source directory’s root.

Hot Reload

In development mode, Accent watches all mounted directories for changes. When you edit a file in a mount source directory, the page cache is invalidated and your browser refreshes automatically (if browser reload is enabled).

No server restart is needed – save a file in any mounted directory and refresh.

Static Build

accent build includes all mounted content in the static output. Mounted pages appear in:

  • The generated HTML files under their mount prefix
  • The sitemap (/sitemap.xml)
  • RSS feeds (/feed.xml) if they match the feed criteria
  • Taxonomy pages (/tags/) based on their frontmatter tags

Reserved Routes

Mounts cannot claim Accent’s built-in route prefixes. The following paths are protected:

/healthz, /readyz, /health, /_dev, /_admin, /_search, /feed.xml, /sitemap.xml, /robots.txt, /.well-known, /api, /media, /content-media, /theme, /tags

Attempting to mount at any of these paths produces a configuration error at startup.

Root mounts (mount: /) are allowed – they merge into the root namespace without claiming a reserved prefix.

Example: Documentation Site with Shared Library

# config.yaml
content:
  directory: ./content

  mounts:
    # Company-wide shared docs
    - source: ../shared-docs-library/content
      mount: /docs/shared

    # Auto-generated API reference
    - source: ./target/api-docs
      mount: /docs/api/reference
      priority: high
      default_template: api-reference.html.jinja

Directory structure:

my-site/
  config.yaml
  content/
    01.home/default.md          -> /home
    02.docs/default.md          -> /docs
    02.docs/01.guides/default.md -> /docs/guides
  target/api-docs/
    users/default.md            -> /docs/api/reference/users
    products/default.md         -> /docs/api/reference/products

../shared-docs-library/content/
  style-guide/default.md        -> /docs/shared/style-guide
  brand-guidelines/default.md   -> /docs/shared/brand-guidelines

Limitations

  • Config reload does not pick up mount changes. Sending SIGHUP or calling POST /_admin/reload re-scans content within existing mounts (new and changed pages appear), but adding, removing, or changing mount source paths in config.yaml requires a server restart. This matches the behavior of content.directory itself.

Backward Compatibility

Sites without content.mounts work identically to before. The content.directory field is unchanged and continues to be the only required content configuration. Mounts are fully opt-in.