diff --git a/.github/skills/doc-writer/SKILL.md b/.github/skills/doc-writer/SKILL.md index 74965854a..dd2c6a266 100644 --- a/.github/skills/doc-writer/SKILL.md +++ b/.github/skills/doc-writer/SKILL.md @@ -192,6 +192,12 @@ Python specific content here. If a heading needs to appear in the **On this page** table of contents, keep the heading outside the `Pivot` content and put only the variant-specific body content inside each `Pivot`. +#### On this page and "Overview" headings + +When a page shows the **On this page** table of contents (the default behavior unless `tableOfContents: false` is set), do **not** add an `Overview` heading at any level (`##`, `###`, etc.). The docs site already provides an implicit overview link to the top of the page, so an explicit `Overview` heading becomes redundant. + +If your opening section is truly introductory, keep it as body copy without an `Overview` heading. If that section has a more specific purpose, use a descriptive heading such as `Key concepts`, `Prerequisites`, or another topic-specific label. + For Aspire AppHost docs, use a single page-level `PivotSelector` with `key="aspire-lang"` when the surrounding section flow should switch as one unit. If a page would otherwise need multiple `aspire-lang` selectors, keep the page-level selector for the main flow and use synced `Tabs`/`TabItem` with `syncKey='aspire-lang'` for repeated language-specific examples later on the page. ```mdx diff --git a/src/frontend/src/content/docs/deployment/custom-deployments.mdx b/src/frontend/src/content/docs/deployment/custom-deployments.mdx index 179cff43e..f9d8ccad8 100644 --- a/src/frontend/src/content/docs/deployment/custom-deployments.mdx +++ b/src/frontend/src/content/docs/deployment/custom-deployments.mdx @@ -8,8 +8,6 @@ import LearnMore from '@components/LearnMore.astro'; Aspire provides powerful APIs for building container images from your resources during publishing and deployment operations. This article covers the key components that enable programmatic container image creation and progress reporting. -## Overview - During publishing and deployment, the container image builder is available to create images for resources that need them. Aspire uses this builder when a resource requires a container image, such as when publishing with Docker Compose. The process involves two main components: - `IResourceContainerImageBuilder`: The service that turns resource definitions into runnable container images. diff --git a/src/frontend/src/content/docs/deployment/pipelines.mdx b/src/frontend/src/content/docs/deployment/pipelines.mdx index 4c319d906..a160a5ba2 100644 --- a/src/frontend/src/content/docs/deployment/pipelines.mdx +++ b/src/frontend/src/content/docs/deployment/pipelines.mdx @@ -49,7 +49,7 @@ Aspire uses a pipeline-based deployment system that enables extensible, composab functionality may change in future releases. -## Pipeline model +## Pipeline deployment model The pipeline deployment system provides a flexible, concurrent model for deploying cloud applications. Pipelines break deployment into discrete, well-defined steps that can be optimized for performance and reliability while maintaining clear visibility into the deployment process. diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started.mdx index 3f8693ede..3e8428fde 100644 --- a/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started.mdx +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started.mdx @@ -26,7 +26,7 @@ In this introduction, you'll see how to install and use the Aspire Azure Contain To follow this guide, you must have created an Aspire solution to work with. To learn how to do that, see [Build your first Aspire app](/get-started/first-app/). -## Overview +## Key capabilities Aspire apps often build and run container images locally but require secure registries for staging and production environments. The Azure Container Registry integration provides the following capabilities: diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-host.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-host.mdx index 2db5cc38a..d27b6634a 100644 --- a/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-host.mdx +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-container-registry/azure-container-registry-host.mdx @@ -20,7 +20,7 @@ import containerRegistryIcon from '@assets/icons/azure-container-registry-icon.s [Azure Container Registry](https://learn.microsoft.com/azure/container-registry) is a managed Docker container registry service that simplifies the storage, management, and deployment of container images. The Aspire integration allows you to provision or reference an existing Azure Container Registry and seamlessly integrate it with your app's compute environments. -## Overview +## Key capabilities Aspire apps often build and run container images locally but require secure registries for staging and production environments. The Azure Container Registry integration provides the following capabilities: diff --git a/src/frontend/tests/unit/filetree-format.vitest.test.ts b/src/frontend/tests/unit/filetree-format.vitest.test.ts index 434b82506..7e8b2d920 100644 --- a/src/frontend/tests/unit/filetree-format.vitest.test.ts +++ b/src/frontend/tests/unit/filetree-format.vitest.test.ts @@ -7,6 +7,7 @@ const testsDir = path.dirname(fileURLToPath(import.meta.url)); const frontendRoot = path.resolve(testsDir, '..', '..'); const docsRoot = path.join(frontendRoot, 'src', 'content', 'docs'); const fileTreeBlockPattern = /]*>([\s\S]*?)<\/FileTree>/g; +const frontmatterPattern = /^\uFEFF?\s*---\r?\n([\s\S]*?)\r?\n---/; function collectDocs(dirPath: string): string[] { const entries = readdirSync(dirPath, { withFileTypes: true }); @@ -78,6 +79,56 @@ function findFileTreeIssues(source: string): Array<{ line: number; reason: strin return issues; } +function isTableOfContentsEnabled(source: string): boolean { + const frontmatterMatch = source.match(frontmatterPattern); + const frontmatter = frontmatterMatch?.[1] ?? ''; + + return !/^\s*tableOfContents\s*:\s*false\b/m.test(frontmatter); +} + +function findOverviewHeadingIssues(source: string): Array<{ line: number; reason: string }> { + if (!isTableOfContentsEnabled(source)) { + return []; + } + + const issues: Array<{ line: number; reason: string }> = []; + const lines = source.split(/\r?\n/); + let currentFence: { char: '`' | '~'; length: number } | null = null; + + for (const [index, line] of lines.entries()) { + const fenceMatch = line.match(/^\s*((`{3,})|(~{3,}))/); + if (fenceMatch) { + const fenceDelimiter = fenceMatch[1]; + const fenceChar = fenceDelimiter[0] as '`' | '~'; + const fenceLength = fenceDelimiter.length; + + if (!currentFence) { + currentFence = { char: fenceChar, length: fenceLength }; + continue; + } + + if (currentFence.char === fenceChar && fenceLength >= currentFence.length) { + currentFence = null; + continue; + } + } + + if (currentFence) { + continue; + } + + if (/^\s*##+\s+Overview\s*$/.test(line)) { + issues.push({ + line: index + 1, + reason: + 'TOC-enabled pages must not include an explicit "Overview" heading; use intro text or a more specific heading.', + }); + } + } + + return issues; +} + test('FileTree markdown lists remain newline-delimited in docs source', () => { const docPaths = collectDocs(docsRoot); const failures = docPaths.flatMap((docPath) => { @@ -91,3 +142,17 @@ test('FileTree markdown lists remain newline-delimited in docs source', () => { expect(failures, failures.join('\n')).toEqual([]); }); + +test('TOC-enabled docs do not include an explicit Overview heading', () => { + const docPaths = collectDocs(docsRoot); + const failures = docPaths.flatMap((docPath) => { + const source = readFileSync(docPath, 'utf8'); + const relativePath = path.relative(frontendRoot, docPath).replaceAll(path.sep, '/'); + + return findOverviewHeadingIssues(source).map( + ({ line, reason }) => `${relativePath}:${line} ${reason}` + ); + }); + + expect(failures, failures.join('\n')).toEqual([]); +});