| name | feature-table-audit |
| description | Audit and fix the vCluster docs feature table that the FeatureTable component renders from src/data/features.yaml. Covers three facets - sidebar tier badges (FREE/ENTERPRISE) versus plan assignments in the plans repo, feature docs_url link targets, and which FeatureTable rows each page imports. Use when verifying badges, checking that feature links resolve to the right page, or confirming a page surfaces the correct rows. |
| context | fork |
Feature Table Audit
The feature comparison table (rendered by the FeatureTable component from src/data/features.yaml) drives three things in the docs, each its own audit facet:
- Sidebar tier badges (FREE / ENTERPRISE pills) that must match plan assignments in
~/git/vcluster/plans/.
- Feature
docs_url link targets that each row links to.
<FeatureTable names="..."> row imports that decide which rows a given page surfaces.
This skill covers all three. Labels and the plans repo come first, then the link-target and row-import audits.
The feature table source (src/data/features.yaml)
src/data/features.yaml is the single source the FeatureTable component reads. Each entry is keyed by a feature ID and carries the row label, description, category, optional pricing, and docs_url:
ha-mode:
name: "High-Availability Mode"
description: "..."
category: "Deployment Modes"
docs_url: "/docs/platform/maintenance/reliability-scaling/high-availability"
pricing: "add-on"
Feature keys are synced from loft-sh/admin-apis, and tiers from loft-sh/plans (see the header comment in the file), so do not hand-edit keys. The docs_url value is the docs-owned lever you audit, and it drives both the link target (facet 2) and the canonical FeatureTable placement (facet 3).
Critical: docs_url is the source of truth for where a feature's <FeatureTable> lives. scripts/sync-feature-tables.js maps each feature's docs_url (anchor stripped) to a file and inserts/updates the <FeatureTable> on that page. npm run validate-feature-tables runs it with --dry-run and the validate-feature-tables CI check fails if anything is out of sync. So after editing any docs_url, run npm run sync-feature-tables and commit the regenerated pages, or CI will fail.
Sidebar tier badges
Sidebar badges in the docs are driven by CSS classes set via frontmatter or category config. They must match the feature assignments in ~/git/vcluster/plans/.
How the Labels Work
Setting a label
On an .mdx page (frontmatter):
sidebar_class_name: pro
sidebar_class_name: free
sidebar_class_name: pro host-nodes
On a category (_category_.json):
{ "className": "pro" }
{ "className": "free" }
The category className labels the collapsible section header only — it does not propagate to pages inside the category. Individual pages need their own sidebar_class_name.
What the badges render
Defined in src/css/sidebar.scss:
.pro → orange "ENTERPRISE" pill
.free → dark "FREE" pill
The Plans Repo
Plans live at ~/git/vcluster/plans/:
| File | Tier |
|---|
free.yaml | Free |
dev.yaml | Dev (superset of free) |
prod.yaml | Prod (superset of dev) |
scale.yaml | Scale/Enterprise (superset of prod) |
internal.yaml | Internal (superset of all) |
Rule: A feature in free.yaml → label free. A feature absent from free.yaml but present in any paid plan → label pro.
Finding the Feature Key
The feature key is exposed on the page via the FeatureTable component:
<FeatureTable names="vault-integration" />
<FeatureTable names="ha-mode,regional-cluster-endpoints" />
The names attribute is the feature key to look up in the plans YAML files.
Pages with <ProAdmonition/> but no FeatureTable require checking the code (see below).
Audit Workflow
1. Find all labeled pages
grep -rn "sidebar_class_name.*pro\|sidebar_class_name.*free" platform vcluster --include="*.mdx"
grep -rn "\"className\"" platform vcluster --include="_category_.json"
2. Find pages with feature references but no label (missing labels)
grep -rln "FeatureTable\|ProAdmonition" platform vcluster --include="*.mdx" \
| grep -v "_partials\|_fragments\|versioned_docs" \
| while read f; do
grep -q "sidebar_class_name" "$f" || echo "UNLABELED: $f"
done
3. Extract feature keys from unlabeled pages
grep -o 'names="[^"]*"' <file.mdx>
4. Check the plan tier
grep "<feature-key>" ~/git/vcluster/plans/free.yaml
grep "<feature-key>" ~/git/vcluster/plans/dev.yaml
If the feature key appears in free.yaml → free. If absent from free.yaml → pro.
5. Apply the label
Add sidebar_class_name: pro (or free) as the last field before the closing --- in the frontmatter.
Verifying ProAdmonition Usage
<ProAdmonition/> shows an "Enterprise-Only Feature" callout inline. It should only appear when the surrounding content is genuinely enterprise-gated.
Common issues:
- Imported but never used — dead import, just remove the
import line.
- Used in a sub-section of a free feature — remove the admonition from that section.
- Used for a feature with no plan key — check the vCluster OSS/Pro Go code for a license gate. Search in
~/git/vcluster/vcluster-pro/ for the feature name near license.GetLicense() or currentLicense.Enabled(...).
Skip These
- Air-gapped install pages inside a
pro-labeled category — the category header carries the badge; individual pages don't need one.
- API reference pages — no label needed.
- Overview/index pages that span multiple tiers — no label needed.
vcluster/introduction/oss-vs-free.mdx — intro page.
What to Check in Code
When a ProAdmonition has no matching FeatureTable, verify the gate in the Go code:
grep -r "migrate\|<feature-name>" ~/git/vcluster/vcluster-pro/pkg/ | grep -i "license\|Enabled\|feature"
grep -r "NewFeatureError" ~/git/vcluster/vcluster/pkg/pro/
If the implementation lives only in vcluster-pro/ with a license check → the ProAdmonition is correct and the page needs sidebar_class_name: pro.
Audit: docs_url link targets
Every docs_url in features.yaml should resolve to a real page whose topic matches the feature.
URL to file mapping (site baseUrl is /docs):
/docs/vcluster/PATH maps to vcluster/PATH.mdx or vcluster/PATH/README.mdx
/docs/platform/PATH maps to platform/PATH.mdx or platform/PATH/README.mdx
Three gotchas that make a link look fine but break:
slug: overrides. A page can set slug: in frontmatter, so the file path is not the served URL. If the literal path file is missing, grep for a slug: that resolves to the URL before calling the link broken (this is how air-gapped and standalone resolve).
- Anchors.
#foo must match a heading (slugified: lowercase, spaces to hyphens) or an explicit {#id}. A valid page with a dead anchor still loads, it just lands at the top.
- Moved pages. A page relocating under a new parent (for example into
configure/platform-configs/) silently breaks the old docs_url; nothing redirects.
Existence, slug, and anchor checks:
ls vcluster/PATH.mdx platform/PATH.mdx 2>/dev/null
grep -rl "^slug:.*PATH" platform vcluster --include="*.mdx"
grep -n "{#anchor-id}\|^#\+ Heading Text" path/to/page.mdx
Orphaned partials. Some config reference partials (for example _partials/config/experimental/genericSync.mdx) carry valid {#id} anchors but are imported into no page, so a docs_url pointing at that anchor never resolves. Confirm something imports the partial:
grep -rln "experimental/genericSync" platform vcluster --include="*.mdx" | grep -v versioned
If nothing imports it, the fix is to create the page (mirroring a sibling page that imports its partial), not to tweak the link.
Audit: <FeatureTable names="..."> row imports
A page's <FeatureTable> renders the row(s) named in names. There are two kinds of placement, and they are governed differently:
Canonical placements are generated, not hand-audited. For every feature with a docs_url, sync-feature-tables.js ensures the page that docs_url resolves to contains a <FeatureTable> listing that feature (multiple features sharing a docs_url are merged into one names="a,b" table). You do not place these by hand. To move or fix a canonical table, change the feature's docs_url and run npm run sync-feature-tables. To preview, run npm run validate-feature-tables (the same --dry-run the CI runs); it prints [INSERT] / [UPDATE] for anything stale and exits non-zero.
The script only manages pages that are some feature's docs_url target. It does not remove <FeatureTable> from other pages, and resolves docs_url only by literal file path; targets that work via slug: or a directory README.mdx (e.g. standalone → binary/, argo-integration, air-gapped-mode) log [WARN] No file found and are skipped, not failed. External URLs (vnode) are skipped too.
Extra placements are the manual part. A page may also surface a row it merely discusses, even though it is not that feature's docs_url target (e.g. the vcluster sleep page showing vcp-distro-sleep-mode). The generator leaves these alone and CI does not check them, so they need human judgment:
grep -rn "<FeatureTable" platform vcluster --include="*.mdx" | grep -v versioned
(names="all" on oss-vs-free.mdx and what-are-virtual-clusters.mdx is intentional and renders the whole table.)
For each, check the named row against the page topic. The classic bug is a wrong row — the HA page once imported platform-external-db instead of ha-mode, and a deletion page imported the Auto Sleep row. Fix by correcting names or dropping the table.
The invariant: a page may surface any row it genuinely discusses, but every row must have one sensible canonical docs_url, and the canonical table placement follows from that docs_url automatically.