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
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/docs_feedback.yml
Original file line number Diff line number Diff line change
@@ -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
82 changes: 58 additions & 24 deletions docs-site/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
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 {
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";
const docsIndexSlug = ["getting-started", "introduction"] as const;
Expand All @@ -22,6 +27,28 @@ 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 repositoryPath = toRepositoryPath(sourcePath);

const params = new URLSearchParams({
template: "docs_feedback.yml",
title,
page: pageUrl,
source: repositoryPath,
});

return `https://github.com/Kaelio/ktx/issues/new?${params.toString()}`;
}

export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
Expand All @@ -34,34 +61,41 @@ export default async function Page(props: {
if (!page) notFound();

const MDX = page.data.body;
const mdxSource = await readDocsPageMarkdown(page.slugs);

const { content: mdxSource, path: sourcePath } =
await readDocsPageMarkdownFile(page.slugs);
const pageUrl = absoluteUrl(page.url);
const hero = isHeroPage(params.slug);

return (
<DocsPage
toc={page.data.toc}
className="!mx-0 min-w-0 justify-self-start md:!mx-auto"
style={{
width: "calc(100vw - 2rem)",
maxWidth: "900px",
<DocsPageFooterProvider
actions={{
mdxSource,
sourceEditUrl: buildSourceEditUrl(sourcePath),
issueUrl: buildIssueUrl(page.data.title, sourcePath, pageUrl),
}}
>
{!hero && (
<>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
<DocsPage
toc={page.data.toc}
className="!mx-0 min-w-0 justify-self-start md:!mx-auto"
slots={{ footer: DocsPageFooter }}
style={{
width: "calc(100vw - 2rem)",
maxWidth: "900px",
}}
>
{!hero && (
<>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsPageActions mdxSource={mdxSource} />
</div>
<DocsDescription className="wrap-anywhere">
{page.data.description}
</DocsDescription>
</>
)}
<DocsBody className="min-w-0 max-w-full wrap-anywhere">
<MDX components={{ ...defaultMdxComponents, pre: CodeBlock }} />
</DocsBody>
</DocsPage>
<DocsDescription className="wrap-anywhere">
{page.data.description}
</DocsDescription>
</>
)}
<DocsBody className="min-w-0 max-w-full wrap-anywhere">
<MDX components={{ ...defaultMdxComponents, pre: CodeBlock }} />
</DocsBody>
</DocsPage>
</DocsPageFooterProvider>
);
}

Expand Down
104 changes: 93 additions & 11 deletions docs-site/components/docs-page-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,76 @@
"use client";

import { useState } from "react";
import { useState, type SVGProps } from "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) {
function CopyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
);
}

function EditIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
);
}

function MessageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M21 15a4 4 0 0 1-4 4H7l-4 4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z" />
</svg>
);
}

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);
Expand All @@ -25,14 +82,39 @@ export function DocsPageActions({ mdxSource }: Props) {

return (
<div className="not-prose flex flex-wrap items-center gap-2 text-xs">
<button
type="button"
onClick={onCopy}
className="inline-flex h-8 items-center 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 data-[state=copied]:border-emerald-500/40 data-[state=copied]:text-emerald-600"
data-state={copied ? "copied" : "idle"}
>
{copied ? "Copied" : "Copy as Markdown"}
</button>
{mdxSource !== undefined && (
<button
type="button"
onClick={onCopy}
className={`${actionClassName} data-[state=copied]:border-emerald-500/40 data-[state=copied]:text-emerald-600`}
data-state={copied ? "copied" : "idle"}
>
<CopyIcon className="size-3.5" aria-hidden="true" />
{copied ? "Copied" : "Copy as Markdown"}
</button>
)}
{sourceEditUrl !== undefined && (
<a
href={sourceEditUrl}
target="_blank"
rel="noreferrer noopener"
className={actionClassName}
>
<EditIcon className="size-3.5" aria-hidden="true" />
Suggest edits
</a>
)}
{issueUrl !== undefined && (
<a
href={issueUrl}
target="_blank"
rel="noreferrer noopener"
className={actionClassName}
>
<MessageIcon className="size-3.5" aria-hidden="true" />
Raise issue
</a>
)}
</div>
);
}
46 changes: 46 additions & 0 deletions docs-site/components/docs-page-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { createContext, useContext, type ReactNode } from "react";
import { PageFooter, type FooterProps } from "fumadocs-ui/layouts/docs/page";
import { DocsPageActions } from "@/components/docs-page-actions";

type DocsPageFooterActions = {
issueUrl: string;
mdxSource: string;
sourceEditUrl: string;
};

const docsPageFooterContext = createContext<DocsPageFooterActions | null>(null);

export function DocsPageFooterProvider({
actions,
children,
}: {
actions: DocsPageFooterActions;
children: ReactNode;
}) {
return (
<docsPageFooterContext.Provider value={actions}>
{children}
</docsPageFooterContext.Provider>
);
}

export function DocsPageFooter(props: FooterProps) {
const actions = useContext(docsPageFooterContext);

return (
<>
{actions !== null && (
<div className="mt-10 mb-4">
<DocsPageActions
mdxSource={actions.mdxSource}
sourceEditUrl={actions.sourceEditUrl}
issueUrl={actions.issueUrl}
/>
</div>
)}
<PageFooter {...props} />
</>
);
}
9 changes: 7 additions & 2 deletions docs-site/lib/docs-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";

export async function readDocsPageMarkdown(slugs: string[]) {
return (await readDocsPageMarkdownFile(slugs)).content;
}

export async function readDocsPageMarkdownFile(slugs: string[]) {
if (
slugs.length === 0 ||
slugs.some((segment) => segment.includes("/") || segment.includes(".."))
Expand All @@ -13,14 +17,15 @@ export async function readDocsPageMarkdown(slugs: string[]) {
const directPath = join(docsRoot, `${slugs.join("/")}.mdx`);

try {
return await readFile(directPath, "utf8");
return { path: directPath, content: await readFile(directPath, "utf8") };
} catch (error) {
if (!isNotFoundError(error)) {
throw error;
}
}

return readFile(join(docsRoot, slugs.join("/"), "index.mdx"), "utf8");
const indexPath = join(docsRoot, slugs.join("/"), "index.mdx");
return { path: indexPath, content: await readFile(indexPath, "utf8") };
}

function isNotFoundError(error: unknown) {
Expand Down
2 changes: 1 addition & 1 deletion docs-site/lib/llm-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ ${links}`;
.join("\n\n");
}

function absoluteUrl(path: string) {
export function absoluteUrl(path: string) {
return `${siteOrigin}${path}`;
}

Expand Down
Loading
Loading