diff --git a/app/ai/page.tsx b/app/ai/page.tsx index 190d948..218d540 100644 --- a/app/ai/page.tsx +++ b/app/ai/page.tsx @@ -8,6 +8,7 @@ export const metadata = getMetadata({ title: "DocumentDB for AI - Vector Search, RAG, and Embeddings", description: "Build AI applications with DocumentDB: a Mongo API-compatible document model, native vector search, PostgreSQL reliability, and self-hosted deployment.", + path: "/ai/", extraKeywords: [ "AI database", "vector search", diff --git a/app/components/Grid.tsx b/app/components/Grid.tsx index 041ab28..fc9c78d 100644 --- a/app/components/Grid.tsx +++ b/app/components/Grid.tsx @@ -16,7 +16,7 @@ export default function Grid({ items }: {

{page.name}

-

+

{page.description}

diff --git a/app/components/Markdown.tsx b/app/components/Markdown.tsx index b547ad7..84d1c72 100644 --- a/app/components/Markdown.tsx +++ b/app/components/Markdown.tsx @@ -244,18 +244,21 @@ function getMarkdownComponents() { ); }, - // Links - a: ({ children, href, ...props }: any) => ( - - {children} - - ), + // Links: only external links open in a new tab; internal links (/docs/..., + // /samples, #anchors) navigate in place + a: ({ children, href, ...props }: any) => { + const isExternal = /^https?:\/\//i.test(href ?? ''); + return ( + + {children} + + ); + }, // Strong text strong: ({ children, ...props }: any) => ( diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index 5ec97c7..9a8087a 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; +import { documentdbDiscordUrl } from "../services/externalLinks"; import { withBasePath } from "../services/sitePath"; type NavItem = { @@ -24,7 +25,7 @@ const navItems: NavItem[] = [ }, { label: "Discord", - href: "https://discord.gg/vH7bYu524D", + href: documentdbDiscordUrl, kind: "anchor", newTab: true, icon: "discord", diff --git a/app/docs/[section]/[[...slug]]/page.tsx b/app/docs/[section]/[[...slug]]/page.tsx index 3e56997..6170f50 100644 --- a/app/docs/[section]/[[...slug]]/page.tsx +++ b/app/docs/[section]/[[...slug]]/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { notFound, redirect } 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"; @@ -48,10 +49,12 @@ export async function generateMetadata({ params }: PageProps) { const selectedNavItem = navigation.find((item) => item.link.includes(file)); const pageTitle = frontmatter.title || selectedNavItem?.title || section; - return { + return getMetadata({ title: `${pageTitle} - DocumentDB Documentation`, - description: frontmatter.description || undefined, - }; + description: frontmatter.description || '', + path: slug.length > 0 ? `/docs/${section}/${slug.join('/')}/` : `/docs/${section}/`, + type: 'article', + }); } export default async function ArticlePage({ params }: PageProps) { @@ -80,6 +83,34 @@ export default async function ArticlePage({ params }: PageProps) { // Use title from frontmatter if available, otherwise fall back to navigation title or section name const pageTitle = frontmatter.title || selectedNavItem?.title || section; const showInstallPrimer = section === "getting-started" && file === "index"; + const sectionTitle = capitalCase(section) + .replace(/documentdb/i, 'DocumentDB') + .replace(/api/i, 'API'); + + // Rendered once, shown in the desktop sidebar and the mobile disclosure + const navigationLinks = navigation.map((item) => { + // Better matching logic for active state + // For index files, match both /section and /section/index + // For other files, match the specific file name + const itemPath = item.link.replace('/docs/', ''); + const currentPath = file === 'index' ? section : `${section}/${file}`; + const isActive = itemPath === currentPath || + (file === 'index' && itemPath === `${section}/index`) || + (item.link.includes(file) && file !== 'index'); + + return ( + + {item.title} + + ); + }); return (
@@ -94,8 +125,8 @@ export default async function ArticlePage({ params }: PageProps) {
- {/* Left Sidebar */} -
+ {/* Left Sidebar (desktop only; mobile gets the disclosure below) */} +
{/* Header */}
Back to Documentation -

- { - capitalCase(section) - .replace(/documentdb/i, 'DocumentDB') - .replace(/api/i, 'API') - } -

+ {/* Not a heading: the article's h1 comes from the markdown content */} +

+ {sectionTitle} +

{/* Menu Items */}
{/* Main Content */} -
+
+ {/* Mobile section navigation */} +
+ + {sectionTitle} navigation + +
+ + ← Back to Documentation + + +
+
+ {/* Coming Soon Component for coming-soon layout */} {frontmatter.layout === 'coming-soon' && } diff --git a/app/docs/page.tsx b/app/docs/page.tsx index eb3fb05..733f554 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -1,5 +1,15 @@ import Link from "next/link"; import { getArticleContent } from "../services/articleService"; +import { getMetadata } from "../services/metadataService"; + +export function generateMetadata() { + const articleContent = getArticleContent(); + return getMetadata({ + title: `${articleContent.landing.title} - DocumentDB`, + description: articleContent.landing.description, + path: '/docs/', + }); +} export default function Docs() { const articleContent = getArticleContent(); diff --git a/app/docs/reference/[type]/[category]/[name]/page.tsx b/app/docs/reference/[type]/[category]/[name]/page.tsx index a677e77..6ec0b7b 100644 --- a/app/docs/reference/[type]/[category]/[name]/page.tsx +++ b/app/docs/reference/[type]/[category]/[name]/page.tsx @@ -20,6 +20,8 @@ export async function generateMetadata({ params }: { params: Promise<{ type: str return getMetadata({ title: `${data.frontmatter.title || name} - DocumentDB MQL Reference`, description: data.frontmatter.description || '', + path: `/docs/reference/${type}/${category}/${name}/`, + type: 'article', extraKeywords: ['reference', type, category, name] }); } diff --git a/app/docs/reference/[type]/[category]/page.tsx b/app/docs/reference/[type]/[category]/page.tsx index 6ec5618..823d1ee 100644 --- a/app/docs/reference/[type]/[category]/page.tsx +++ b/app/docs/reference/[type]/[category]/page.tsx @@ -17,8 +17,9 @@ export async function generateMetadata({ params }: { params: Promise<{ type: str const title = `${capitalCase(category)} ${capitalCase(pluralize(type))}`; const description = getCategoryDescription(type, category); return getMetadata({ - title: `${title} - DocumentDB MQL Reference`, + title: `${title} - DocumentDB MQL Reference`, description: await sanitizeMarkdown(description), + path: `/docs/reference/${type}/${category}/`, extraKeywords: ['reference', type, category] }); } @@ -38,9 +39,9 @@ export default async function CommandReferencePage({ params }: { params: Promise
-

+

MongoDB Query Language (MQL) {capitalCase(category)} {capitalCase(pluralize(type))} -

+
{description && (
diff --git a/app/docs/reference/[type]/page.tsx b/app/docs/reference/[type]/page.tsx index 3cb97e4..504a475 100644 --- a/app/docs/reference/[type]/page.tsx +++ b/app/docs/reference/[type]/page.tsx @@ -20,8 +20,9 @@ export async function generateMetadata({ params }: { params: Promise<{ type: str const title = capitalCase(pluralize(type)); const description = getTypeDescription(type); return getMetadata({ - title: `${title} - DocumentDB MQL Reference`, + title: `${title} - DocumentDB MQL Reference`, description: await sanitizeMarkdown(description), + path: `/docs/reference/${type}/`, extraKeywords: ['reference', type] }); } @@ -37,9 +38,9 @@ export default async function CommandReferencePage({ params }: { params: Promise
-

+

MongoDB Query Language (MQL) {capitalCase(pluralize(type))} -

+
{description && (
diff --git a/app/docs/reference/layout.tsx b/app/docs/reference/layout.tsx index 679440f..fd2abce 100644 --- a/app/docs/reference/layout.tsx +++ b/app/docs/reference/layout.tsx @@ -29,7 +29,8 @@ export default function ReferenceLayout({ >
-
+ {/* Sidebar (desktop only; mobile gets the disclosure below) */} +
-
+
+ {/* Mobile reference navigation */} +
+ + MQL reference navigation + +
+ +
+
{children}
diff --git a/app/docs/reference/page.tsx b/app/docs/reference/page.tsx index 3ff1963..e9d2670 100644 --- a/app/docs/reference/page.tsx +++ b/app/docs/reference/page.tsx @@ -12,6 +12,7 @@ export async function generateMetadata() { return getMetadata({ title: 'DocumentDB MQL Reference', description: await sanitizeMarkdown(description), + path: '/docs/reference/', extraKeywords: ['reference'] }); } @@ -23,9 +24,9 @@ export default function Home() {
-

+

MongoDB Query Language (MQL) -

+
{description && (
diff --git a/app/kubernetes-operator/page.tsx b/app/kubernetes-operator/page.tsx index 9fc9c4b..78521e5 100644 --- a/app/kubernetes-operator/page.tsx +++ b/app/kubernetes-operator/page.tsx @@ -235,6 +235,7 @@ export const metadata = getMetadata({ title: "DocumentDB Kubernetes Operator", description: "Run DocumentDB on Kubernetes with a PostgreSQL foundation, replication, HA, backups, TLS, and hybrid or multi-cloud topologies.", + path: "/kubernetes-operator/", extraKeywords: [ "Kubernetes", "operator", diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..cfe6ef2 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; + +const suggestions = [ + { + title: "Documentation", + description: "Guides, quick starts, and the MQL reference.", + href: "/docs", + }, + { + title: "Download", + description: "Docker, APT, and RPM install commands.", + href: "/packages", + }, + { + title: "Samples", + description: "Ready-to-run example applications.", + href: "/samples", + }, +] as const; + +export default function NotFound() { + return ( +
+

+ 404 - Page not found +

+

+ This page doesn't exist +

+

+ The page you're looking for may have moved. These are the most + useful places to continue from. +

+ +
+ {suggestions.map((item) => ( + +

+ {item.title} +

+

{item.description}

+ + ))} +
+ +
+ + Back to home + + + Report a broken link + +
+
+ ); +} diff --git a/app/packages/layout.tsx b/app/packages/layout.tsx new file mode 100644 index 0000000..0e1daad --- /dev/null +++ b/app/packages/layout.tsx @@ -0,0 +1,18 @@ +import { getMetadata } from "../services/metadataService"; + +// The packages page is a client component, so its metadata lives here. +export const metadata = getMetadata({ + title: "Download DocumentDB - Docker, APT, and RPM Packages", + description: + "Run DocumentDB locally with Docker or install the PostgreSQL extension from GPG-signed APT and RPM packages for Ubuntu, Debian, and RHEL-compatible systems.", + path: "/packages/", + extraKeywords: ["download", "install", "Docker", "APT", "RPM", "Debian", "Ubuntu", "RHEL"], +}); + +export default function PackagesLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return children; +} diff --git a/app/page.tsx b/app/page.tsx index 60f0f33..fcbb05c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,8 +2,15 @@ import Image from "next/image"; import Link from "next/link"; import CommandSnippet from "./components/CommandSnippet"; import { documentdbKubernetesOperatorDocsUrl } from "./services/externalLinks"; +import { getMetadata } from "./services/metadataService"; import { withBasePath } from "./services/sitePath"; +export const metadata = getMetadata({ + title: 'DocumentDB - Open Source Document Database', + description: 'A powerful, scalable open-source document database solution built on PostgreSQL for modern applications.', + path: '/', +}); + type CapabilityIconName = | "foundation" | "index" diff --git a/app/samples/layout.tsx b/app/samples/layout.tsx new file mode 100644 index 0000000..0191f09 --- /dev/null +++ b/app/samples/layout.tsx @@ -0,0 +1,18 @@ +import { getMetadata } from "../services/metadataService"; + +// The samples page is a client component, so its metadata lives here. +export const metadata = getMetadata({ + title: "DocumentDB Samples Gallery", + description: + "Ready-to-run DocumentDB code samples: vector search, RAG, semantic search, and full-stack applications in Python, Node.js, and TypeScript.", + path: "/samples/", + extraKeywords: ["samples", "examples", "vector search", "RAG", "Python", "Node.js", "TypeScript"], +}); + +export default function SamplesLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return children; +} diff --git a/app/services/articleService.ts b/app/services/articleService.ts index 4fa0a52..d8fdc59 100644 --- a/app/services/articleService.ts +++ b/app/services/articleService.ts @@ -5,6 +5,7 @@ import matter from 'gray-matter'; import { Article } from '../types/Article'; import { Link } from '../types/Link'; import { buildAptInstallCommand, buildRpmInstallCommand } from '../lib/packageInstall'; +import { documentdbDiscordUrl } from './externalLinks'; const articlesDirectory = path.join(process.cwd(), 'articles'); const dockerGuideContent = `# Docker Quick Start @@ -372,7 +373,7 @@ For extension-specific help or bugs: - [GitHub repository](https://github.com/microsoft/vscode-documentdb) - [GitHub discussions](https://github.com/microsoft/vscode-documentdb/discussions) - [GitHub issues](https://github.com/microsoft/vscode-documentdb/issues) -- [DocumentDB Discord](https://discord.gg/vH7bYu524D) +- [DocumentDB Discord](${documentdbDiscordUrl}) ## Next steps diff --git a/app/services/externalLinks.ts b/app/services/externalLinks.ts index eab069c..bc7b4c6 100644 --- a/app/services/externalLinks.ts +++ b/app/services/externalLinks.ts @@ -1,3 +1,5 @@ +export const documentdbDiscordUrl = 'https://discord.gg/vH7bYu524D'; + export const documentdbKubernetesOperatorDocsUrl = 'https://documentdb.io/documentdb-kubernetes-operator/preview/'; diff --git a/app/services/metadataService.ts b/app/services/metadataService.ts index 5cbe99a..b8bbfc3 100644 --- a/app/services/metadataService.ts +++ b/app/services/metadataService.ts @@ -16,17 +16,32 @@ export const sanitizeMarkdown = async (markdown: string | undefined): Promise ({ +export const siteUrl = 'https://documentdb.io'; + +export const getMetadata = ({ title, description, path, type = 'website', extraKeywords = [] }: { + title: string, + description: string, + /** + * Site-relative path of the page (e.g. '/docs/reference/'), used as the + * canonical URL. Pages without a known path get no canonical tag. + */ + path?: string, + /** Open Graph object type; use 'article' for docs/reference content pages. */ + type?: 'website' | 'article', + extraKeywords?: string[], +}): Metadata => ({ + metadataBase: new URL(siteUrl), keywords: [...getBaseKeywords(), ...extraKeywords], title, description, + ...(path ? { alternates: { canonical: path } } : {}), openGraph: { - type: 'article', + type, title, description, images: [ { - url: 'https://documentdb.io/images/social-card.png', + url: `${siteUrl}/images/social-card.png`, } ] }, @@ -36,7 +51,7 @@ export const getMetadata = ({ title, description, extraKeywords = [] }: { title: description, images: [ { - url: 'https://documentdb.io/images/social-card.png', + url: `${siteUrl}/images/social-card.png`, } ] }, diff --git a/blogs/_config.yml b/blogs/_config.yml index f9503d1..fe6661a 100644 --- a/blogs/_config.yml +++ b/blogs/_config.yml @@ -1,7 +1,11 @@ title: DocumentDB Blog description: Insights, updates, and deep dives into the world of document databases and open-source innovation. +# Shared external links. The Next.js app keeps the same values in +# app/services/externalLinks.ts; update both when one changes. +discord_url: https://discord.gg/vH7bYu524D baseurl: /blogs -url: "" +# Absolute host for og:url, og:image, and the Atom feed's permanent IDs. +url: "https://documentdb.io" permalink: pretty markdown: kramdown kramdown: diff --git a/blogs/_layouts/default.html b/blogs/_layouts/default.html index 5d49624..f786f96 100644 --- a/blogs/_layouts/default.html +++ b/blogs/_layouts/default.html @@ -6,6 +6,33 @@ {% if page.title %}{{ page.title }} · {% endif %}DocumentDB + + {% assign social_title = page.title | default: site.title %} + {% assign social_description = page.description | default: site.description %} + {% if page.cover_image %} + {% if page.cover_image contains '://' %} + {% assign social_image = page.cover_image %} + {% else %} + {% assign social_image = page.cover_image | absolute_url %} + {% endif %} + {% else %} + {% assign social_image = site.url | append: '/images/social-card.png' %} + {% endif %} + + + + + + + {% if page.date %} + + {% endif %} + + + + + + @@ -31,7 +58,7 @@