From 04b86beec04ec12e8f18f906465c6628d05c5210 Mon Sep 17 00:00:00 2001 From: BittuBarnwal7479 Date: Thu, 2 Jul 2026 16:53:22 +0530 Subject: [PATCH 1/4] B --- docs-site/app/docs/[[...slug]]/page.tsx | 49 +++++++++- docs-site/components/docs-page-actions.tsx | 52 ++++++++--- docs-site/lib/docs-markdown.ts | 9 +- docs-site/tests/docs-index-route.test.mjs | 100 ++++++++++++++++++--- 4 files changed, 183 insertions(+), 27 deletions(-) diff --git a/docs-site/app/docs/[[...slug]]/page.tsx b/docs-site/app/docs/[[...slug]]/page.tsx index e0677c9e9..94fd06fc5 100644 --- a/docs-site/app/docs/[[...slug]]/page.tsx +++ b/docs-site/app/docs/[[...slug]]/page.tsx @@ -5,11 +5,16 @@ import { DocsTitle, DocsDescription, } from "fumadocs-ui/page"; +import { PageFooter } from "fumadocs-ui/layouts/docs/page"; import { notFound, redirect } from "next/navigation"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { CodeBlock } from "@/components/code-block"; import { DocsPageActions } from "@/components/docs-page-actions"; -import { readDocsPageMarkdown } from "@/lib/docs-markdown"; +import { + readDocsPageMarkdown, + resolveDocsPageMarkdownPath, +} from "@/lib/docs-markdown"; +import { relative } from "node:path"; const docsIndexPath = "/docs/getting-started/introduction"; const docsIndexSlug = ["getting-started", "introduction"] as const; @@ -22,6 +27,33 @@ function isHeroPage(slug: string[] | undefined) { return slug?.join("/") === "getting-started/introduction"; } +function toRepositoryPath(sourcePath: string) { + return `docs-site/${relative(process.cwd(), sourcePath).replaceAll("\\", "/")}`; +} + +function buildSourceEditUrl(sourcePath: string) { + return `https://github.com/Kaelio/ktx/edit/main/${toRepositoryPath(sourcePath)}`; +} + +function buildIssueUrl(pageTitle: string, sourcePath: string, pageUrl: string) { + const title = `[docs] ${pageTitle}`; + const body = [ + `Documentation page: ${pageUrl}`, + `Source file: ${toRepositoryPath(sourcePath)}`, + "", + "What should change?", + "", + ].join("\n"); + + const params = new URLSearchParams({ + template: "bug_report.yml", + title, + body, + }); + + return `https://github.com/Kaelio/ktx/issues/new?${params.toString()}`; +} + export default async function Page(props: { params: Promise<{ slug?: string[] }>; }) { @@ -35,6 +67,8 @@ export default async function Page(props: { const MDX = page.data.body; const mdxSource = await readDocsPageMarkdown(page.slugs); + const sourcePath = await resolveDocsPageMarkdownPath(page.slugs); + const pageUrl = `https://docs.kaelio.com/ktx/docs/${page.slugs.join("/")}`; const hero = isHeroPage(params.slug); @@ -42,6 +76,19 @@ export default async function Page(props: { +
+ +
+ + + ), + }} style={{ width: "calc(100vw - 2rem)", maxWidth: "900px", diff --git a/docs-site/components/docs-page-actions.tsx b/docs-site/components/docs-page-actions.tsx index ce85aca05..754358e90 100644 --- a/docs-site/components/docs-page-actions.tsx +++ b/docs-site/components/docs-page-actions.tsx @@ -1,19 +1,27 @@ "use client"; import { useState } from "react"; +import { CircleAlert, PencilLine } from "lucide-react"; type Props = { - mdxSource: string; + mdxSource?: string; + issueUrl?: string; + sourceEditUrl?: string; }; function stripFrontmatter(source: string) { return source.trim().replace(/^---\n[\s\S]*?\n---\n?/, "").trim(); } -export function DocsPageActions({ mdxSource }: Props) { +const actionClassName = + "inline-flex h-8 items-center gap-1.5 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-muted-foreground transition-colors hover:border-fd-primary/40 hover:text-fd-foreground"; + +export function DocsPageActions({ mdxSource, issueUrl, sourceEditUrl }: Props) { const [copied, setCopied] = useState(false); const onCopy = async () => { + if (mdxSource === undefined) return; + try { await navigator.clipboard.writeText(stripFrontmatter(mdxSource)); setCopied(true); @@ -25,14 +33,38 @@ export function DocsPageActions({ mdxSource }: Props) { return (
- + {mdxSource !== undefined && ( + + )} + {sourceEditUrl !== undefined && ( + + + )} + {issueUrl !== undefined && ( + + + )}
); } diff --git a/docs-site/lib/docs-markdown.ts b/docs-site/lib/docs-markdown.ts index 603126277..19aa4b87c 100644 --- a/docs-site/lib/docs-markdown.ts +++ b/docs-site/lib/docs-markdown.ts @@ -2,6 +2,10 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; export async function readDocsPageMarkdown(slugs: string[]) { + return readFile(await resolveDocsPageMarkdownPath(slugs), "utf8"); +} + +export async function resolveDocsPageMarkdownPath(slugs: string[]) { if ( slugs.length === 0 || slugs.some((segment) => segment.includes("/") || segment.includes("..")) @@ -13,14 +17,15 @@ export async function readDocsPageMarkdown(slugs: string[]) { const directPath = join(docsRoot, `${slugs.join("/")}.mdx`); try { - return await readFile(directPath, "utf8"); + await readFile(directPath, "utf8"); + return directPath; } catch (error) { if (!isNotFoundError(error)) { throw error; } } - return readFile(join(docsRoot, slugs.join("/"), "index.mdx"), "utf8"); + return join(docsRoot, slugs.join("/"), "index.mdx"); } function isNotFoundError(error: unknown) { diff --git a/docs-site/tests/docs-index-route.test.mjs b/docs-site/tests/docs-index-route.test.mjs index e2ab24f0c..e7a44c80e 100644 --- a/docs-site/tests/docs-index-route.test.mjs +++ b/docs-site/tests/docs-index-route.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { once } from "node:events"; -import { readFile, writeFile } from "node:fs/promises"; +import { readdir, readFile, writeFile } from "node:fs/promises"; import http from "node:http"; import https from "node:https"; import { dirname, join } from "node:path"; @@ -17,6 +17,8 @@ let docsServer; let docsServerOutput = ""; let nextEnvPath; let nextEnvContents; +const docsSiteDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const pnpmExecPath = process.env.npm_execpath; async function getAvailablePort() { const server = createServer(); @@ -64,30 +66,65 @@ before(async () => { return; } - const docsSiteDir = join( - dirname(fileURLToPath(import.meta.url)), - "..", - ); nextEnvPath = join(docsSiteDir, "next-env.d.ts"); nextEnvContents = await readFile(nextEnvPath, "utf8"); const port = await getAvailablePort(); docsSiteUrl = `http://127.0.0.1:${port}`; - docsServer = spawn( - "pnpm", - ["exec", "next", "dev", "--hostname", "127.0.0.1", "--port", `${port}`], - { - cwd: docsSiteDir, - env: { ...process.env, NEXT_TELEMETRY_DISABLED: "1" }, - stdio: ["ignore", "pipe", "pipe"], - }, - ); + const command = pnpmExecPath === undefined ? "pnpm" : process.execPath; + const args = + pnpmExecPath === undefined + ? ["exec", "next", "dev", "--hostname", "127.0.0.1", "--port", `${port}`] + : [ + pnpmExecPath, + "exec", + "next", + "dev", + "--hostname", + "127.0.0.1", + "--port", + `${port}`, + ]; + + docsServer = spawn(command, args, { + cwd: docsSiteDir, + env: { ...process.env, NEXT_TELEMETRY_DISABLED: "1" }, + stdio: ["ignore", "pipe", "pipe"], + }); docsServer.stdout.on("data", appendDocsServerOutput); docsServer.stderr.on("data", appendDocsServerOutput); await waitForDocsServer(); }); +async function listDocsPages(dir = join(docsSiteDir, "content", "docs")) { + const entries = await readdir(dir, { withFileTypes: true }); + const pages = await Promise.all( + entries.map(async (entry) => { + const entryPath = join(dir, entry.name); + + if (entry.isDirectory()) { + return listDocsPages(entryPath); + } + + if (!entry.isFile() || !entry.name.endsWith(".mdx")) { + return []; + } + + const sourcePath = entryPath + .slice(docsSiteDir.length + 1) + .replaceAll("\\", "/"); + const routePath = sourcePath + .replace(/^content\/docs\//, "") + .replace(/(?:\/index)?\.mdx$/, ""); + + return [{ routePath, sourcePath: `docs-site/${sourcePath}` }]; + }), + ); + + return pages.flat(); +} + after(async () => { if (docsServer && docsServer.exitCode === null) { docsServer.kill("SIGTERM"); @@ -222,6 +259,41 @@ test("/ktx/api/search returns docs search results", async () => { ); }); +test("docs pages render source edit and issue actions", async () => { + const pages = await listDocsPages(); + assert.ok(pages.length > 0, "docs pages should be discovered from content"); + + const sampledPages = [ + pages.find((page) => page.routePath === "getting-started/introduction"), + pages.find((page) => page.routePath === "cli-reference/ktx"), + ]; + + for (const page of sampledPages) { + assert.ok(page, "sample docs page should exist"); + + const response = await fetch( + `${docsSiteUrl}${docsBasePath}/docs/${page.routePath}`, + ); + assert.equal(response.status, 200, page.routePath); + + const html = await response.text(); + assert.match(html, />Suggest editsRaise issue { const root = await requestWithHost("ktx.sh", "/"); assert.equal(root.status, 308); From 6eac594372c4479b9dc5a31c8d855c004ce572c2 Mon Sep 17 00:00:00 2001 From: BittuBarnwal7479 Date: Fri, 3 Jul 2026 16:45:47 +0530 Subject: [PATCH 2/4] feat: add documentation feedback form and enhance page actions --- .github/ISSUE_TEMPLATE/docs_feedback.yml | 31 ++++++++ docs-site/app/docs/[[...slug]]/page.tsx | 89 +++++++++------------- docs-site/components/docs-page-actions.tsx | 58 +++++++++++++- docs-site/components/docs-page-footer.tsx | 46 +++++++++++ docs-site/lib/docs-markdown.ts | 10 +-- docs-site/lib/llm-docs.ts | 4 +- docs-site/tests/docs-index-route.test.mjs | 46 +++++++---- 7 files changed, 209 insertions(+), 75 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/docs_feedback.yml create mode 100644 docs-site/components/docs-page-footer.tsx diff --git a/.github/ISSUE_TEMPLATE/docs_feedback.yml b/.github/ISSUE_TEMPLATE/docs_feedback.yml new file mode 100644 index 000000000..f97c87668 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_feedback.yml @@ -0,0 +1,31 @@ +name: Docs feedback +description: Report a docs typo, gap, or confusing page +title: "[docs] " +labels: ["docs"] +body: + - type: markdown + attributes: + value: | + Use this form for documentation fixes, confusing examples, stale commands, or missing setup details. + - type: input + id: page + attributes: + label: Documentation page + description: The page where you found the issue. + validations: + required: true + - type: input + id: source + attributes: + label: Source file + description: The docs source file, if known. + validations: + required: false + - type: textarea + id: feedback + attributes: + label: What should change? + description: Tell us what is wrong, missing, stale, or confusing. + placeholder: This page says X, but it should say Y. + validations: + required: true diff --git a/docs-site/app/docs/[[...slug]]/page.tsx b/docs-site/app/docs/[[...slug]]/page.tsx index 94fd06fc5..7a6409db0 100644 --- a/docs-site/app/docs/[[...slug]]/page.tsx +++ b/docs-site/app/docs/[[...slug]]/page.tsx @@ -5,15 +5,15 @@ import { DocsTitle, DocsDescription, } from "fumadocs-ui/page"; -import { PageFooter } from "fumadocs-ui/layouts/docs/page"; import { notFound, redirect } from "next/navigation"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { CodeBlock } from "@/components/code-block"; -import { DocsPageActions } from "@/components/docs-page-actions"; import { - readDocsPageMarkdown, - resolveDocsPageMarkdownPath, -} from "@/lib/docs-markdown"; + DocsPageFooter, + DocsPageFooterProvider, +} from "@/components/docs-page-footer"; +import { readDocsPageMarkdownFile } from "@/lib/docs-markdown"; +import { absoluteUrl } from "@/lib/llm-docs"; import { relative } from "node:path"; const docsIndexPath = "/docs/getting-started/introduction"; @@ -37,18 +37,13 @@ function buildSourceEditUrl(sourcePath: string) { function buildIssueUrl(pageTitle: string, sourcePath: string, pageUrl: string) { const title = `[docs] ${pageTitle}`; - const body = [ - `Documentation page: ${pageUrl}`, - `Source file: ${toRepositoryPath(sourcePath)}`, - "", - "What should change?", - "", - ].join("\n"); + const repositoryPath = toRepositoryPath(sourcePath); const params = new URLSearchParams({ - template: "bug_report.yml", + template: "docs_feedback.yml", title, - body, + page: pageUrl, + source: repositoryPath, }); return `https://github.com/Kaelio/ktx/issues/new?${params.toString()}`; @@ -66,49 +61,41 @@ export default async function Page(props: { if (!page) notFound(); const MDX = page.data.body; - const mdxSource = await readDocsPageMarkdown(page.slugs); - const sourcePath = await resolveDocsPageMarkdownPath(page.slugs); - const pageUrl = `https://docs.kaelio.com/ktx/docs/${page.slugs.join("/")}`; - + const { content: mdxSource, path: sourcePath } = + await readDocsPageMarkdownFile(page.slugs); + const pageUrl = absoluteUrl(page.url); const hero = isHeroPage(params.slug); return ( - -
- -
- - - ), - }} - style={{ - width: "calc(100vw - 2rem)", - maxWidth: "900px", + - {!hero && ( - <> -
+ + {!hero && ( + <> {page.data.title} - -
- - {page.data.description} - - - )} - - - -
+ + {page.data.description} + + + )} + + + +
+ ); } diff --git a/docs-site/components/docs-page-actions.tsx b/docs-site/components/docs-page-actions.tsx index 754358e90..f308b2497 100644 --- a/docs-site/components/docs-page-actions.tsx +++ b/docs-site/components/docs-page-actions.tsx @@ -1,7 +1,6 @@ "use client"; -import { useState } from "react"; -import { CircleAlert, PencilLine } from "lucide-react"; +import { useState, type SVGProps } from "react"; type Props = { mdxSource?: string; @@ -13,6 +12,56 @@ function stripFrontmatter(source: string) { return source.trim().replace(/^---\n[\s\S]*?\n---\n?/, "").trim(); } +function CopyIcon(props: SVGProps) { + return ( + + + + + ); +} + +function EditIcon(props: SVGProps) { + return ( + + + + + ); +} + +function MessageIcon(props: SVGProps) { + return ( + + + + ); +} + const actionClassName = "inline-flex h-8 items-center gap-1.5 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-muted-foreground transition-colors hover:border-fd-primary/40 hover:text-fd-foreground"; @@ -40,6 +89,7 @@ export function DocsPageActions({ mdxSource, issueUrl, sourceEditUrl }: Props) { className={`${actionClassName} data-[state=copied]:border-emerald-500/40 data-[state=copied]:text-emerald-600`} data-state={copied ? "copied" : "idle"} > +