Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/scripts/update-example-dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function updateExampleDates() {

// Read the current MDX file
const mdxPath = path.join(
__dirname,
import.meta.dirname,
"../../app/en/resources/examples/page.mdx"
);
let content = fs.readFileSync(mdxPath, "utf8");
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules
.DS_Store
.env.local
public/sitemap*.xml
public/_markdown/
.env
_pagefind/

Expand Down
101 changes: 101 additions & 0 deletions app/_components/copy-page-override.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client";

import { usePathname } from "next/navigation";
import { useCallback, useEffect } from "react";

const COPY_FEEDBACK_DELAY_MS = 2000;
const COPY_BUTTON_TEXT = "Copy page";
const COPIED_TEXT = "Copied";
const DROPDOWN_IDENTIFIER = "Markdown for LLMs";

/**
* This component overrides the default nextra-theme-docs "Copy page" button behavior
* to fetch clean markdown from our API instead of copying raw MDX source.
*/
export function CopyPageOverride() {
const pathname = usePathname();

const fetchAndCopyMarkdown = useCallback(async (): Promise<boolean> => {
try {
const markdownUrl = `/api/markdown${pathname}.md`;
const response = await fetch(markdownUrl);

if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
}

const markdown = await response.text();
await navigator.clipboard.writeText(markdown);
return true;
} catch {
return false;
}
}, [pathname]);

useEffect(() => {
const isCopyButton = (button: HTMLButtonElement): boolean => {
const text = button.textContent || "";
return text.includes(COPY_BUTTON_TEXT) || text.includes(COPIED_TEXT);
};

const updateButtonFeedback = (button: HTMLButtonElement): void => {
const textNodes = button.querySelectorAll("*");
for (const node of textNodes) {
if (node.textContent === COPY_BUTTON_TEXT) {
node.textContent = COPIED_TEXT;
setTimeout(() => {
node.textContent = COPY_BUTTON_TEXT;
}, COPY_FEEDBACK_DELAY_MS);
return;
}
}
};

const handleButtonClick = async (event: MouseEvent): Promise<void> => {
const target = event.target as HTMLElement;
const button = target.closest("button") as HTMLButtonElement | null;

if (!(button && isCopyButton(button))) {
return;
}

event.preventDefault();
event.stopPropagation();

const success = await fetchAndCopyMarkdown();
if (success) {
updateButtonFeedback(button);
}
};

const handleDropdownClick = async (event: MouseEvent): Promise<void> => {
const target = event.target as HTMLElement;
const option = target.closest('[role="option"]');
const optionText = option?.textContent || "";

const isDropdownCopyOption =
optionText.includes(COPY_BUTTON_TEXT) &&
optionText.includes(DROPDOWN_IDENTIFIER);

if (!isDropdownCopyOption) {
return;
}

event.preventDefault();
event.stopPropagation();

await fetchAndCopyMarkdown();
document.body.click();
};

document.addEventListener("click", handleButtonClick, true);
document.addEventListener("click", handleDropdownClick, true);

return () => {
document.removeEventListener("click", handleButtonClick, true);
document.removeEventListener("click", handleDropdownClick, true);
};
}, [fetchAndCopyMarkdown]);

return null;
}
2 changes: 2 additions & 0 deletions app/_components/custom-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type React from "react";
import { CopyPageOverride } from "@/app/_components/copy-page-override";
import { PlaceholderReplacer } from "@/app/_components/placeholder-replacer";
import { OrySessionProvider } from "@/app/_lib/ory-session-context";

Expand All @@ -7,6 +8,7 @@ const CustomLayout: React.FC<{ children: React.ReactNode }> = ({
}) => (
<OrySessionProvider>
<PlaceholderReplacer />
<CopyPageOverride />
<main className="custom-main">{children}</main>
</OrySessionProvider>
);
Expand Down
37 changes: 30 additions & 7 deletions app/api/markdown/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const dynamic = "force-dynamic";
// Regex pattern for removing .md extension
const MD_EXTENSION_REGEX = /\.md$/;

// Directory containing pre-generated clean markdown files
const CLEAN_MARKDOWN_DIR = join(process.cwd(), "public", "_markdown");

export async function GET(
request: NextRequest,
_context: { params: Promise<{ slug?: string[] }> }
Expand All @@ -17,28 +20,48 @@ export async function GET(
// Remove /api/markdown prefix to get the original path
const originalPath = url.pathname.replace("/api/markdown", "");

// Remove .md extension
// Remove .md extension if present
const pathWithoutMd = originalPath.replace(MD_EXTENSION_REGEX, "");

// Map URL to file path
// Try clean markdown first (preferred)
// e.g., /en/home/quickstart -> public/_markdown/en/home/quickstart.md
const cleanMarkdownPath = join(CLEAN_MARKDOWN_DIR, `${pathWithoutMd}.md`);

try {
await access(cleanMarkdownPath);
const content = await readFile(cleanMarkdownPath, "utf-8");

return new NextResponse(content, {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
},
});
} catch {
// Clean markdown not found, fall back to raw MDX
}

// Fallback: serve raw MDX (for backwards compatibility or if clean files not generated)
// e.g., /en/home/quickstart -> app/en/home/quickstart/page.mdx
const filePath = join(process.cwd(), "app", `${pathWithoutMd}/page.mdx`);
const rawMdxPath = join(process.cwd(), "app", `${pathWithoutMd}/page.mdx`);

// Check if file exists
try {
await access(filePath);
await access(rawMdxPath);
} catch {
return new NextResponse("Markdown file not found", { status: 404 });
}

const content = await readFile(filePath, "utf-8");
const content = await readFile(rawMdxPath, "utf-8");

// Return the raw markdown with proper headers
// Return the raw MDX with a warning header
return new NextResponse(content, {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"X-Content-Source": "raw-mdx", // Indicate this is raw MDX, not clean markdown
},
});
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"lint": "pnpm dlx ultracite check",
"format": "pnpm dlx ultracite fix",
"prepare": "husky install",
"postbuild": "pnpm run custompagefind",
"postbuild": "pnpm run generate:markdown && pnpm run custompagefind",
"generate:markdown": "pnpm dlx tsx scripts/generate-clean-markdown.ts",
"translate": "pnpm dlx tsx scripts/i18n-sync/index.ts && pnpm format",
"sync:metas": "pnpm dlx tsx scripts/sync-metas.ts app/en",
"llmstxt": "pnpm dlx tsx scripts/generate-llmstxt.ts",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-syntax-highlighter": "15.5.13",
"@types/turndown": "^5.0.6",
"@types/unist": "3.0.3",
"commander": "14.0.2",
"dotenv": "^17.2.3",
Expand All @@ -90,6 +92,7 @@
"remark": "^15.0.1",
"remark-rehype": "^11.1.2",
"tailwindcss": "4.1.16",
"turndown": "^7.2.2",
"typescript": "5.9.3",
"ultracite": "6.1.0",
"vitest": "4.0.5",
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading