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
40 changes: 32 additions & 8 deletions app/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import Link from "next/link";
import { useMemo } from 'react';
import type { ReactElement } from 'react';
import Code from './Code';
import { kebabCase } from 'change-case';
import { resolveMarkdownLink } from '../lib/markdownLinks';

export default function Markdown({ content }: { content: string }) {
interface MarkdownProps {
content: string;
sourcePath: string;
}

export default function Markdown({ content, sourcePath }: MarkdownProps) {
const processedContent = useMemo(() => {
// Split content by H2 headings to group sections
const sections = content.split(/^## /gm);
Expand All @@ -22,7 +29,7 @@ export default function Markdown({ content }: { content: string }) {
<div key={`intro-${index}`} className="mb-8">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={getMarkdownComponents()}
components={getMarkdownComponents(sourcePath)}
>
{section}
</ReactMarkdown>
Expand All @@ -42,7 +49,7 @@ export default function Markdown({ content }: { content: string }) {
</h2>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={getMarkdownComponents()}
components={getMarkdownComponents(sourcePath)}
>
{sectionContent}
</ReactMarkdown>
Expand All @@ -52,12 +59,12 @@ export default function Markdown({ content }: { content: string }) {
});

return elements;
}, [content]);
}, [content, sourcePath]);

return <div className="space-y-8">{processedContent}</div>;
}

function getMarkdownComponents() {
function getMarkdownComponents(sourcePath: string) {
return {
// H1 headings (main page title from Markdown content)
h1: ({ children, ...props }: any) => (
Expand Down Expand Up @@ -247,11 +254,28 @@ function getMarkdownComponents() {
// 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 ?? '');
const resolvedHref = resolveMarkdownLink(href, sourcePath);
const isExternal = /^https?:\/\//i.test(resolvedHref ?? '');
const linkClassName = 'text-blue-400 hover:text-blue-300 transition-colors';

// A rewritten href is a resolved document route, so it is known to be an
// internal page and is rendered with next/link. That applies the basePath
// from next.config identically on the server and in the browser -
// NEXT_BASE_PATH is not readable from the client bundle, so neither this
// component nor the resolver may prefix the path itself. Any other href
// is left exactly as the author wrote it.
if (resolvedHref && resolvedHref !== href) {
return (
<Link href={resolvedHref} className={linkClassName} {...props}>
{children}
</Link>
);
}

return (
<a
href={href}
className="text-blue-400 hover:text-blue-300 transition-colors"
href={resolvedHref}
className={linkClassName}
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
{...props}
>
Expand Down
5 changes: 4 additions & 1 deletion app/docs/[section]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,10 @@ export default async function ArticlePage({ params }: PageProps) {
)}

{/* Markdown Content */}
<Markdown content={content} />
<Markdown
content={content}
sourcePath={`/docs/${section}/${file}.md`}
/>
</div>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/docs/reference/[type]/[category]/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export default async function ReferencePage({ params }: { params: Promise<{ type
<Breadcrumb type={type} category={category} name={pageTitle} />

{/* Markdown Content */}
<Markdown content={data.content} />
<Markdown
content={data.content}
sourcePath={`/docs/reference/${type}/${category}/${decodedName}.md`}
/>
</article>
);
}
5 changes: 4 additions & 1 deletion app/docs/reference/[type]/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export default async function CommandReferencePage({ params }: { params: Promise
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-green-500 rounded-full mb-6"></div>
{description && (
<div className="text-gray-400 text-lg mb-6">
<Markdown content={description} />
<Markdown
content={description}
sourcePath={`/docs/reference/${type}/${category}/_metadata.description.md`}
/>
</div>
)}
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/docs/reference/[type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export default async function CommandReferencePage({ params }: { params: Promise
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-green-500 rounded-full mb-6"></div>
{description && (
<div className="text-gray-400 text-lg mb-6">
<Markdown content={description} />
<Markdown
content={description}
sourcePath={`/docs/reference/${type}/_metadata.description.md`}
/>
</div>
)}
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/docs/reference/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export default function Home() {
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-green-500 rounded-full mb-6"></div>
{description && (
<div className="text-gray-400 text-lg mb-6">
<Markdown content={description} />
<Markdown
content={description}
sourcePath="/docs/reference/_metadata.description.md"
/>
</div>
)}
</div>
Expand Down
71 changes: 71 additions & 0 deletions app/lib/markdownLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Deliberately free of any base-path handling. This module is reachable from a
// client component, and NEXT_BASE_PATH is not NEXT_PUBLIC_-prefixed, so its
// value is stripped from the browser bundle: reading it here would produce a
// prefixed href during the static render and an unprefixed one after
// hydration. The route returned below is base-path-relative, and the caller
// renders it with next/link, which applies the basePath configured in
// next.config on both the server and the client.
const markdownOrigin = 'https://documentdb.invalid';

const docsRoot = '/docs/';

// content.config.json publishes the docs repository's api-reference/ folder at
// /docs/reference. Authors write links against the source layout they can see,
// so a cross-section link naming api-reference has to be mapped onto the
// section the site actually serves. Every other mapping keeps its folder name.
const publishedSectionBySourceFolder: Record<string, string> = {
'api-reference': 'reference',
};

function applySectionMapping(pathname: string): string {
if (!pathname.startsWith(docsRoot)) {
return pathname;
}

const rest = pathname.slice(docsRoot.length);
const separatorIndex = rest.indexOf('/');
const section = separatorIndex === -1 ? rest : rest.slice(0, separatorIndex);
const published = publishedSectionBySourceFolder[section];

if (!published) {
return pathname;
}

const remainder = separatorIndex === -1 ? '' : rest.slice(separatorIndex);
return `${docsRoot}${published}${remainder}`;
}

export function resolveMarkdownLink(
href: string | undefined,
sourcePath: string,
): string | undefined {
if (!href) {
return href;
}

if (/^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith('//')) {
return href;
}

const hrefPath = href.split(/[?#]/, 1)[0];
if (!hrefPath.toLowerCase().endsWith('.md')) {
return href;
}

const normalizedSourcePath = sourcePath.startsWith('/')
? sourcePath
: `/${sourcePath}`;
// URL applies the same dot-segment rules as a browser without coupling the
// result to the page URL that happens to render this source file.
const target = new URL(href, new URL(normalizedSourcePath, markdownOrigin));

if (target.pathname.toLowerCase().endsWith('/index.md')) {
target.pathname = target.pathname.slice(0, -'index.md'.length);
} else {
target.pathname = `${target.pathname.slice(0, -'.md'.length)}/`;
}

const publishedPathname = applySectionMapping(target.pathname);

return `${publishedPathname}${target.search}${target.hash}`;
}
Loading