Build a documentation edition for each customer from one shared content project. Keep product definitions in the shared repository and supply each customer's purchases, additional features, and branding through that deployment's own profile file. Customer profiles do not need to share a repository.
By default, you do not need to mark every page. Pages without feature requirements are shared across customer editions. For customer deployments that need a deliberate decision on each page, enable explicit classification. In either mode, add requirements to pages or sections that depend on specific features.
Available since 0.1.0-alpha.15.
Before you begin
Start with a configured documentation project, its authored pages, and CLI 0.1.0-alpha.15 or later. Your deployment must be able to supply one local profile file and any declared branding assets. The fictional names and products below illustrate the format; replace them with your own catalog.
A stock keeping unit (SKU) identifies a product or bundle you sell. A feature ID identifies functionality that documentation describes. A SKU revision grants a set of features; a profile can also grant features directly for standalone purchases or grandfathered agreements.
Define the shared SKU manifest
Create sku-manifest.yaml beside docs.config.yaml:
schemaVersion: 1
features:
analytics: { description: Analytics reports }
connectors: { description: External data connectors }
reports.classic: { lifecycle: deprecated }
skus:
professional:
revisions:
"2025-04":
features: [analytics, reports.classic]
"2026-10":
features: [analytics, connectors]
Retain historical revisions unchanged when packaging changes. An existing customer can continue using the original revision, or receive an explicit feature grant. Marking a feature deprecated or retired does not remove its documentation grant.
Enable profile selection in the shared configuration:
profiles:
manifest: ./sku-manifest.yaml
required: true
pruneNavigation: true
required defaults to true. A missing profile fails the build. pruneNavigation defaults to false; enabling it removes known excluded destinations and empty groups. Misspelled navigation targets remain errors.
Mark pages and shared sections
Pages declare feature requirements with requires. They do not name SKU IDs. The profile's SKU mappings and direct grants determine which features are available.
Unmarked pages are shared by default
With profiles.requireClassification omitted or false, a page with no authored or inherited requires is included in every eligible customer edition. Common setup, introductions, and shared troubleshooting can remain unmarked.
Shared content still has to pass the project's access, publication, language, and facet rules. A draft stays excluded from normal builds, and a page for an older version still needs that publication enabled.
The following table shows feature selection after those other rules pass. Grants can come from SKUs or direct feature entries:
| Page requirements | No granted features | Analytics only | Analytics and Connectors |
|---|---|---|---|
| No authored or inherited requirements | Included | Included | Included |
allOf: [analytics] |
Omitted | Included | Included |
allOf: [analytics, connectors] |
Omitted | Omitted | Included |
anyOf: [analytics, connectors] |
Omitted | Included | Included |
Forgetting a requirement makes a feature-specific page shared. Shoalrun does not infer requirements from its folder, title, product metadata, or navigation label. Enable explicit classification to catch missing page declarations, and review feature-specific content before distributing an edition.
Require a deliberate page classification
Enable strict classification in the shared project configuration:
profiles:
manifest: ./sku-manifest.yaml
required: true
pruneNavigation: true
requireClassification: true
Every public page in the shared project's enabled publications must now explicitly declare shared: true or requires in its own source frontmatter. An unclassified page fails the build with its source filename; it does not silently disappear. The check runs before customer feature and publication selection, so a later purchase or publication change cannot expose a previously unchecked page. Drafts and pages outside the shared project's enabled publications do not need classification for that build.
For common instructions, add this page-level declaration:
shared: true
For a product-specific page, use requires as shown below. Do not combine shared: true and requires on the same page. shared: false is invalid. shared must stay page-specific and cannot appear in frontmatter defaults or enforced settings. Inherited feature requirements still restrict content, but do not satisfy strict classification: each page needs its own declaration.
To migrate, enable the policy locally and classify the pages named in build errors after reviewing their content. Put the declaration in page templates to make the choice part of creating a page; avoid marking an entire collection shared without reviewing it.
This policy catches missing declarations. It cannot tell whether a declaration is correct or detect an ungated product-specific paragraph inside a shared page. It does not authenticate readers; the deployment still controls access to the resulting edition.
Restrict a feature-specific page
Add a requirement to each page that depends on specific functionality. This integration guide needs both features:
requires:
allOf: [analytics, connectors]
Use anyOf when either feature is sufficient. If a requirement contains both clauses, both must pass. When the resolved requirements do not match the profile's grants, the whole page is omitted.
Restrict part of a shared page
A page classified shared: true can still contain product-specific sections. Use <ForFeature> around each section that depends on features. Its attributes contain whitespace-separated feature IDs:
These setup instructions apply to every customer.
<ForFeature allOf="analytics connectors">
## Connect your reports <Anchor id="connect-reports" />
Import a dataset through a connector, then select it in your analytics report.
</ForFeature>
These troubleshooting tips apply to every customer.
The page's content outside <ForFeature> stays shared. A block appears only when its requirements match; excluding a block does not exclude the rest of the page. If a page-level requirement excludes the whole page, a matching block inside it cannot restore that page.
Excluded blocks contribute no text, links, snippet expansions, API embeds, or images to the edition. Put links to optional pages inside matching conditions. A link remaining in selected prose must point to a selected destination.
Classify generated API references
Generated pages take an explicit classification from their API definition or OpenAPI operation. For an API whose generated references are shared, add shared: true beside generateReferences:
apis:
example:
shared: true
source:
kind: file
path: ./openapi.yaml
focused: true
generateReferences: true
For a feature-specific API, use requires instead of shared. An individual OpenAPI operation can declare x-shoalrun-shared: true or x-shoalrun-requires: { allOf: [analytics] }. Its declaration satisfies classification for that generated page; API-level feature requirements still apply. Operations with differing requirements need their own declarations. General frontmatter defaults do not satisfy generated-page classification. Authored API reference pages follow the ordinary page rule.
Supply the profile from the deployment
Mount or generate the customer profile outside the shared docs repository. For example, a deployment could provide /run/config/docs-profile.yaml and a brand directory beside it:
schemaVersion: 1
skus:
- { id: professional, revision: "2026-10" }
features:
- reports.classic
site:
title: Example Partner documentation
origin: https://docs.partner.example
logo: ./brand/logo.svg
favicon: ./brand/icon.png
theme:
palette: kelp
publications:
enabled:
- { channel: production, version: current, language: en }
This profile grants Analytics and Connectors through the SKU, plus Classic Reports through a direct grant. A customer with only grandfathered functionality can omit skus and declare features: [reports.classic]. No synthetic SKU is needed.
SKU and direct grants combine into a set. Unknown IDs and revisions fail validation. Declare features: [] with no SKU grants for a shared-content-only edition; null is invalid. That edition still includes eligible shared pages and text outside feature blocks, while pages and blocks requiring a feature are omitted. A missing required profile fails the build rather than selecting this shared-only behavior.
Profile logo and favicon paths resolve from the profile directory and must stay inside it. Shared configuration paths retain their own origins. Shoalrun reads the chosen profile and declared assets without discovering other customers' files in that deployment directory.
Profiles accept selected site, theme, and existing snippet variables settings. They can narrow publications.enabled to combinations already enabled by the shared project. They cannot replace content sources, register plugins, change access policy, or override navigation. Profile CSS, custom fonts, and file-backed palettes are not supported.
Build and inspect the edition
From the shared documentation project directory, build into a deployment-specific output:
shoalrun build docs.config.yaml dist/customer \
--profile /run/config/docs-profile.yaml
To export application content, pass the same profile to shoalrun export:
shoalrun export docs.config.yaml --out dist/customer-content \
--profile /run/config/docs-profile.yaml
If several publications are enabled, an export still needs its normal explicit publication selection. Grandfathered features and older instructions are independent: the feature grant selects functionality, while the publication version selects the release's instructions.
Use the profile when validating documentation edits:
shoalrun docs baseline --config docs.config.yaml \
--out .shoalrun/baseline.json --profile /run/config/docs-profile.yaml
# Edit your documentation, then validate the same edition.
shoalrun docs validate --config docs.config.yaml \
--baseline .shoalrun/baseline.json --profile /run/config/docs-profile.yaml
Review and preview use the returned report. The snapshot captures the external profile and declared assets; later changes make the report stale. Treat reports and captured project directories as private build artifacts.
Choose navigation labels
Navigation headings come from the shared project's ordinary navigation configuration. For example:
navigation:
- id: product-guides
label: Product guides
items:
- { id: analytics, doc: guide.analytics }
- { id: connectors, doc: guide.connectors }
Product guides is authored text. You can name this group Your products, Reference, or another label that suits your readers. The framework does not supply the words “Your products.” Profile selection removes ineligible destinations from the group; it does not rename the group or infer purchases from navigation labels.
Check the result
Inspect the edition's sidebar, selected pages, and shared sections. Then inspect its Markdown, search indexes, documents.json, and exports for the same selections. An excluded feature's content should be absent from the output files, rather than hidden by browser controls.
A normal site build writes a private selection report at .shoalrun/profile-selection.json. It lists resolved features, grant sources, and inclusion decisions. Match its buildId to the output before treating it as a receipt for that output. The profile itself, its source filenames, and the report are not served as public metadata.
Affected pages excluded from the selected profile remain listed in validation coverage with a warning that their rendered content was not checked. Validate another profile that includes those pages before accepting their content changes.
HTML, Markdown, and agents
HTML displays the selected documentation with the deployment's title, logo, favicon, theme, and configured links. The masthead keeps the title as its accessible label. Markdown retains the selected instructions and links, without the site's branding controls. Feature selection happens during compilation and does not need browser JavaScript.
Agents reading search, discovery, or exported content receive the selected edition. They cannot retrieve excluded instructions from that edition's Markdown twin. Existing browser reader views still affect visibility only; use feature requirements for build-time omission.
Current limits
Profiled API builds reject full OpenAPI source publication. Set source.publish: false and source.focused: true for focused operation downloads. Generated operations can declare x-shoalrun-requires; API definitions and explicit bindings also accept requires. Generated references respect inherited frontmatter requirements.
Operation embeds respect the selection. Standalone <ApiSchema api="..." schema="..." /> embeds are not supported in profiled builds. Static vercel build --profile FILE is supported; profiled chat-function builds are not.
dev and checked-plan authoring do not yet accept deployment profiles. Use build/serve or captured validation/preview. Your deployment supplies the declared purchases and grants; Shoalrun does not query billing systems or expire entitlements. Protect private editions through host authentication.