Skip to content

Deploying to the Shared Docs Site

How to publish documentation from your repo to docs.saif.com. No per-repo Terraform needed — cloud-foundations owns the infrastructure.

File Structure

Add the following to your repo:

.azdo/
  azure-docs.yml        # deploy pipeline (manual trigger)
  azure-docs-pr.yml     # PR validation pipeline (no deploy)
  vars/
    docs.yml            # docs config variables
docs/
  requirements.txt      # Python dependencies for MkDocs
mkdocs.yml              # your MkDocs config

SAIF Brand Styling

All docs sites share a common SAIF-branded stylesheet hosted by cloud-foundations at docs.saif.com/assets/stylesheets/saif-docs-theme.css. Add it to your mkdocs.yml:

extra_css:
  - https://docs.saif.com/assets/stylesheets/saif-docs-theme.css

This provides SAIF Evergreen/Pacific branding with light and dark mode support. The stylesheet is maintained in cloud-foundations at docs-root/assets/stylesheets/saif-docs-theme.css, which is a separate MkDocs site (mkdocs.root.yml) that publishes to the $web container root so the URL above stays at the root of the domain. Pages render unstyled if offline during local mkdocs serve.

vars/docs.yml

variables:
  - name: projectId
    value: 'your-repo-name'
  - name: documentationSourceDirectory
    value: '../../'
  - name: outputDirectory
    value: 'staticsite'
  - name: siteType
    value: 'mkdocs'
  - name: containerFolderName
    value: 'your-folder-name'        # sets your URL path: docs.saif.com/{folder}/

azure-docs.yml

name: $(MajorVersion).$(MinorVersion).$(PatchVersion)

trigger: none
pr: none

pool:
  vmImage: 'ubuntu-latest'

variables:
  - group: TerraformCloud
  - template: templates/versioning.yml
  - template: vars/docs.yml
  - template: vars/vars.yml
  - template: saif-vars.yml@templates

resources:
  repositories:
    - repository: templates
      type: git
      name: SAIF/pipeline-templates
      ref: refs/heads/releases/v3

extends:
  template: azure-docs-v2.yml@templates
  parameters:
    projectId: ${{ variables.projectId }}
    documentationSourceDirectory: ${{ variables.documentationSourceDirectory }}
    outputDirectory: ${{ variables.outputDirectory }}
    siteType: ${{ variables.siteType }}
    containerFolderName: ${{ variables.containerFolderName }}
    # deployToPlatformDev: true   # uncomment to also deploy to platformdev

azure-docs-v2.yml is an extends template (it defines the pipeline's outer structure), not an includes template, so it belongs at the pipeline's own root under extends: — not nested under stages: - template:. That distinction matters beyond style: if this connection's service endpoint ever gets a CheckRequiredTemplate check (see Orchestrator Pipelines and Required Template Checks), only a root extends: satisfies it. A stages: - template: include of the same file compiles to the same pipeline but is invisible to that check. The check also matches on repository ref, not just template path — pin resources.repositories.templates.ref to the same refs/heads/releases/v3 the check's ciRequiredTemplates entry expects (see infra/bootstrap/Pulumi.bootstrap.yaml), not refs/heads/main.

azure-docs-pr.yml

name: $(MajorVersion).$(MinorVersion).$(PatchVersion)$(PreReleaseVersionSeparator)$(PreReleaseVersion)$(RevisionSeparator)$(Revision)

trigger: none

pr:
  branches:
    include:
      - main
  paths:
    include:
      - .azdo/azure-docs-pr.yml
      - .azdo/vars/docs.yml
      - docs/*
      - mkdocs.yml

pool:
  vmImage: 'ubuntu-latest'

variables:
  - group: TerraformCloud
  - template: templates/versioning.yml
  - template: vars/docs.yml
  - template: vars/vars.yml
  - template: saif-vars.yml@templates

resources:
  repositories:
    - repository: templates
      type: git
      name: SAIF/pipeline-templates
      ref: refs/heads/releases/v3

extends:
  template: azure-docs-pr-v2.yml@templates
  parameters:
    projectId: ${{ variables.projectId }}
    documentationSourceDirectory: ${{ variables.documentationSourceDirectory }}
    outputDirectory: ${{ variables.outputDirectory }}
    siteType: ${{ variables.siteType }}
    containerFolderName: ${{ variables.containerFolderName }}

Same root extends: shape as azure-docs.yml above, and the same refs/heads/releases/v3 pin — azure-docs-pr-v2.yml is allow-listed in the same ciRequiredTemplates entry set, so copying this example with stages: - template: or the main ref would fail the check the same way.

Then register both pipeline files in Azure DevOps. No additional service connections or permissions are required.

Frontmatter Contract for the Shared Search Index

Pages published to docs.saif.com are also indexed into the shared saif-docs Azure AI Search index. A few frontmatter conventions affect how your app's pages rank and de-duplicate in that index. See the search index reference for the full field contract.

Set a description in page frontmatter:

---
description: A short, page-specific summary for search.
---

MkDocs Material reads page.meta.description for the <meta name="description"> tag, falling back to config.site_description when a page has no frontmatter description. That fallback means every page in your app without its own description shares one boilerplate string in the index. The index's scoring profile weights description content, so a shared boilerplate description is a real ranking downside, not a cosmetic gap — set per-page descriptions rather than relying on the fallback.

Tags/keywords are opt-in, and are not a filterable tag list:

MkDocs Material does not emit <meta name="keywords"> out of the box (it only emits <meta name="description">). Until your app adds a theme override partial that emits <meta name="keywords" content="..."> from frontmatter tags, the index's tags field stays null for your pages — harmlessly. This is something you can opt into; the platform doesn't provide it by default yet.

If you do opt in, understand what you get. tags is a single string, not a list, so the whole comma-joined keyword string lands in one field. It contributes to relevance as free text, but it cannot be filtered per tag. Don't build a tag-filtering feature on it. See the search index reference for why it stays that way.

Publish under your containerFolderName subfolder, at least two levels deep:

Publish to $web/{containerFolderName}/..., not to the container root. The index derives its app and repo fields from the path segment immediately after $web, and its section field from the segment after that, so publishing at the root breaks both derivations. cloud-foundations follows the same rule: its own docs publish under cloud-foundations/, and only the landing page, the 404 and the shared assets/ folder sit at the container root.

Depth matters for section. A page at $web/{app}/guides/setup.html gets section = 'guides'. A page published directly at $web/{app}/index.html gets section = 'index.html', because there is no folder in that position. Pages at the container root break it outright — they have no segment in that position at all, and the indexer drops those documents rather than indexing them with an empty section.

Use section to let consumers filter out a class of your content:

section is filterable and facetable, so a consumer can exclude a whole category of pages with a request-level $filter:

section ne 'release-notes'

This is the supported way to keep low-value-for-search content (release notes, changelogs, generated API dumps) out of an agent's results. Group that content under its own folder so the filter has something to target. A filter works in every query mode, including agentic retrieval, where the index's scoring profile has no effect at all.

Expect both HTML and Markdown twins in search results:

If your app also publishes raw Markdown twins alongside HTML (some apps do this via a copy_markdown.py-style build step), both the .html and .md documents for the same page are indexed. Consumers that want exactly one document per page must apply the $filter:

format eq null or format eq '.html'

Do not use a bare format eq '.html' — documents the indexer hasn't yet reprocessed have format = null and would be wrongly excluded.

Troubleshooting

Build succeeds but content doesn't appear at docs.saif.com/{folder}/

  • Confirm containerFolderName in your vars file matches exactly what you expect in the URL
  • Check that the pipeline ran against the correct environment (the storage account is per-environment)
  • Verify the Front Door cache isn't serving stale content — it may take a few minutes to propagate

mkdocs build fails with a missing plugin or extension

  • Add the dependency to docs/requirements.txt in your repo — the build activity installs from this file automatically