From fc3f9f4e879163cbe639317e1a7d91f1a283acd3 Mon Sep 17 00:00:00 2001 From: Guanzhou Song Date: Mon, 24 Aug 2026 21:44:04 -0400 Subject: [PATCH] Fix four things a 0.116 readiness review reproduced Each of these was found by running the site's own instructions, not by reading them, so each fix is stated in the terms the failure appeared in. Pinning a version fails as documented. The Download page derives one version string from the extension packages and prints it everywhere, but the two package families are versioned differently: `postgresql-18-documentdb` is `0.116-0` while the full-stack `documentdb-18` is `0.116.0`. Following the page gives `E: Version '0.116-0' for 'documentdb-18' was not found`. Derive both, name both, and say which belongs to which -- and stop hardcoding four package names in the pin commands so they track the actual selection. The Docker quick start produced an empty database. `--init-data` defaults to false, and `--skip-init-data` is a legacy alias for `--init-data false`, so the documented command yields `postgres, template0, template1` and a sample collection count of 0 while the page claimed sample data loads by default. Pass `--init-data true` and correct the table. Also add the image tag list: `latest` is `pg17`, and the tags carry the PostgreSQL major, which is the only way to evaluate on the same major you intend to deploy on. Two doc URLs were dead and nobody could see it. `redirect()` cannot work under `output: "export"` -- there is no server to send a 3xx -- so Next rendered both as `` bodies served with HTTP 200. They were in sitemap.xml and linked from two live pages, and every status-code link checker called them healthy. Render a real page that links to the replacement and forwards on hydration, mark it noindex so the sitemap step drops it, and fail the build if any route ever ships as an error document again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8df9084a-ccaf-432c-b015-2ccd8893a9d8 Signed-off-by: Guanzhou Song --- app/components/MovedNotice.tsx | 56 +++++++++++++++++++++++++ app/docs/[section]/[[...slug]]/page.tsx | 52 +++++++++++++++++++---- app/lib/releaseInfo.ts | 28 ++++++++++++- app/packages/page.tsx | 34 ++++++++------- app/services/articleService.ts | 53 ++++++++++++++++++----- scripts/generate-sitemap.mjs | 35 +++++++++++++++- 6 files changed, 223 insertions(+), 35 deletions(-) create mode 100644 app/components/MovedNotice.tsx diff --git a/app/components/MovedNotice.tsx b/app/components/MovedNotice.tsx new file mode 100644 index 0000000..3dc9597 --- /dev/null +++ b/app/components/MovedNotice.tsx @@ -0,0 +1,56 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +type MovedNoticeProps = { + /** Where this page's content now lives. */ + href: string; + /** Human-readable name of the destination page. */ + title: string; +}; + +/** + * Stands in for a page whose content has been superseded by another page. + * + * The obvious implementation is `redirect()` in the server component, and this + * replaces exactly that. It cannot work here: the site is exported as static + * HTML (`output: "export"`), so there is no server to issue a 3xx, and Next + * renders the thrown redirect as an error document instead. The result was a + * page that returned HTTP 200 with an empty `__next_error__` body while still + * being listed in sitemap.xml and linked from two live pages, so both readers + * and crawlers hit a dead end. + * + * Rendering real markup serves both: a crawler sees a working link to the + * replacement, and a reader who followed a bookmark is forwarded once the + * bundle hydrates. + */ +export default function MovedNotice({ href, title }: MovedNoticeProps) { + const router = useRouter(); + + useEffect(() => { + router.replace(href); + }, [href, router]); + + return ( +
+
+

This page has moved

+

+ Its content now lives in{" "} + + {title} + + . You should be redirected automatically. +

+ + Continue to {title} + +
+
+ ); +} diff --git a/app/docs/[section]/[[...slug]]/page.tsx b/app/docs/[section]/[[...slug]]/page.tsx index 7a0a5dc..363c487 100644 --- a/app/docs/[section]/[[...slug]]/page.tsx +++ b/app/docs/[section]/[[...slug]]/page.tsx @@ -1,11 +1,12 @@ import Link from "next/link"; -import { notFound, redirect } from 'next/navigation'; +import { notFound } from 'next/navigation'; import { capitalCase } from 'change-case'; import { getAllArticlePaths, getArticleByPath } from "../../../services/articleService"; import { getMetadata } from "../../../services/metadataService"; import ComingSoon from "../../../components/ComingSoon"; import CommandSnippet from "../../../components/CommandSnippet"; import Markdown from "../../../components/Markdown"; +import MovedNotice from "../../../components/MovedNotice"; const dockerQuickRunCommand = `docker run -dt --name documentdb \\ -p 10260:10260 \\ @@ -19,6 +20,30 @@ const primerPrimaryLinkClass = const primerSecondaryLinkClass = "font-semibold text-blue-300 transition-colors hover:text-blue-200"; +/** + * Slugs under /docs/getting-started/ that no longer have their own content and + * now point at the page that replaced them. Kept in one place so the metadata + * and the rendered notice can never disagree about where a page went. + */ +const movedGettingStartedPages: Record = { + 'prebuilt-packages': { + href: '/docs/getting-started/packages', + title: 'Linux Packages Quick Start', + }, + 'vscode-extension-guide': { + href: '/docs/getting-started/vscode-quickstart', + title: 'Visual Studio Code Quick Start', + }, +}; + +function getMovedPage(section: string, slug: string[]) { + if (section !== 'getting-started') { + return undefined; + } + + return movedGettingStartedPages[slug[slug.length - 1] ?? '']; +} + export async function generateStaticParams() { const paths = getAllArticlePaths(); @@ -37,6 +62,21 @@ interface PageProps { export async function generateMetadata({ params }: PageProps) { const { section, slug = [] } = await params; + const movedPage = getMovedPage(section, slug); + + // These URLs stay reachable for old bookmarks and inbound links, but the + // content is somewhere else now. Pointing crawlers at the replacement stops + // the two copies from competing, and keeps them out of sitemap.xml, which + // is generated by reading the robots directive out of the built HTML. + if (movedPage) { + return { + title: `${movedPage.title} - DocumentDB Documentation`, + description: `This page has moved to ${movedPage.title}.`, + alternates: { canonical: movedPage.href.endsWith('/') ? movedPage.href : `${movedPage.href}/` }, + robots: { index: false, follow: true }, + }; + } + const articleData = getArticleByPath(section, slug); if (!articleData) { @@ -59,14 +99,10 @@ export async function generateMetadata({ params }: PageProps) { export default async function ArticlePage({ params }: PageProps) { const { section, slug = [] } = await params; - const currentSlug = slug[slug.length - 1]; - - if (section === 'getting-started' && currentSlug === 'prebuilt-packages') { - redirect('/docs/getting-started/packages'); - } + const movedPage = getMovedPage(section, slug); - if (section === 'getting-started' && currentSlug === 'vscode-extension-guide') { - redirect('/docs/getting-started/vscode-quickstart'); + if (movedPage) { + return ; } const articleData = getArticleByPath(section, slug); diff --git a/app/lib/releaseInfo.ts b/app/lib/releaseInfo.ts index 0171b0b..6afc8a4 100644 --- a/app/lib/releaseInfo.ts +++ b/app/lib/releaseInfo.ts @@ -18,8 +18,18 @@ export type ReleaseInfo = { aptVersion: string; /** Extension package version on RPM, e.g. "0.116.0-1.el9". */ rpmVersion: string; - /** Version of every non-extension package, e.g. "0.116.0". */ + /** + * Version of every non-extension package, e.g. "0.116.0". + * + * The extension keeps the control-file form (`0.116-0`) while the meta, + * per-major, gateway, tools and common packages use the flat dotted form. + * Pinning examples MUST pick the right one for the package being pinned: + * `apt install documentdb-18=0.116-0` fails with "Version '0.116-0' for + * 'documentdb-18' was not found", because that package is `0.116.0`. + */ metaVersion: string; + /** RPM form of the non-extension packages, e.g. "0.116.0-1". */ + metaRpmVersion: string; releaseUrl: string; assetNames: readonly string[]; }; @@ -33,6 +43,7 @@ export const FALLBACK_RELEASE: ReleaseInfo = { aptVersion: "0.116-0", rpmVersion: "0.116.0-1.el9", metaVersion: "0.116.0", + metaRpmVersion: "0.116.0-1", releaseUrl: "https://github.com/documentdb/documentdb/releases/tag/v0.116-0", assetNames: [], }; @@ -103,7 +114,20 @@ export function parseReleaseInfo(payload: unknown): ReleaseInfo { firstMatch(names, /^documentdb-(\d+\.\d+\.\d+)-\d+\.noarch\.rpm$/) ?? FALLBACK_RELEASE.metaVersion; - return { tagName, aptVersion, rpmVersion, metaVersion, releaseUrl, assetNames: names }; + // e.g. documentdb-0.116.0-1.noarch.rpm -> 0.116.0-1 + const metaRpmVersion = + firstMatch(names, /^documentdb-(\d+\.\d+\.\d+-\d+)\.noarch\.rpm$/) ?? + FALLBACK_RELEASE.metaRpmVersion; + + return { + tagName, + aptVersion, + rpmVersion, + metaVersion, + metaRpmVersion, + releaseUrl, + assetNames: names, + }; } /** diff --git a/app/packages/page.tsx b/app/packages/page.tsx index 76ae72d..894d735 100644 --- a/app/packages/page.tsx +++ b/app/packages/page.tsx @@ -535,27 +535,33 @@ export default function PackagesPage() {

- Use the commands below to discover available versions before pinning. The - examples name the extension package; substitute{" "} - {selectedPackageNames} to pin the package - your selected target actually installs. Replace{" "} - <VERSION> with the version string - shown by the list command (e.g.{" "} - {repoAptVersionExample} for APT,{" "} - {repoRpmVersionExample} for RPM). The - package repositories can lag the newest GitHub release, so pin to a version the - list command actually shows. + Use the commands below to discover available versions before pinning, and pin{" "} + {selectedPackageNames} — the package your + selected target actually installs. +

+

+ The two package families carry different version strings, so the + right <VERSION> depends on which you + pin. The extension is{" "} + {repoAptVersionExample} (APT) /{" "} + {repoRpmVersionExample} (RPM), while{" "} + documentdb,{" "} + documentdb-<pg> and the gateway are{" "} + {release.metaVersion} (APT) /{" "} + {release.metaRpmVersion} (RPM). Pinning{" "} + documentdb-18={repoAptVersionExample}{" "} + fails — always take the string the list command prints.

APT — list then pin

- apt-cache madison postgresql-18-documentdb + apt-cache madison {selectedPackageNames}
- sudo apt install postgresql-18-documentdb=<VERSION> + sudo apt install {selectedPackageNames}=<VERSION>
@@ -563,12 +569,12 @@ export default function PackagesPage() {

RPM — list then pin

- dnf --showduplicates list postgresql18-documentdb + dnf --showduplicates list {selectedPackageNames}
- sudo dnf install postgresql18-documentdb-<VERSION> + sudo dnf install {selectedPackageNames}-<VERSION>
diff --git a/app/services/articleService.ts b/app/services/articleService.ts index 7a9e947..cfc85b9 100644 --- a/app/services/articleService.ts +++ b/app/services/articleService.ts @@ -33,14 +33,18 @@ docker run -dt --name documentdb \\ -p 10260:10260 \\ ghcr.io/documentdb/documentdb/documentdb-local:latest \\ --username \\ - --password + --password \\ + --init-data true \`\`\` > Replace \`\` and \`\` with your own credentials. > -> DocumentDB Local loads built-in sample data into \`sampledb\` by default. See -> [DocumentDB Local](/docs/documentdb-local) for \`--skip-init-data\`, -> \`--init-data-path\`, certificate setup, and additional runtime options. +> \`--init-data true\` seeds the built-in sample data into \`sampledb\`, which the +> verification step below queries. It is **not** enabled by default — without it the +> container starts with no \`sampledb\` and \`use sampledb\` returns nothing. The data is +> seeded once per data volume; re-create the volume to seed again. See +> [DocumentDB Local](/docs/documentdb-local) for \`--init-data-path\`, certificate setup, +> and additional runtime options. ## Verify the container @@ -80,7 +84,7 @@ If you prefer certificate validation instead of \`--tlsAllowInvalidCertificates\ The quick start command above is ideal for disposable local environments. When you need more control: - Use \`--data-path\` with a mounted host directory to keep data across container restarts -- Use \`--skip-init-data\` if you want an empty instance instead of the default \`sampledb\` collections +- Omit \`--init-data true\` if you want an empty instance instead of the \`sampledb\` collections - Use \`--init-data-path\` to run your own \`.js\` initialization scripts with \`mongosh\` at startup The built-in sample dataset includes \`users\`, \`products\`, \`orders\`, and \`analytics\` collections in \`sampledb\`. @@ -946,17 +950,46 @@ If \`mongosh\` does not connect on the first try: - [Samples Gallery](/samples) `; -const documentdbLocalDataInitializationContent = `## Data initialization +const documentdbLocalDataInitializationContent = `## Container image tags + +The \`latest\` tag is a convenience alias. Pin an explicit tag for anything reproducible: + +| Tag | Contents | +|---|---| +| \`ghcr.io/documentdb/documentdb/documentdb-local:pg18-0.116.0\` | DocumentDB 0.116.0 on PostgreSQL 18 | +| \`…:pg17-0.116.0\` | DocumentDB 0.116.0 on PostgreSQL 17 | +| \`…:pg16-0.116.0\` · \`…:pg15-0.116.0\` | PostgreSQL 16 and 15 | +| \`…:latest\` | Currently identical to \`pg17-0.116.0\` | + +> \`latest\` tracks **PostgreSQL 17**, while the \`documentdb\` package on Linux pins +> **PostgreSQL 18**. If you evaluate in Docker and then deploy from packages, you change +> major version unless you pin the tag deliberately. + +Every image records what it was built from: + +\`\`\`bash +docker run --rm --entrypoint cat ghcr.io/documentdb/documentdb/documentdb-local:pg18-0.116.0 /version.txt +\`\`\` + +## Data initialization + +DocumentDB Local starts **empty**. Pass \`--init-data true\` to seed a \`sampledb\` database +with the \`users\`, \`products\`, \`orders\`, and \`analytics\` collections: + +\`\`\`bash +docker run -dt -p 10260:10260 --name documentdb \\ + ghcr.io/documentdb/documentdb/documentdb-local:latest \\ + --username --password --init-data true +\`\`\` -DocumentDB Local starts with built-in sample data by default. The container creates a -\`sampledb\` database with the \`users\`, \`products\`, \`orders\`, and \`analytics\` -collections so you can explore queries right away. +Seeding happens once per data volume, on a fresh volume. Re-create the volume to seed again. ### Control initialization behavior | Requirement | Arg | Env | Default | Description | |---|---|---|---|---| -| Skip built-in sample data | \`--skip-init-data\` | \`SKIP_INIT_DATA\` | \`false\` | Start without loading the default sample collections. | +| Load built-in sample data | \`--init-data [true\\|false]\` | \`INIT_DATA\` | \`false\` | Seed the \`sampledb\` sample collections on a fresh data volume. | +| Skip built-in sample data | \`--skip-init-data\` | \`SKIP_INIT_DATA\` | — | Legacy alias for \`--init-data false\`. Does not affect \`--init-data-path\`. | | Run custom initialization scripts | \`--init-data-path [PATH]\` | \`INIT_DATA_PATH\` | \`/init_doc_db.d\` | Execute every \`.js\` file in the mounted directory with \`mongosh\`. | The built-in sample dataset currently includes 5 users, 5 products, 4 orders, and 2 diff --git a/scripts/generate-sitemap.mjs b/scripts/generate-sitemap.mjs index b4e4f3e..e134566 100644 --- a/scripts/generate-sitemap.mjs +++ b/scripts/generate-sitemap.mjs @@ -33,6 +33,7 @@ const excludedTopLevelDirectories = new Set([ ]); let noindexPagesSkipped = 0; +const brokenPages = []; /** * True when the page asks crawlers not to index it. Reads the meta tag in @@ -46,6 +47,20 @@ function isNoindex(html) { ); } +/** + * Detects a page that rendered as Next's error document rather than as content. + * + * A server-side `redirect()` in a statically exported route cannot redirect — + * there is no server — so Next emits an error document instead. It is served + * with HTTP 200 and a `` shell, which means neither a + * status-code link check nor a crawler can tell it from a real page. Two such + * routes were listed in this sitemap and linked from live pages before this + * check existed. Fail the build rather than advertise them again. + */ +function isErrorDocument(html) { + return /]*\bid=["']__next_error__["']/i.test(html); +} + function xmlEscape(value) { return value .replace(/&/g, '&') @@ -65,7 +80,10 @@ function collectPages(directory, relativePath = '') { const indexFile = path.join(directory, 'index.html'); if (fs.existsSync(indexFile)) { - if (isNoindex(fs.readFileSync(indexFile, 'utf8'))) { + const html = fs.readFileSync(indexFile, 'utf8'); + if (isErrorDocument(html)) { + brokenPages.push(relativePath === '' ? '/' : `/${relativePath}/`); + } else if (isNoindex(html)) { noindexPagesSkipped += 1; } else { pages.push({ @@ -97,6 +115,21 @@ if (!fs.existsSync(outDir)) { const pages = collectPages(outDir).sort((a, b) => a.url.localeCompare(b.url)); +if (brokenPages.length > 0) { + console.error( + [ + 'These routes rendered as Next error documents rather than as pages:', + ...brokenPages.map((url) => ` ${url}`), + '', + 'They would be served with HTTP 200 and an empty body, so neither a status-code', + 'link check nor a crawler can tell them from real pages. The usual cause is a', + 'server-side redirect() in a statically exported route, which cannot redirect.', + 'Render a page instead (see app/components/MovedNotice.tsx).', + ].join('\n'), + ); + process.exit(1); +} + if (pages.length === 0) { console.error('No pages found in out/ - refusing to write an empty sitemap.'); process.exit(1);