From b029a2369a01a11cc1a8f33cd0c1e901bd9a9985 Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 11:54:28 -0500 Subject: [PATCH 1/9] add logic --- app/api/source-map/route.ts | 24 +++++++++++++ scripts/lint-404s/README.md | 65 ++++++++++++++++++++++++++++++++++ scripts/lint-404s/main.ts | 70 ++++++++++++++++++++++++++++++++----- 3 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 app/api/source-map/route.ts create mode 100644 scripts/lint-404s/README.md diff --git a/app/api/source-map/route.ts b/app/api/source-map/route.ts new file mode 100644 index 0000000000000..2f4eb2e3af57c --- /dev/null +++ b/app/api/source-map/route.ts @@ -0,0 +1,24 @@ +import {NextResponse} from 'next/server'; + +import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; +import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/mdx'; + +/** + * API endpoint that returns a mapping of slugs to their source file paths. + * This is used by the 404 link checker to deduplicate pages that share the same source. + */ +export async function GET() { + const docs = await (isDeveloperDocs ? getDevDocsFrontMatter() : getDocsFrontMatter()); + + const sourceMap: Record = {}; + + for (const doc of docs) { + // Normalize slug (remove trailing slash if present) + const slug = doc.slug.replace(/\/$/, ''); + // sourcePath will be null for API-generated pages, which we want to keep + sourceMap[slug] = doc.sourcePath ?? null; + } + + return NextResponse.json(sourceMap); +} + diff --git a/scripts/lint-404s/README.md b/scripts/lint-404s/README.md new file mode 100644 index 0000000000000..6db24d2b8955d --- /dev/null +++ b/scripts/lint-404s/README.md @@ -0,0 +1,65 @@ +# 404 Link Checker + +This script checks all documentation pages for broken internal links (404s). + +## Usage + +```bash +# Basic usage (with deduplication - recommended) +bun ./scripts/lint-404s/main.ts + +# Show progress for each page +bun ./scripts/lint-404s/main.ts --progress + +# Skip deduplication and check all pages (for debugging) +bun ./scripts/lint-404s/main.ts --skip-deduplication + +# Filter to a specific path +bun ./scripts/lint-404s/main.ts --path platforms/javascript +``` + +## Deduplication + +By default, the checker **deduplicates common files** to improve performance. + +### Why? + +The Sentry docs use a "common" file system where documentation is shared across multiple platforms. For example: + +- `/platforms/apple/common/configuration/index.mdx` is rendered as: + - `/platforms/apple/guides/ios/configuration/` + - `/platforms/apple/guides/macos/configuration/` + - `/platforms/apple/guides/watchos/configuration/` + - ... and many more + +Without deduplication, the checker would fetch and test the same content dozens of times, which: + +- Takes much longer to run +- Wastes CI resources +- Provides no additional value (the content is identical) + +### How it works + +1. The checker fetches a source map from `/api/source-map` that maps each slug to its source file +2. It tracks which source files have been checked +3. For common files, it only checks the first instance +4. **API-generated pages** are always checked (they have no source file) + +This typically reduces the number of pages checked from **~9,000 to ~2,500**, a **72% reduction**. + +### When to use `--skip-deduplication` + +Use this flag to skip deduplication and verify that all rendered pages work correctly, even if they share the same source. This is rarely necessary but can help debug issues with: + +- Path routing +- Platform-specific rendering bugs +- Edge cases in the build system + +## Ignore List + +The `ignore-list.txt` file contains paths that should be skipped during checking. Add paths here (one per line) if they're known to be inaccessible or are special cases. + +## Exit Codes + +- `0` - No 404s found +- `1` - 404s were detected diff --git a/scripts/lint-404s/main.ts b/scripts/lint-404s/main.ts index 706d42ff2f238..9b47197aca786 100644 --- a/scripts/lint-404s/main.ts +++ b/scripts/lint-404s/main.ts @@ -13,6 +13,7 @@ const trimSlashes = (s: string) => s.replace(/(^\/|\/$)/g, ''); const ignoreListFile = path.join(dirname(import.meta.url), './ignore-list.txt'); const showProgress = process.argv.includes('--progress'); +const deduplicatePages = !process.argv.includes('--skip-deduplication'); // Get the path filter if specified const pathFilterIndex = process.argv.indexOf('--path'); @@ -35,22 +36,73 @@ async function fetchWithFollow(url: URL | string): Promise { return r; } +async function deduplicateSlugs( + allSlugs: string[] +): Promise<{skippedCount: number; slugsToCheck: string[]}> { + try { + const sourceMap: Record = await fetch( + `${baseURL}api/source-map` + ).then(r => r.json()); + + const checkedSources = new Set(); + const slugsToCheck: string[] = []; + let skippedCount = 0; + + for (const slug of allSlugs) { + const normalizedSlug = slug.replace(/\/$/, ''); + const sourcePath = sourceMap[normalizedSlug]; + + // Always check API-generated pages (no source file) + if (!sourcePath) { + slugsToCheck.push(slug); + continue; + } + + // Skip if we've already checked this source file + if (checkedSources.has(sourcePath)) { + skippedCount++; + continue; + } + + // First time seeing this source file + checkedSources.add(sourcePath); + slugsToCheck.push(slug); + } + + return {skippedCount, slugsToCheck}; + } catch (error) { + console.warn('āš ļø Failed to fetch source map:', error.message); + console.warn('Falling back to checking all pages...\n'); + return {skippedCount: 0, slugsToCheck: allSlugs}; + } +} + async function main() { const sitemap = await fetch(`${baseURL}sitemap.xml`).then(r => r.text()); - const slugs = [...sitemap.matchAll(/([^<]*)<\/loc>/g)] + const allSlugs = [...sitemap.matchAll(/([^<]*)<\/loc>/g)] .map(l => l[1]) .map(url => trimSlashes(new URL(url).pathname)) .filter(Boolean) .filter(slug => (pathFilter ? slug.startsWith(pathFilter) : true)); - const allSlugsSet = new Set(slugs); - - if (pathFilter) { - console.log('Checking 404s on %d pages in /%s', slugs.length, pathFilter); - } else { - console.log('Checking 404s on %d pages', slugs.length); + const allSlugsSet = new Set(allSlugs); + + // Deduplicate pages with same source file (default behavior) + const {skippedCount, slugsToCheck} = deduplicatePages + ? await deduplicateSlugs(allSlugs) + : {skippedCount: 0, slugsToCheck: allSlugs}; + + if (skippedCount > 0) { + console.log( + 'Deduplication: checking %d unique pages (skipped %d duplicates)\n', + slugsToCheck.length, + skippedCount + ); } + const pathInfo = pathFilter ? ` in /${pathFilter}` : ''; + console.log('Checking 404s on %d pages%s', slugsToCheck.length, pathInfo); + const all404s: {page404s: Link[]; slug: string}[] = []; // check if the slug equivalent of the href is in the sitemap @@ -100,7 +152,7 @@ async function main() { return false; } - for (const slug of slugs) { + for (const slug of slugsToCheck) { const pageUrl = new URL(slug, baseURL); const now = performance.now(); const html = await fetchWithFollow(pageUrl.href).then(r => r.text()); @@ -134,7 +186,7 @@ async function main() { } if (all404s.length === 0) { - console.log('\n\nšŸŽ‰ No 404s found'); + console.log('\nšŸŽ‰ No 404s found'); return false; } const numberOf404s = all404s.map(x => x.page404s.length).reduce((a, b) => a + b, 0); From c665eb9f6a2595498e7b1461f4317dce8bb257ef Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 13:07:49 -0500 Subject: [PATCH 2/9] make slug normalization consistent --- app/api/source-map/route.ts | 4 ++-- scripts/lint-404s/main.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/api/source-map/route.ts b/app/api/source-map/route.ts index 2f4eb2e3af57c..ad56cc46bce89 100644 --- a/app/api/source-map/route.ts +++ b/app/api/source-map/route.ts @@ -13,8 +13,8 @@ export async function GET() { const sourceMap: Record = {}; for (const doc of docs) { - // Normalize slug (remove trailing slash if present) - const slug = doc.slug.replace(/\/$/, ''); + // Normalize slug (remove leading and trailing slashes to match main.ts trimSlashes) + const slug = doc.slug.replace(/(^\/|\/$)/g, ''); // sourcePath will be null for API-generated pages, which we want to keep sourceMap[slug] = doc.sourcePath ?? null; } diff --git a/scripts/lint-404s/main.ts b/scripts/lint-404s/main.ts index 9b47197aca786..fda76e7df46a6 100644 --- a/scripts/lint-404s/main.ts +++ b/scripts/lint-404s/main.ts @@ -49,7 +49,8 @@ async function deduplicateSlugs( let skippedCount = 0; for (const slug of allSlugs) { - const normalizedSlug = slug.replace(/\/$/, ''); + // Use same normalization as route.ts (remove leading and trailing slashes) + const normalizedSlug = slug.replace(/(^\/|\/$)/g, ''); const sourcePath = sourceMap[normalizedSlug]; // Always check API-generated pages (no source file) From 680154bea6bbeab9441e74164b7bc6b77ccf4cff Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 13:26:50 -0500 Subject: [PATCH 3/9] trigger lint when api files & scripts change --- .github/workflows/lint-404s.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint-404s.yml b/.github/workflows/lint-404s.yml index c665495a0a990..c433bdedf83d5 100644 --- a/.github/workflows/lint-404s.yml +++ b/.github/workflows/lint-404s.yml @@ -23,6 +23,8 @@ jobs: - 'docs/**' - 'includes/**' - 'platform-includes/**' + - 'scripts/lint-404s/**' + - 'app/api/source-map/**' dev-docs: - 'develop-docs/**' - uses: oven-sh/setup-bun@v2 From 406463a77fb5a5976a6f924030e7e7ced64b4deb Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 13:53:26 -0500 Subject: [PATCH 4/9] optimize cache logic --- .github/workflows/lint-404s.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint-404s.yml b/.github/workflows/lint-404s.yml index c433bdedf83d5..f296ac309aaff 100644 --- a/.github/workflows/lint-404s.yml +++ b/.github/workflows/lint-404s.yml @@ -32,26 +32,36 @@ jobs: bun-version: latest - uses: actions/cache@v4 - id: cache + id: cache-node-modules with: path: | ${{ github.workspace }}/node_modules - ${{ github.workspace }}/.next/cache ${{ github.workspace }}/.eslintcache - key: node-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/yarn.lock') }} + key: node-modules-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/yarn.lock') }} restore-keys: | - node-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}- + node-modules-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}- + + # Cache the Next.js build output to avoid rebuilding when docs content changes + - uses: actions/cache@v4 + id: cache-nextjs + with: + path: | + ${{ github.workspace }}/.next + key: nextjs-build-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('src/**', 'app/**', 'next.config.ts', 'tsconfig.json', 'tailwind.config.mjs') }} + restore-keys: | + nextjs-build-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}- + nextjs-build-${{ runner.os }}- - run: yarn install --frozen-lockfile - run: yarn next build - if: steps.filter.outputs.docs == 'true' + if: steps.filter.outputs.docs == 'true' && steps.cache-nextjs.outputs.cache-hit != 'true' env: SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 NEXT_PUBLIC_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 - run: yarn build:developer-docs - if: steps.filter.outputs.dev-docs == 'true' + if: steps.filter.outputs.dev-docs == 'true' && steps.cache-nextjs.outputs.cache-hit != 'true' env: SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 NEXT_PUBLIC_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 From 424c688c8feb94719bc29395337d29fe39ecaad1 Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 14:48:38 -0500 Subject: [PATCH 5/9] reduce serverless function size to avoid 250MB limit - Extract lightweight frontmatter module to avoid bundling mdx-bundler - Add aggressive outputFileTracingExcludes for build dependencies - Update source-map API route to use lightweight imports --- app/api/source-map/route.ts | 25 ++++++++- app/sitemap.ts | 2 +- next.config.ts | 59 ++++++++++++++++++++- src/frontmatter.ts | 101 ++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/frontmatter.ts diff --git a/app/api/source-map/route.ts b/app/api/source-map/route.ts index ad56cc46bce89..ef47e3bbb0445 100644 --- a/app/api/source-map/route.ts +++ b/app/api/source-map/route.ts @@ -1,7 +1,8 @@ import {NextResponse} from 'next/server'; +import {apiCategories} from 'sentry-docs/build/resolveOpenAPI'; +import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/frontmatter'; import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; -import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/mdx'; /** * API endpoint that returns a mapping of slugs to their source file paths. @@ -9,13 +10,33 @@ import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/mdx'; */ export async function GET() { const docs = await (isDeveloperDocs ? getDevDocsFrontMatter() : getDocsFrontMatter()); + + // For non-developer docs, add API-generated pages (they have undefined sourcePath) + if (!isDeveloperDocs) { + const categories = await apiCategories(); + categories.forEach(category => { + docs.push({ + title: category.name, + slug: `api/${category.slug}`, + sourcePath: undefined, + }); + + category.apis.forEach(api => { + docs.push({ + title: api.name, + slug: `api/${category.slug}/${api.slug}`, + sourcePath: undefined, + }); + }); + }); + } const sourceMap: Record = {}; for (const doc of docs) { // Normalize slug (remove leading and trailing slashes to match main.ts trimSlashes) const slug = doc.slug.replace(/(^\/|\/$)/g, ''); - // sourcePath will be null for API-generated pages, which we want to keep + // sourcePath will be null for API-generated pages sourceMap[slug] = doc.sourcePath ?? null; } diff --git a/app/sitemap.ts b/app/sitemap.ts index 173989e0a6fae..cfb22fb2975a3 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,7 +1,7 @@ import type {MetadataRoute} from 'next'; +import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/frontmatter'; import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; -import {getDevDocsFrontMatter, getDocsFrontMatter} from 'sentry-docs/mdx'; export default async function sitemap(): Promise { if (isDeveloperDocs) { diff --git a/next.config.ts b/next.config.ts index c5a9d9ad67acc..07a3067404cd4 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,11 @@ import {withSentryConfig} from '@sentry/nextjs'; import {REMOTE_IMAGE_PATTERNS} from './src/config/images'; import {redirects} from './redirects.js'; +// Exclude build-time-only dependencies from serverless function bundles to stay under +// Vercel's 250MB limit. These packages (esbuild, mdx-bundler, sharp, etc.) are only +// needed during the build process to compile MDX and optimize assets. The compiled +// output is used at runtime, so bundling these ~150-200MB of dependencies would bloat +// functions unnecessarily and cause deployment failures. const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS ? { '/**/*': [ @@ -13,6 +18,24 @@ const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS './.next/cache/mdx-bundler/**/*', './.next/cache/md-exports/**/*', 'docs/**/*', + // Exclude heavy build dependencies + 'node_modules/@esbuild/**/*', + 'node_modules/esbuild/**/*', + 'node_modules/@aws-sdk/**/*', + 'node_modules/@google-cloud/**/*', + 'node_modules/prettier/**/*', + 'node_modules/@prettier/**/*', + 'node_modules/sharp/**/*', + 'node_modules/mermaid/**/*', + // Exclude MDX processing dependencies + 'node_modules/mdx-bundler/**/*', + 'node_modules/rehype-preset-minify/**/*', + 'node_modules/rehype-prism-plus/**/*', + 'node_modules/rehype-prism-diff/**/*', + 'node_modules/remark-gfm/**/*', + 'node_modules/remark-mdx-images/**/*', + 'node_modules/unified/**/*', + 'node_modules/rollup/**/*', ], } : { @@ -23,7 +46,24 @@ const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS './.next/cache/md-exports/**/*', './apps/**/*', 'develop-docs/**/*', - 'node_modules/@esbuild/*', + // Exclude heavy build dependencies + 'node_modules/@esbuild/**/*', + 'node_modules/esbuild/**/*', + 'node_modules/@aws-sdk/**/*', + 'node_modules/@google-cloud/**/*', + 'node_modules/prettier/**/*', + 'node_modules/@prettier/**/*', + 'node_modules/sharp/**/*', + 'node_modules/mermaid/**/*', + // Exclude MDX processing dependencies + 'node_modules/mdx-bundler/**/*', + 'node_modules/rehype-preset-minify/**/*', + 'node_modules/rehype-prism-plus/**/*', + 'node_modules/rehype-prism-diff/**/*', + 'node_modules/remark-gfm/**/*', + 'node_modules/remark-mdx-images/**/*', + 'node_modules/unified/**/*', + 'node_modules/rollup/**/*', ], '/platform-redirect': [ '**/*.gif', @@ -57,7 +97,22 @@ if (process.env.NODE_ENV !== 'development' && !process.env.NEXT_PUBLIC_SENTRY_DS const nextConfig = { pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx', 'mdx'], trailingSlash: true, - serverExternalPackages: ['rehype-preset-minify'], + serverExternalPackages: [ + 'rehype-preset-minify', + 'esbuild', + '@esbuild/darwin-arm64', + '@esbuild/darwin-x64', + '@esbuild/linux-arm64', + '@esbuild/linux-x64', + '@esbuild/win32-x64', + 'mdx-bundler', + 'sharp', + '@aws-sdk/client-s3', + '@google-cloud/storage', + 'prettier', + '@prettier/plugin-xml', + 'mermaid', + ], outputFileTracingExcludes, images: { contentDispositionType: 'inline', // "open image in new tab" instead of downloading diff --git a/src/frontmatter.ts b/src/frontmatter.ts new file mode 100644 index 0000000000000..11b103c4a7e42 --- /dev/null +++ b/src/frontmatter.ts @@ -0,0 +1,101 @@ +import matter from 'gray-matter'; +import {readFile} from 'node:fs/promises'; +import path from 'node:path'; +import {limitFunction} from 'p-limit'; + +import getAllFilesRecursively from './files'; +import {FrontMatter} from './types'; +import {isNotNil} from './utils'; + +const root = path.resolve(__dirname, '..'); +const FILE_CONCURRENCY_LIMIT = 20; + +const formatSlug = (slug: string): string => + slug + .replace(/^platforms\//, '') + .replace(/\/_category_.mdx?$/, '') + .replace(/\/index.mdx?$/, '') + .replace(/\.mdx?$/, ''); + +let getDocsFrontMatterCache: Promise | undefined; + +export function getDocsFrontMatter(): Promise { + if (!getDocsFrontMatterCache) { + getDocsFrontMatterCache = getDocsFrontMatterUncached(); + } + return getDocsFrontMatterCache; +} + +async function getDocsFrontMatterUncached(): Promise { + const docsPath = path.join(root, 'docs'); + const files = await getAllFilesRecursively(docsPath); + const allFrontMatter: FrontMatter[] = []; + + await Promise.all( + files.map( + limitFunction( + async file => { + const fileName = file.slice(docsPath.length + 1); + if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { + return; + } + + if (fileName.indexOf('/common/') !== -1) { + return; + } + + const source = await readFile(file, 'utf8'); + const {data: frontmatter} = matter(source); + allFrontMatter.push({ + ...(frontmatter as FrontMatter), + slug: formatSlug(fileName), + sourcePath: path.join('docs', fileName), + }); + }, + {concurrency: FILE_CONCURRENCY_LIMIT} + ) + ) + ); + + return allFrontMatter; +} + +let getDevDocsFrontMatterCache: Promise | undefined; + +export function getDevDocsFrontMatter(): Promise { + if (!getDevDocsFrontMatterCache) { + getDevDocsFrontMatterCache = getDevDocsFrontMatterUncached(); + } + return getDevDocsFrontMatterCache; +} + +async function getDevDocsFrontMatterUncached(): Promise { + const folder = 'develop-docs'; + const docsPath = path.join(root, folder); + const files = await getAllFilesRecursively(docsPath); + const frontMatters = ( + await Promise.all( + files.map( + limitFunction( + async file => { + const fileName = file.slice(docsPath.length + 1); + if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { + return undefined; + } + + const source = await readFile(file, 'utf8'); + const {data: frontmatter} = matter(source); + return { + ...(frontmatter as FrontMatter), + slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), + sourcePath: path.join(folder, fileName), + }; + }, + {concurrency: FILE_CONCURRENCY_LIMIT} + ) + ) + ) + ).filter(isNotNil); + return frontMatters; +} + From 3c9fa262211f8c4c66dc8d53fe04d4fddccb855a Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:51:04 +0000 Subject: [PATCH 6/9] [getsentry/action-github-commit] Auto commit --- app/api/source-map/route.ts | 3 +-- src/frontmatter.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/source-map/route.ts b/app/api/source-map/route.ts index ef47e3bbb0445..c7a273acbf727 100644 --- a/app/api/source-map/route.ts +++ b/app/api/source-map/route.ts @@ -10,7 +10,7 @@ import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; */ export async function GET() { const docs = await (isDeveloperDocs ? getDevDocsFrontMatter() : getDocsFrontMatter()); - + // For non-developer docs, add API-generated pages (they have undefined sourcePath) if (!isDeveloperDocs) { const categories = await apiCategories(); @@ -42,4 +42,3 @@ export async function GET() { return NextResponse.json(sourceMap); } - diff --git a/src/frontmatter.ts b/src/frontmatter.ts index 11b103c4a7e42..f189f50481591 100644 --- a/src/frontmatter.ts +++ b/src/frontmatter.ts @@ -98,4 +98,3 @@ async function getDevDocsFrontMatterUncached(): Promise { ).filter(isNotNil); return frontMatters; } - From 4035523a4c88f84bd36b96275980d0318a125c85 Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 15:01:25 -0500 Subject: [PATCH 7/9] remove docs exclusion from sitemap route --- next.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index 07a3067404cd4..0de6540ad6029 100644 --- a/next.config.ts +++ b/next.config.ts @@ -78,7 +78,6 @@ const outputFileTracingExcludes = process.env.NEXT_PUBLIC_DEVELOPER_DOCS 'public/og-images/**/*', ], 'sitemap.xml': [ - 'docs/**/*', 'public/mdx-images/**/*', 'public/og-images/**/*', '**/*.gif', From 51a3d4dbdb0ffe287a06294de6cf8184d8d95b2d Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 15:54:42 -0500 Subject: [PATCH 8/9] fix path resolve issue --- src/frontmatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontmatter.ts b/src/frontmatter.ts index f189f50481591..d9bb794a3c584 100644 --- a/src/frontmatter.ts +++ b/src/frontmatter.ts @@ -7,7 +7,7 @@ import getAllFilesRecursively from './files'; import {FrontMatter} from './types'; import {isNotNil} from './utils'; -const root = path.resolve(__dirname, '..'); +const root = process.cwd(); const FILE_CONCURRENCY_LIMIT = 20; const formatSlug = (slug: string): string => From 9e8f7d4e4938e74f638fd9df83b8dadcf139223c Mon Sep 17 00:00:00 2001 From: paulj Date: Mon, 10 Nov 2025 16:36:51 -0500 Subject: [PATCH 9/9] separate Next.js build caches for docs and dev-docs --- .github/workflows/lint-404s.yml | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint-404s.yml b/.github/workflows/lint-404s.yml index f296ac309aaff..f919bac9f40d9 100644 --- a/.github/workflows/lint-404s.yml +++ b/.github/workflows/lint-404s.yml @@ -42,26 +42,39 @@ jobs: node-modules-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}- # Cache the Next.js build output to avoid rebuilding when docs content changes + # Separate caches for docs and dev-docs since they produce different outputs - uses: actions/cache@v4 - id: cache-nextjs + id: cache-nextjs-docs + if: steps.filter.outputs.docs == 'true' with: path: | ${{ github.workspace }}/.next - key: nextjs-build-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('src/**', 'app/**', 'next.config.ts', 'tsconfig.json', 'tailwind.config.mjs') }} + key: nextjs-build-docs-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('src/**', 'app/**', 'next.config.ts', 'tsconfig.json', 'tailwind.config.mjs') }} restore-keys: | - nextjs-build-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}- - nextjs-build-${{ runner.os }}- + nextjs-build-docs-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}- + nextjs-build-docs-${{ runner.os }}- + + - uses: actions/cache@v4 + id: cache-nextjs-dev-docs + if: steps.filter.outputs.dev-docs == 'true' + with: + path: | + ${{ github.workspace }}/.next + key: nextjs-build-dev-docs-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('src/**', 'app/**', 'next.config.ts', 'tsconfig.json', 'tailwind.config.mjs') }} + restore-keys: | + nextjs-build-dev-docs-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}- + nextjs-build-dev-docs-${{ runner.os }}- - run: yarn install --frozen-lockfile - run: yarn next build - if: steps.filter.outputs.docs == 'true' && steps.cache-nextjs.outputs.cache-hit != 'true' + if: steps.filter.outputs.docs == 'true' && steps.cache-nextjs-docs.outputs.cache-hit != 'true' env: SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 NEXT_PUBLIC_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 - run: yarn build:developer-docs - if: steps.filter.outputs.dev-docs == 'true' && steps.cache-nextjs.outputs.cache-hit != 'true' + if: steps.filter.outputs.dev-docs == 'true' && steps.cache-nextjs-dev-docs.outputs.cache-hit != 'true' env: SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0 NEXT_PUBLIC_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0