# Customize Portal Content

:::note Beta Feature
The Enterprise Portal is Beta. Features described on this page are subject to change.
:::

This topic describes how to customize the content in the new Enterprise Portal, including the content template structure, table of contents configuration, MDX components, template variables, visibility rules, and downloadable assets.

For branding and theme customization, see [Customize Portal Branding](/vendor/enterprise-portal-v2-branding). To serve different content for different release versions, see [Manage Content Versions](/vendor/enterprise-portal-v2-versioned-docs).

## Content template structure {#content-template-structure}

The default content template ([replicatedhq/enterprise-portal-content](https://github.com/replicatedhq/enterprise-portal-content)) provides a working portal out of the box with the following structure:

```
your-content-repo/
├── pages/
│   ├── home.md
│   ├── installation/
│   │   ├── release-history.md
│   │   ├── linux.md
│   │   ├── kurl.md
│   │   ├── helm.md
│   │   └── kots.md
│   ├── updates/
│   │   └── instances.md
│   ├── security.md
│   ├── support/
│   │   ├── bundles.md
│   │   ├── faq.md
│   │   └── contact.md
│   └── automation/
│       ├── api-reference.md
│       └── workflow-guide.md
├── toc.yaml
└── theme.yaml
```

The template's `toc.yaml` organizes content into Installation, Instances & Updates, Security, Support, and Automation. Install method pages are entitlement-gated. Existing Cluster (KOTS) is gated on `isKotsInstallEnabled` only. Linux (kURL) is gated on `isKurlInstallEnabled` and `isAirgapSupported` because the default kURL page is still downloads-only for air gap.

```yaml
navigation:
  - title: Installation
    items:
      - title: Release History
        page: pages/installation/release-history.md
      - title: Linux (Embedded Cluster)
        page: pages/installation/linux.md
        visible_when:
          entitlements:
            - isEmbeddedClusterDownloadEnabled
      - title: Linux (kURL)
        page: pages/installation/kurl.md
        visible_when:
          entitlements:
            - isKurlInstallEnabled
            - isAirgapSupported
      - title: Existing Cluster (Helm)
        page: pages/installation/helm.md
        visible_when:
          entitlements:
            - isHelmInstallEnabled
      - title: Existing Cluster (KOTS)
        page: pages/installation/kots.md
        visible_when:
          entitlements:
            - isKotsInstallEnabled

  - title: Instances & Updates
    page: pages/updates/instances.md

  - title: Security
    page: pages/security.md
    visible_when:
      entitlements:
        - canViewSecurity

  - title: Support
    items:
      - title: Support Bundles
        page: pages/support/bundles.md
      - title: FAQ
        page: pages/support/faq.md
      - title: Contact Support
        page: pages/support/contact.md

  - title: Automation
    items:
      - title: API Reference
        page: pages/automation/api-reference.md
      - title: Workflow Guide
        page: pages/automation/workflow-guide.md

overrides:
  home: pages/home.md
```

The template pages use built-in MDX components (see [MDX Components](#mdx-components)) to render interactive installation instructions, version selectors, support bundle uploads, and more. You can customize any page by editing the markdown and MDX, or replace the entire structure with your own.

## Table of contents

The `toc.yaml` file at the root of your repo defines the sidebar navigation. Each navigation item has a `title` and one of the following content types:

| Key | What it does |
| :--- | :--- |
| `page` | Renders a markdown file from your repo (e.g. `pages/getting-started.md`) |
| `link` | Opens an external URL in a new tab instead of a page (see [External links](#external-links)) |
| `terraform_module` | Generates docs from a Terraform module source URI (see [Terraform Modules](/vendor/enterprise-portal-v2-terraform)) |
| `helm_chart` | Generates reference docs from a Helm chart in your promoted release (see [Helm Reference Docs](/vendor/enterprise-portal-v2-helm-reference)) |
| `items` | Nests child navigation items to create expandable sections. Items can nest to any depth, with each child following the same structure |

Every item also supports `icon` and `visible_when` (see [Visibility](#visibility) below).

The `toc.yaml` acts as an allowlist: pages that exist in your repo but are not listed in `toc.yaml` are not reachable through the portal navigation or direct URL. Omitting a page from `toc.yaml` hides it entirely. To keep the page in the repo and listed in the TOC without publishing it, set `hidden: true` in the page frontmatter. See [Hide unpublished pages](#hide-unpublished-pages).

:::note
The `overrides.home` page bypasses the TOC allowlist and remains accessible even if it is not listed in `toc.yaml`. To hide the home landing page, set `hidden: true` or `visible_when` in its frontmatter. `overrides.home` cannot restore a hidden page or a child of a hidden parent.
:::

Supported icon values: `rocket`, `download`, `cloud`, `settings`, `wrench`, `life-buoy`, `file-text`, `star`, `book`, `shield`, `package`, `refresh-cw`, `database`, `key`. If omitted, defaults to `book`.

### Complete example

Here's a `toc.yaml` showing all content types together:

```yaml
navigation:
  - title: Getting Started
    icon: rocket
    page: pages/getting-started.md

  - title: Installation
    icon: download
    items:
      - title: Requirements
        page: pages/installation/requirements.md
      - title: Helm Installation
        page: pages/installation/helm.md
        visible_when:
          entitlements:
            - isHelmInstallEnabled
      - title: Air Gap Installation
        page: pages/installation/airgap.md
        visible_when:
          entitlements:
            - isAirgapSupported

  - title: Infrastructure
    icon: database
    items:
      - title: AWS Module
        terraform_module: github.com/your-org/your-terraform//modules/aws?ref=v1.0.0
        visible_when:
          entitlements:
            - isAWSEnabled

  - title: Reference
    icon: book
    items:
      - title: My App Chart
        helm_chart:
          name: my-app

overrides:
  home: pages/getting-started.md
```

### Nested navigation

Navigation items can nest to any depth. Each child item follows the same structure and can contain its own `items` array:

```yaml
navigation:
  - title: Configuration
    icon: settings
    items:
      - title: Required Values
        page: pages/configuration/required-values.md
        items:
          - title: Authentication
            page: pages/configuration/auth.md
          - title: Networking
            page: pages/configuration/networking.md
      - title: Optional Values
        page: pages/configuration/optional-values.md
```

In this example, **Configuration** expands to show **Required Values** and **Optional Values**. **Required Values** further expands to show **Authentication** and **Networking**. Each level is collapsible in the sidebar.

Any item with `items` can also have its own `page`, `visible_when`, and `icon`. If all children of a parent are hidden by `visible_when`, the parent is also hidden automatically.

If a TOC node has a `page` with `hidden: true`, the portal drops that node and its subtree, including nested `helm_chart` and `terraform_module` items. A section that is only a title (no `page`) cannot use `hidden`. For more information, see [Hide unpublished pages](#hide-unpublished-pages).

### External links {#external-links}

Use `link` to add a navigation item that opens an external URL in a new tab instead of rendering a page from your content repo:

```yaml
- title: Support
  icon: life-buoy
  items:
    - title: Submit a Support Request
      link: https://support.example.com
    - title: Support Bundles
      page: pages/support/bundles.md
```

Supported `link` values:

- Fully-qualified `http://` or `https://` URLs
- `mailto:` links, including query parameters (for example, `mailto:support@example.com?subject=Help`)
- Bare external hostnames (for example, `support.example.com` or `www.google.com/search?q=replicated`), which the portal treats as `https://` automatically

A `link` value must be a static URL. Template variables (`\{\{ \}\}`) and other dynamic values are not supported, and validation rejects relative or internal-looking paths. If a navigation item's `link` value fails validation, content sync (and local preview) fails with an error that names the offending item.

If an item specifies both `link` and `page`, `link` takes priority.

## MDX components {#mdx-components}

Enterprise Portal content supports MDX, which is markdown with embedded React components. These components render interactive UI elements that adapt to each customer's license, entitlements, and instance state.

### Layout and callouts

**`<Note>`**: Informational callout box.

```markdown
<Note title="Before You Begin">
Run `kubectl get sc` to confirm a default StorageClass is available.
</Note>
```

**`<Tip>`**: Highlighted tip or best practice.

```markdown
<Tip title="New to this portal?">
Start with the Installation Guide for your deployment method.
</Tip>
```

**`<Warning>`**: Warning callout for important caveats.

```markdown
<Warning title="Back Up Before Updating">
Always create a backup before applying updates.
</Warning>
```

**`<Tabs>` / `<Tab>`**: Tabbed content sections.

```markdown
<Tabs>
<Tab title="Linux">
Linux-specific instructions here...
</Tab>
<Tab title="Helm">
Helm-specific instructions here...
</Tab>
</Tabs>
```

Props: `<Tabs>` accepts `defaultActiveTab`. `<Tab>` requires `title`.

**`<Accordion>`**: Collapsible content section.

```markdown
<Accordion title="Advanced Configuration" defaultOpen={false}>
Detailed configuration options...
</Accordion>
```

Props: `title` (required), `defaultOpen` (optional, defaults to false).

**`<OptionSelector>` / `<Option>`**: Persistent option picker that remembers the customer's selection across page loads.

```markdown
<OptionSelector label="Install Method" defaultOption="Linux" storageKey="install-method">
<Option value="Linux">
- [Linux Installation](/installation/linux)
- [Linux Support Bundles](/operations/bundles/linux)
</Option>
<Option value="Helm">
- [Helm Installation](/installation/helm)
- [Helm Support Bundles](/operations/bundles/helm)
</Option>
</OptionSelector>
```

Props: `label` (display label), `defaultOption` (pre-selected value), `storageKey` (key for persisting selection in browser storage).

### Display

**`<CodeBlock>`**: Syntax-highlighted code block.

```markdown
<CodeBlock language="yaml" title="values.yaml">
replicaCount: 3
image:
  repository: myapp
</CodeBlock>
```

Props: `language`, `title`.

**`<CommandBlock>`**: Styled terminal command with copy button.

```markdown
<CommandBlock>
kubectl get pods -A
</CommandBlock>
```

Props: `command`, `label`, `encoded`.

**`<ContentLink>`**: Internal navigation links. Standard markdown links (`[text](path)`) are automatically converted to use client-side navigation, so you don't need to use this component directly.

Link paths resolve relative to the current page's URL, and the portal injects the customer's selected version into the resolved route. Because resolution is relative, a bare `section/page` path resolves correctly only from a top-level page. From a page inside a subdirectory, the same path resolves against that subdirectory. For example, `[Configure values](installation/values)` authored on the `installation/helm` page resolves to `installation/installation/values`, which does not exist.

To link reliably from any page, use a leading-slash absolute path. Absolute paths resolve from the content root, and the portal still injects the selected version:

```markdown
See [Configure values](/installation/values).
```

From any page, this path resolves to `/<version>/installation/values`.

### Installation components

These components render dynamic, customer-specific installation instructions based on the customer's license entitlements, selected install method, and instance state.

**`<PendingInstallSelector>`**: Shows a list of in-progress installations scoped to a specific install method. Customers select an installation to populate the commands below with that installation's credentials.

```markdown
<PendingInstallSelector method="linux" />
```

Props: `method` (`"linux"`, `"helm"`, `"kots"`, or `"kurl"`, required), `network` (`"online"` or `"airgap"`, optional filter), `title` (optional, defaults to "Installations in progress"), `emptyText` (optional), `initialVisible` (optional, number of items to show before "Show all").

**`<NewInstall>`**: Button to start a new installation. Creates a service account and generates credentials, which populate the install commands on the page.

```markdown
<NewInstall method="helm" />
```

Props: `method` (`"linux"`, `"helm"`, `"kots"`, or `"kurl"`, required), `network` (`"online"` or `"airgap"`, optional), `label` (optional, defaults to "New installation").

If you omit `network`, the default depends on the page's network selection: `"airgap"` for air gap, or `"online"` otherwise.

:::note
The install commands on the page are personalized to the selected installation. If you switch installations or rename your instance, the commands update automatically.
:::

**`<NetworkAvailability>`**: Selector for online, proxy, or air gap installation mode. The air gap option is shown only when the customer's license supports air gap. Proxy selection also shows a Proxy URL field.

```markdown
<NetworkAvailability installType="linux" />
```

Props: `installType` (optional, `"linux"`, `"helm"`, `"kots"`, or `"kurl"`). Pass it so the air gap radio is gated on the correct entitlement. Without it, the air gap option is hidden.

**`<VersionSelector>`**: Dropdown for selecting which release version to install.

```markdown
<VersionSelector installType="helm" />
```

Props: `installType` (`"linux"`, `"helm"`, `"kots"`, or `"kurl"`).

**`<KubernetesDistribution />`**: Selector for the target Kubernetes distribution (EKS, GKE, AKS, etc.). No props.

**`<RegistryAccess />`**: Selects registry access settings and accepts a private registry hostname. Props: `installType` (`"linux"`, `"helm"`, `"kots"`, or `"kurl"`, optional, defaults to `"helm"`). For Helm, KOTS, and kURL, the component appears only when the page's network selection is air gap.

**`<LinuxInstallAssets />`**: Renders the full Linux/Embedded Cluster installation command sequence. Adapts to the customer's selected version and network availability. For air gap installations, includes an optional toggle for private registry image relocation. When enabled, the customer enters their registry hostname and the commands update to include image pull, tag, and push steps targeting that registry. Props: `stepNumber` (optional, starting step number).

**`<HelmInstallAssets />`**: Renders the full Helm installation command sequence. Props: `stepNumber` (optional), `charts` (optional, comma-separated list of chart names to include), `exclude` (optional, comma-separated list of chart names to hide).

When a customer selects limited or no registry access, `<HelmInstallAssets />` automatically includes browser download links for the Helm chart tarballs needed for their install. A download link for the preflight plugin binary is always included, regardless of registry access settings. No additional configuration is required.

**`<LinuxAirgapInstallAssets />`**: Linux air gap installation commands. Shows an unavailable notice if the air gap bundle is not yet built. Props: `stepNumber`.

**`<HelmAirgapInstallAssets />`**: Helm air gap installation commands. Props: `stepNumber`, `registryAvailability`, `charts` (optional), `exclude` (optional).

When a customer selects limited or no registry access, `<HelmAirgapInstallAssets />` automatically includes browser download links for Helm chart tarballs. These links are also shown in the update flow for customers in the same registry mode.

**`<KotsInstallAssets />`**: Renders the full online or proxy KOTS existing-cluster installation command sequence. Generated commands can include:

- `--license-file ./license.yaml`
- `--app-version-label`
- `--namespace` with the application slug when the slug is 63 characters or fewer
- On non-stable channels, the install positional `appSlug/channelSlug`
- When proxy is selected, `--http-proxy` / `--https-proxy` / `--no-proxy` on the install line, and `curl -x <proxy>` on the CLI download

`--shared-password` is omitted so KOTS prompts for the Admin Console password instead of printing a secret on the page.

Props: `stepNumber` (optional, starting step number).

**`<KurlInstallAssets />`**: Renders the full online or proxy kURL installation command sequence. When proxy is selected, the generated command uses `curl -x <proxy>` for the installer download and prefixes `HTTP_PROXY`/`HTTPS_PROXY` onto the `sudo bash`. Props: `stepNumber` (optional, starting step number).

**`<KotsAirgapInstallAssets />`**: Renders KOTS air gap download steps for the selected release. This component does not generate `push-images` or `kots install --kotsadm-registry` commands with customer registry credentials. The default KOTS page documents that air gap procedure in markdown instead. Props: `stepNumber`.

**`<KurlAirgapInstallAssets />`**: Renders kURL air gap installation steps. The first step includes artifact download links. The following step extracts the installer and runs `install.sh airgap`. Props: `stepNumber`.

**`<KotsInstallStep>`**: Renders one online KOTS install step by name. Props: `step` (required), `stepNumber` (optional). Use `step="install_kots"`.

**`<KurlInstallStep>`**: Renders one online kURL install step by name. Props: `step` (required), `stepNumber` (optional). Use `step="install_kurl"`.

**`<KotsAirgapInstallStep>`**: Renders one KOTS air gap download step by name. Props: `step` (required), `stepNumber` (optional). Use `step="download_assets"`. `step="download"` is an alias for `download_assets`.

**`<KurlAirgapInstallStep>`**: Renders one kURL air gap install step by name. Props: `step` (required), `stepNumber` (optional). Use `step="download_assets"` or `step="install_kurl"`.

For `<KotsInstallStep>`, `<KurlInstallStep>`, and `<KurlAirgapInstallStep>`, `step="install"` selects `install_kots` or `install_kurl`. `<KotsAirgapInstallStep>` only has `download_assets`, so `step="install"` has no target there.

**`<KotsDownloadAssets />`**: Renders a KOTS downloads section with a version picker for the selected release. Available when the customer's license has the KOTS install type. The KOTS CLI and optional Preflight and Support Bundle CLIs appear even when air gap is disabled. The Admin Console bundle and the application air gap bundle appear only when air gap is enabled, and only when `cliOnly` is not set. This component does not render install commands. Do not move the air gap mount out of `<WhenNetwork mode="airgap">` to put CLIs on the online branch. A customer who has air gap on the license but selected online would then see those bundles as required. Use a second mount with `cliOnly={true}` on the online/proxy branch instead (the default KOTS page does this). Props: `stepNumber`, `cliOnly` (optional).

**`<KurlDownloadAssets />`**: Renders a kURL downloads section with a version picker for the selected release. Available when the customer's license has the kURL install type. Optional KOTS, Preflight, and Support Bundle CLIs appear even when air gap is disabled. The kURL installer bundle (air gap tarball) and the application air gap bundle appear only when air gap is enabled, and only when `cliOnly` is not set. This component does not render install commands. Props: `stepNumber`, `cliOnly` (optional).

**`<WhenNetwork>`**: Shows or hides child content based on the customer's Network Availability selection. Use this to keep online generated commands and air gap documentation on the same page. Accepted `mode` tokens are `online`, `proxy`, and `airgap` (comma-separated). With no `<NetworkAvailability/>` on the page the selection stays `"online"`, so `mode="airgap"` content never appears. Unrecognized tokens such as `"Airgap"` or `"air-gap"` are ignored; in content preview the portal shows an authoring warning listing the accepted modes. Props: `mode` (required), `children`.

**`<InstanceName />`**: Prompts the customer to name their instance after installation. Renaming updates the service account name and refreshes the install commands on the page.

```markdown
<InstanceName />
```

Props: `title` (optional, defaults to "Name Your Instance"), `method` (optional, `"linux"`, `"helm"`, `"kots"`, or `"kurl"`).

**`<AdminConsoleUrl />`**: Displays the admin console URL for Embedded Cluster installations. No props.

**`<HelmReleaseImages />`**: Lists all container images included in a Helm release for the selected version. Includes a copy button for the full image list. Props: `title` (optional, defaults to "Helm images"), `emptyText` (optional, message when no images are found).

#### Filtering charts in install instructions

For applications with multiple Helm charts, you can control which charts appear in generated install and update instructions using the `charts` and `exclude` props. This lets you create separate install pages or sections for different charts, or hide optional addon charts from the default instructions.

```markdown
\{/* Only show instructions for specific charts */\}
<HelmInstallAssets charts="core-app, worker" />

\{/* Show all charts except optional addons */\}
<HelmInstallAssets exclude="optional-addon" />

\{/* Chart filtering also works on air gap and update components */\}
<HelmAirgapInstallAssets charts="core-app" />
<HelmUpdateAssets exclude="optional-addon" />
```

The `charts` and `exclude` props accept comma-separated chart names. Use `charts` to include only the listed charts, or `exclude` to show all charts except the listed ones.

**`<InstallStep>`**: Wraps content in a numbered step container.

```markdown
<InstallStep stepNumber={1} title="Download the installer">
Run the following command to download...
</InstallStep>
```

Props: `stepNumber`, `title`, `optional` (boolean).

**`<Prerequisites>`**: Styled prerequisites section.

```markdown
<Prerequisites title="Before You Begin">
- Kubernetes 1.26+
- Helm 3.x
</Prerequisites>
```

#### KOTS and kURL installer pages {#add-kots-or-kurl-installer-pages}

KOTS and kURL instances already appear on the Instances & Updates page with no setup. Embedded Cluster and Helm instances can expand into generated CLI upgrade steps. KOTS and kURL upgrades are performed in the Admin Console. The `<InstancesAndUpdates />` component in the default template renders this page.

The default content template also ships dedicated install pages, already listed in `toc.yaml`:

- Embedded Cluster: `pages/installation/linux.md`
- Helm: `pages/installation/helm.md`
- KOTS existing cluster: `pages/installation/kots.md`
- kURL: `pages/installation/kurl.md`

You do not need to author a KOTS page from scratch. Start from the template page and customize it.

##### What the default KOTS page contains

`pages/installation/kots.md` is gated on `isKotsInstallEnabled`. It includes:

- An installation selector (`<PendingInstallSelector>`, `<NewInstall>`, `<InstanceName>`)
- `<NetworkAvailability installType="kots" />` and `<VersionSelector installType="kots" />`
- `<LicenseDownload />` for every network mode (online and proxy generated `kubectl kots install` commands require `--license-file ./license.yaml`)
- Online and proxy installs: generated commands from `<KotsInstallAssets />`, then workstation CLIs from `<KotsDownloadAssets cliOnly={true} />`, wrapped in `<WhenNetwork mode="online,proxy">`
- Air gap installs: a documentation-style procedure wrapped in `<WhenNetwork mode="airgap">`, including:
  - Requirements for a private registry and **two** registry accounts (read-write for `push-images`, read-only for `kots install`). These are the customer's registry credentials, not portal or license credentials.
  - `<KotsDownloadAssets />`
  - Installing the downloaded KOTS CLI (not `curl https://kots.io/install`)
  - Example `push-images` and `kots install --kotsadm-registry` commands with documentation placeholders such as `REGISTRY_HOST`, `RW_USERNAME`, and `RO_USERNAME`
  - Admin Console steps to upload the license and `.airgap` bundle

The portal generates a command only when every value is something it knows (license, app slug, channel, version, artifact URL). It does not interpolate customer registry hostnames or passwords into generated KOTS air gap commands.

##### What to customize on the default KOTS page

Common vendor edits:

- **Branding and product naming**: Replace generic wording with your product name and support contact.
- **Admin Console URL**: The default air gap procedure uses the port-forward URL (`http://localhost:8800` or `kubectl kots admin-console`). If customers reach the Admin Console through an ingress or load balancer, replace that step with your URL.
- **Registry conventions**: Document your registry hostname or host/namespace pattern, account naming, and any required prep (for example, creating the registry namespace before push).
- **Content around `<KotsDownloadAssets />`**: Put your own steps above and below the download component. Pass `stepNumber` when you want the component to own the numbered step chrome, or wrap it in `<InstallStep>` and omit `stepNumber` so your surrounding copy owns the numbering. Do not do both.
- **Online CLI downloads**: The default page already mounts `<KotsDownloadAssets cliOnly={true} />` on the online/proxy branch. Lead that copy with Preflight and Support Bundle (not curl-dependent). Then cover the fallback. If the customer cannot run `curl https://kots.io/install`, they download the KOTS CLI, extract it, rename `kots` to `kubectl-kots`, put it on PATH, and run the `kubectl kots install` line from step 2. Do not enable air gap on the license only to get the KOTS CLI. Do not move the air gap `<KotsDownloadAssets />` out of `<WhenNetwork mode="airgap">`. The default KOTS air gap procedure is unchanged.

```markdown
<InstallStep stepNumber={1} title="Your network and registry rules">
Harbor project, jump host, support contact, …
</InstallStep>

<InstallStep stepNumber={2} title="Download the installation assets">
<KotsDownloadAssets />
</InstallStep>

<InstallStep stepNumber={3} title="Push Admin Console images">
Your REGISTRY_HOST convention, then the docs-style command.
</InstallStep>
```

##### kURL installer page

The default `pages/installation/kurl.md` page provides downloads for entitled air gap customers. Its frontmatter and `toc.yaml` entry are gated on both `isKurlInstallEnabled` and `isAirgapSupported`. For generated online or air gap install commands, add `<NetworkAvailability installType="kurl" />`, `<VersionSelector installType="kurl" />`, and `<KurlInstallAssets />` (which adapts when Network Availability changes) or a separate `<KurlAirgapInstallAssets />` branch, the same way Helm and Embedded Cluster pages use generated install assets. If you ship generated online kURL commands to online-only customers, drop `isAirgapSupported` from both the page frontmatter `visible_when` and the `toc.yaml` entry. The default kURL page is unchanged. If you add `<KurlDownloadAssets />` to a page those customers can see, pass `cliOnly={true}` so they get the optional CLIs without the kURL installer tarball or application air gap bundle.

### Update components

**`<LinuxUpdateAssets />`**: Renders Linux/Embedded Cluster upgrade instructions for the customer's current instance. For air gap upgrades, includes a registry hostname input for image relocation when the customer uses a private registry. Props: `stepNumber`.

**`<HelmUpdateAssets />`**: Renders Helm upgrade instructions. Props: `stepNumber`, `charts` (optional), `exclude` (optional).

**`<KotsUpdateAssets />`**: Renders a single line directing the customer to upgrade this instance in the Admin Console. It generates no CLI commands and no download links, and it ignores `stepNumber`.

**`<KurlUpdateAssets />`**: Renders a single line directing the customer to upgrade this instance in the Admin Console. It generates no CLI commands and no download links, and it ignores `stepNumber`.

**`<UpgradePath>`**: Conditionally shows content based on the customer's current version. Lets you show different upgrade instructions depending on which version the customer is upgrading *from*.

**Props:**

| Prop | Description |
| :--- | :--- |
| `fromBelow` | Show content only when the customer's current version is *below* this version |
| `fromAbove` | Show content only when the customer's current version is *above* this version |
| `fromBelowOrEqual` | Show content only when the current version is at or below this version |
| `fromAtLeast` | Show content only when the current version is at least this version |

All version values must be strict `major.minor.patch` format (e.g. `2.0.0`). A `v` prefix is stripped automatically. Pre-release suffixes (e.g. `2.0.0-rc.1`) are not supported.

Props can be combined to define a version range (AND logic). If the customer has no selected instance or the version can't be parsed, the content is hidden.

**Example:**

````markdown
<UpgradePath fromBelow="2.0.0">
## Upgrading from 1.x

Before upgrading to 2.x, you must migrate your database schema. Run:

```shell
./migrate-schema.sh
```
</UpgradePath>

<UpgradePath fromAtLeast="2.0.0">
## Upgrading from 2.x

No manual migration needed. The upgrade is automatic.
</UpgradePath>
````

In this example, a customer on version 1.5.0 would see the "Upgrading from 1.x" instructions, while a customer on 2.1.0 would see the "Upgrading from 2.x" block.

**`<MarkUpdateComplete />`**: Button for the customer to confirm an upgrade is complete. No props.

**`<ReleaseNotes />`**: Displays release notes for the target update version. No props.

### Portal components

**`<ReleaseHistory />`**: Renders a browsable release history timeline with version details and release notes. For air gap releases, each entry shows a build status badge (Ready, Building, Failed, or Not built). Props: `limit` (optional, max number of releases to show).

**`<SupportBundleUpload />`**: Upload interface for support bundles. No props.

**`<SupportBundleUploadHistory />`**: Lists previously uploaded support bundles. Props: `limit`.

**`<LinuxBundles />`**: Linux-specific support bundle generation instructions. No props.

**`<HelmBundles />`**: Helm-specific support bundle generation instructions. A download link for the support-bundle plugin binary is always included, regardless of registry access settings. No props.

**`<KurlBundles />`**: kURL-specific support bundle generation instructions. No props.

**`<KotsBundles />`**: KOTS-specific support bundle generation instructions for an existing cluster. No props.

Each of the four support bundle components renders the steps for one installation method and nothing else: no heading and no tab strip of its own. The page is what decides which methods a customer sees. The default template's Support Bundles page wraps all four in a single [`<Tabs>`](#layout-and-callouts) block, with each tab guarded by the [entitlement](#entitlements) for that installation method.

**`<ContactInfo />`**: Displays the support contact information configured in `theme.yaml`. No props.

**`<LicenseDownload />`**: Renders a button that lets customers download their license file. The button appears only for customers whose license enables Embedded Cluster, KOTS, or kURL installations, which are the installation methods that use a license file. Helm customers do not see it. No props.

**`<InstancesAndUpdates />`**: Renders the full instances and updates management page inline. Helm and Embedded Cluster instances can expand into inline upgrade CLI commands. KOTS and kURL upgrades are performed in the Admin Console. No props.

### Security components

These components require Security Center to be enabled for the customer and are available for Helm and Embedded Cluster installs only. See [Security Center](/vendor/security-center-about) for more information.

**`<CVEReport />`**: Displays CVEs by severity, component, and container image for the selected release. For each CVE, the **Fixed In** column identifies the earliest vendor release that resolves it, when one is available. The report also compares CVE counts with the latest release. Depending on your Security Center settings, the report shows only fixable CVEs or includes a toggle for customers to show all CVEs. When **Enable raw CVE scan to be downloadable** is enabled, the report also displays **Download full CVE report**, which downloads the raw Grype scan JSON for the selected release. This download is distinct from the SPDX SBOM available through `<SBOMReport />`. No props.

**`<SBOMReport />`**: Displays SBOM metadata and a download option for the selected release. If no SBOM is available, the component displays an unavailable state. No props.

**`<SecurityVersionSelector />`**: Selects the install type and release used by the security reports. For older releases, the component shows active instances and links to their upgrade options. When customers select the latest release, the component identifies active instances on older releases. No props.

## Template variables

Enterprise Portal content supports Mustache-style templating with `\{\{ \}\}` syntax for dynamic rendering. Template variables and MDX components can be used together in the same page.

### Available variables

| Variable | Description |
| :--- | :--- |
| `\{\{ app.name \}\}` | Your application name |
| `\{\{ app.slug \}\}` | URL-friendly app identifier |
| `\{\{ customer.name \}\}` | Customer's name |
| `\{\{ customer.email \}\}` | Customer's email |
| `\{\{ customer.id \}\}` | Customer ID |
| `\{\{ channel.name \}\}` | Release channel (Stable, Beta, etc.) |
| `\{\{ channel.slug \}\}` | URL-friendly channel identifier |
| `\{\{ release.version \}\}` | Current release version |
| `\{\{ license.* \}\}` | Any field from the license object (e.g. `\{\{ license.id \}\}`, `\{\{ license.expiresAt \}\}`) |
| `\{\{ entitlements.* \}\}` | Any entitlement value, built-in or custom (see below) |

:::note
`\{\{ release.version \}\}` depends on the customer having reported an active instance. It may be empty for new customers who haven't installed yet. Consider using `\{\{ channel.name \}\}` as a more reliable alternative for version-dependent content.
:::

### Conditionals

**`\{\{#if\}\}`**: Show content when a value is truthy. Supports nesting.

```text
{{#if entitlements.isHAEnabled}}
## High Availability Setup
For production deployments, enable HA mode...
{{/if}}
```

Values are considered falsy if they are null, empty string, `false`, `"false"`, `"0"`, or `0`. Everything else is truthy.

**`\{\{#ifEquals\}\}`**: Show content when a value equals a specific string. Useful for per-customer or per-channel content.

```text
{{#ifEquals channel.name "Stable"}}
You are on the Stable channel. Updates are less frequent but thoroughly tested.
{{/ifEquals}}

{{#ifEquals customer.name "Acme Corp"}}
## Acme-Specific Configuration
Your dedicated endpoint is acme.example.com
{{/ifEquals}}
```

The comparison value must be quoted (double or single quotes). Supports nesting.

### Entitlements

Entitlements are accessible as `\{\{ entitlements.<name> \}\}` for interpolation and `\{\{#if entitlements.<name>\}\}` for conditionals. There are two kinds:

- **Built-in flags**: Set on the license through the Vendor Portal: `isHelmInstallEnabled`, `isAirgapSupported`, `isEmbeddedClusterDownloadEnabled`, `isEmbeddedClusterMultiNodeEnabled`, `isKotsInstallEnabled`, `isKurlInstallEnabled`, `isHelmAirgapEnabled`
- **Custom entitlements**: Any that you define (e.g. `isHAEnabled`, `isEnterpriseEnabled`)

Example:

```text
{{#if entitlements.isHelmInstallEnabled}}
## Helm Installation
Follow these steps to install using Helm...
{{/if}}

{{#if entitlements.isHAEnabled}}
## High Availability Setup
For production deployments, enable HA mode...
{{/if}}
```

## Visibility {#visibility}

Use `visible_when` to control which customers see which content. All conditions must be satisfied (AND logic). To keep a page unpublished for every customer, set `hidden: true` in the page frontmatter. `hidden` is not a license or entitlement gate. See [Hide unpublished pages](#hide-unpublished-pages).

### Navigation visibility (toc.yaml)

Apply `visible_when` on any navigation item in `toc.yaml`:

```yaml
- title: Enterprise Features
  page: pages/enterprise.md
  visible_when:
    entitlements:
      - isEnterpriseEnabled
    channel:
      include: [Stable, Beta]
      exclude: [Internal]
    customer_type:
      include: [paid]
```

All three filter types (`entitlements`, `channel`, and `customer_type`) are fully evaluated server-side. Unrecognized filter dimensions are rejected (fail closed).

To hide a page entirely, you can also omit it from `toc.yaml`. Pages not listed in the TOC are not reachable through navigation or direct URL (see [Table of contents](#table-of-contents) above). To keep the page listed in `toc.yaml` but unpublished, use `hidden: true` in the page frontmatter instead.

#### Per-user permissions in visible_when

In addition to license entitlements, you can gate content on per-user permissions by referencing them in the `entitlements` list:

```yaml
- title: Security Reports
  page: pages/security.md
  visible_when:
    entitlements:
      - canViewSecurity
```

The following permissions are available:

| Permission | Description |
| :--- | :--- |
| `canViewSecurity` | Can view Security Center (CVE reports, SBOMs). Resolves based on the customer's Security Center entitlement. |
| `canInviteUser` | Can invite users to the portal team |
| `canRemoveUser` | Can remove users from the portal team |
| `canManageServiceAccts` | Can create or revoke service accounts |
| `canUploadBundles` | Can upload support bundles |

Permissions are folded into the `entitlements` namespace, so they work in both `visible_when` conditions and `\{\{ entitlements.canViewSecurity \}\}` template expressions. Currently, `canViewSecurity` is the most useful permission for content gating because it resolves based on whether Security Center is enabled for the customer. The other four permissions resolve to `true` for all portal users.

#### Custom license fields in visible_when

In addition to built-in entitlements, you can use your own custom license fields in `visible_when` conditions. Any custom license field you create in the Vendor Portal is automatically available in the `entitlements` list. This lets you gate content on a per-customer basis using fields you define.

For example, if you create a boolean custom license field called `hasAdvancedReporting`:

```yaml
- title: Advanced Reporting
  page: pages/advanced-reporting.md
  visible_when:
    entitlements:
      - hasAdvancedReporting
```

Only customers whose license has `hasAdvancedReporting` set to `true` will see this page. Custom license fields also work in template expressions (`\{\{ entitlements.hasAdvancedReporting \}\}`).

For information about creating custom license fields, see [Manage custom license fields](/vendor/licenses-adding-custom-fields#manage-custom-license-fields).

#### License type visibility (customer_type)

Use `customer_type` to show or hide content based on the customer's license type. Valid values are `community`, `trial`, and `paid`.

This is useful when you have different content needs for different tiers. For example, you might show upgrade prompts to trial users, or restrict advanced documentation to paid customers only.

```yaml
# Show only to paid customers
- title: Production Deployment Guide
  page: pages/production-deploy.md
  visible_when:
    customer_type:
      include: [paid]

# Show to trial and community customers
- title: Upgrade to Paid
  page: pages/upgrade.md
  visible_when:
    customer_type:
      include: [trial, community]
```

Like `channel`, `customer_type` supports both `include` and `exclude` lists.

### Page-level visibility (frontmatter)

You can also apply `visible_when` or `hidden: true` in a page's YAML frontmatter. `visible_when` adds a per-page access gate on top of the navigation rules in `toc.yaml`. `hidden: true` keeps the page unpublished for every customer.

```markdown
---
title: Embedded Cluster Installation Requirements
visible_when:
  entitlements:
    - isEmbeddedClusterDownloadEnabled
---

# Embedded Cluster Installation Requirements

Ensure your environment meets these requirements...
```

The same `entitlements` conditions available in `toc.yaml` work in frontmatter, including built-in entitlements, custom license fields, and per-user permissions. Use this when you want to keep the gating condition on the page itself rather than on its `toc.yaml` navigation entry.

Frontmatter `visible_when` is an additional gate, not a replacement for navigation. A page must still be reachable through the navigation (or declared as an `overrides` target) for a customer to access it directly. A page that is not referenced anywhere in `toc.yaml` is not accessible, regardless of its frontmatter.

#### Hide unpublished pages {#hide-unpublished-pages}

`hidden` controls whether a page is published, not who can see it. Use `visible_when` to show a page to some customers and not others based on their license. Use `hidden: true` to keep a page out of every customer's portal until you are ready to publish it, regardless of license.

Set `hidden: true` in a page's YAML frontmatter when the page belongs in your content repo but is not ready for customers yet. For example, stage a product announcement, a new install guide, or a revised section next to your live docs, then publish it by removing the flag when you are ready. Keep the page listed in `toc.yaml` while it is hidden so the navigation slot is already wired when you publish, rather than leaving the draft in a stray branch or an unwired file you have to remember later. Local preview hides the page the same as production, so it does not appear in the preview sidebar while `hidden` is set.

Put `hidden` on the markdown page, not on `toc.yaml`.

```markdown
---
title: Q3 announcement
hidden: true
---
```

Keep the page listed in `toc.yaml` as a normal page entry:

```yaml
- title: Q3 announcement
  page: pages/announcements/q3.md
```

Customers do not see the page until you remove `hidden` or set it to `false`, then re-sync the content.

`hidden: true` marks unpublished content. It is not a license or entitlement gate. `visible_when` for built-in entitlements, custom license fields, and permissions is unchanged. `hidden: true` takes precedence over `visible_when`. Even if the customer would pass `visible_when`, the page stays unpublished. Entitlements cannot unhide it.

If a TOC node has a `page` with `hidden: true`, the portal drops that node and its subtree. The subtree includes child pages, nested `helm_chart` items, and `terraform_module` items. This is the same ancestor-gating behavior as `visible_when` on a parent.

A section that is only a title (no `page`) cannot use `hidden`. Set `hidden` on each child page, or use `visible_when` on the TOC node.

`overrides.home` cannot restore a hidden page or a child of a hidden parent. To hide the home landing page, set `hidden: true` on that page.

Omitting a page from `toc.yaml` still hides it entirely. Use `hidden` when you want to keep the page in the TOC and publish it later.

:::note
If you sync `hidden` against an Enterprise Portal binary that does not support it, the portal drops unknown YAML keys. After a binary that supports `hidden` is deployed, re-sync so the flag is stored.
:::

## Downloadable assets

You can distribute files (scripts, checklists, configuration templates, etc.) through Enterprise Portal by committing them to an `assets/` directory in your content repo.

### Repo structure

```
your-content-repo/
├── assets/
│   ├── deploy.sh
│   └── onboarding-checklist.pdf
├── pages/
│   └── resources.md
└── toc.yaml
```

Any file committed to the `assets/` directory is available for download. There are no restrictions on file type or size beyond what your git hosting provider allows (GitHub supports files up to ~100MB via raw content). Assets are fetched directly from your git repo when a customer clicks a download link, so changes are available as soon as you push.

### Linking to assets

Use the `\{\{asset "path"\}\}` template tag in your markdown to generate a download link:

```text
## Resources
- [Deployment Script]({{asset "assets/deploy.sh"}})
- [Onboarding Checklist]({{asset "assets/onboarding-checklist.pdf"}})
```

The tag expands to a URL that authenticates the customer via their session and serves the file directly. Assets are version-aware, so each content branch can have its own set of files.

The path inside the tag is relative to the repo root and must start with `assets/`.

:::note
Any asset referenced by an `\{\{asset\}\}` tag on a page that a customer can see is downloadable by that customer. Asset access is controlled by the visibility of the page that references them, not by the file path or directory structure. A page with `hidden: true` does not authorize asset downloads. If you need to restrict an asset to specific customers, use template conditionals (like `\{\{#ifEquals customer.name\}\}`) around the asset reference, or place the asset reference on a page gated with `visible_when`. See the example below.
:::

### Per-customer asset delivery

To deliver different assets to different customers, combine `\{\{asset\}\}` with template conditionals. The conditional controls which customers see the download link, and only customers who can see the link can download the file:

```text
## Resources

{{#ifEquals customer.name "Acme Corp"}}
- [Acme Deployment Guide]({{asset "assets/acme-deploy-guide.pdf"}})
- [Acme ArgoCD Values]({{asset "assets/acme-argocd-values.yaml"}})
{{/ifEquals}}

{{#ifEquals customer.name "Globex"}}
- [Globex CloudFormation Template]({{asset "assets/globex-cloudformation.yaml"}})
{{/ifEquals}}

{{#if entitlements.isAWSEnabled}}
- [AWS Deployment Script]({{asset "assets/deploy-aws.sh"}})
{{/if}}

- [Onboarding Checklist]({{asset "assets/onboarding-checklist.pdf"}})
```

In this example, only Acme Corp can download their deployment guide, and only customers with the `isAWSEnabled` entitlement can download the AWS script. The onboarding checklist is available to all customers because it is not wrapped in a conditional.

This lets you maintain one content repo with assets for all customers. Each customer only sees the downloads that apply to them. Unconditioned assets (like the onboarding checklist) appear for everyone.

This pattern works for any file type: Terraform modules, Argo manifests, Bicep templates, CloudFormation stacks, bash scripts, PDFs, or configuration templates.