Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions app/components/MovedNotice.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-neutral-900 py-16">
<div className="mx-auto max-w-2xl px-4 text-center sm:px-6">
<h1 className="mb-4 text-3xl font-bold text-white">This page has moved</h1>
<p className="mb-8 text-gray-300">
Its content now lives in{" "}
<Link href={href} className="text-blue-400 underline hover:text-blue-300">
{title}
</Link>
. You should be redirected automatically.
</p>
<Link
href={href}
className="inline-flex items-center justify-center rounded-md border border-blue-400 bg-blue-500/10 px-5 py-2 text-sm font-semibold text-blue-200 transition-colors hover:bg-blue-500/20"
>
Continue to {title}
</Link>
</div>
</div>
);
}
52 changes: 44 additions & 8 deletions app/docs/[section]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 \\
Expand All @@ -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<string, { href: string; title: string }> = {
'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();

Expand All @@ -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) {
Expand All @@ -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 <MovedNotice href={movedPage.href} title={movedPage.title} />;
}

const articleData = getArticleByPath(section, slug);
Expand Down
28 changes: 26 additions & 2 deletions app/lib/releaseInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Expand All @@ -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: [],
};
Expand Down Expand Up @@ -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,
};
}

/**
Expand Down
34 changes: 20 additions & 14 deletions app/packages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,40 +535,46 @@ export default function PackagesPage() {
</summary>
<div className="mt-4 space-y-4">
<p className="text-sm text-gray-400">
Use the commands below to discover available versions before pinning. The
examples name the extension package; substitute{" "}
<code className="text-gray-300">{selectedPackageNames}</code> to pin the package
your selected target actually installs. Replace{" "}
<code className="text-gray-300">&lt;VERSION&gt;</code> with the version string
shown by the list command (e.g.{" "}
<code className="text-gray-300">{repoAptVersionExample}</code> for APT,{" "}
<code className="text-gray-300">{repoRpmVersionExample}</code> 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{" "}
<code className="text-gray-300">{selectedPackageNames}</code> — the package your
selected target actually installs.
</p>
<p className="text-sm text-amber-300">
The two package families carry <strong>different version strings</strong>, so the
right <code className="text-gray-300">&lt;VERSION&gt;</code> depends on which you
pin. The extension is{" "}
<code className="text-gray-300">{repoAptVersionExample}</code> (APT) /{" "}
<code className="text-gray-300">{repoRpmVersionExample}</code> (RPM), while{" "}
<code className="text-gray-300">documentdb</code>,{" "}
<code className="text-gray-300">documentdb-&lt;pg&gt;</code> and the gateway are{" "}
<code className="text-gray-300">{release.metaVersion}</code> (APT) /{" "}
<code className="text-gray-300">{release.metaRpmVersion}</code> (RPM). Pinning{" "}
<code className="text-gray-300">documentdb-18={repoAptVersionExample}</code>{" "}
fails — always take the string the list command prints.
</p>
<div>
<p className="mb-1 text-xs font-semibold text-gray-400">APT — list then pin</p>
<div className="rounded-md border border-neutral-700 bg-black p-3">
<code className="text-xs text-green-400 sm:text-sm">
apt-cache madison postgresql-18-documentdb
apt-cache madison {selectedPackageNames}
</code>
</div>
<div className="mt-2 rounded-md border border-neutral-700 bg-black p-3">
<code className="text-xs text-green-400 sm:text-sm">
sudo apt install postgresql-18-documentdb=&lt;VERSION&gt;
sudo apt install {selectedPackageNames}=&lt;VERSION&gt;
</code>
</div>
</div>
<div>
<p className="mb-1 text-xs font-semibold text-gray-400">RPM — list then pin</p>
<div className="rounded-md border border-neutral-700 bg-black p-3">
<code className="text-xs text-green-400 sm:text-sm">
dnf --showduplicates list postgresql18-documentdb
dnf --showduplicates list {selectedPackageNames}
</code>
</div>
<div className="mt-2 rounded-md border border-neutral-700 bg-black p-3">
<code className="text-xs text-green-400 sm:text-sm">
sudo dnf install postgresql18-documentdb-&lt;VERSION&gt;
sudo dnf install {selectedPackageNames}-&lt;VERSION&gt;
</code>
</div>
</div>
Expand Down
53 changes: 43 additions & 10 deletions app/services/articleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ docker run -dt --name documentdb \\
-p 10260:10260 \\
ghcr.io/documentdb/documentdb/documentdb-local:latest \\
--username <YOUR_USERNAME> \\
--password <YOUR_PASSWORD>
--password <YOUR_PASSWORD> \\
--init-data true
\`\`\`

> Replace \`<YOUR_USERNAME>\` and \`<YOUR_PASSWORD>\` 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

Expand Down Expand Up @@ -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\`.
Expand Down Expand Up @@ -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 <YOUR_USERNAME> --password <YOUR_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
Expand Down
35 changes: 34 additions & 1 deletion scripts/generate-sitemap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<html id="__next_error__">` 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 /<html[^>]*\bid=["']__next_error__["']/i.test(html);
}

function xmlEscape(value) {
return value
.replace(/&/g, '&amp;')
Expand All @@ -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({
Expand Down Expand Up @@ -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);
Expand Down