Repository navigation
feat(docs): AI-friendly docs — llms.txt + Copy-for-AI #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { hookToMarkdown } from "@/lib/hook-markdown"; | ||
| import { HOOKS, getHook } from "@/lib/hooks-registry"; | ||
|
|
||
| /* /docs/<slug>/llms.txt — the machine-readable twin of every hook page: that | ||
| one hook rendered through the shared Markdown renderer. Prerendered for every | ||
| hook alongside its HTML page. */ | ||
| export function generateStaticParams() { | ||
| return HOOKS.map(({ slug }) => ({ slug })); | ||
| } | ||
|
|
||
| export async function GET( | ||
| _request: Request, | ||
| { params }: { params: Promise<{ slug: string }> }, | ||
| ): Promise<Response> { | ||
| const { slug } = await params; | ||
| const hook = getHook(slug); | ||
|
|
||
| if (!hook) { | ||
| return new Response("Not found\n", { | ||
| status: 404, | ||
| headers: { "Content-Type": "text/plain; charset=utf-8" }, | ||
| }); | ||
| } | ||
|
|
||
| return new Response(`${hookToMarkdown(hook)}\n`, { | ||
| headers: { | ||
| "Content-Type": "text/plain; charset=utf-8", | ||
| "Cache-Control": "public, max-age=0, must-revalidate", | ||
| }, | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { buildLlmsFull } from "@/lib/llms"; | ||
|
|
||
| /* /llms-full.txt — the whole library in one Markdown document (every hook via | ||
| the shared renderer). No request input, so it is prerendered at build time. */ | ||
| export const dynamic = "force-static"; | ||
|
|
||
| export function GET(): Response { | ||
| return new Response(buildLlmsFull(), { | ||
| headers: { | ||
| "Content-Type": "text/plain; charset=utf-8", | ||
| "Cache-Control": "public, max-age=0, must-revalidate", | ||
| }, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { buildLlmsIndex } from "@/lib/llms"; | ||
|
|
||
| /* /llms.txt — the concise, llmstxt.org-standard index. No request input, so it | ||
| is prerendered to a static file at build time. */ | ||
| export const dynamic = "force-static"; | ||
|
|
||
| export function GET(): Response { | ||
| return new Response(buildLlmsIndex(), { | ||
| headers: { | ||
| "Content-Type": "text/plain; charset=utf-8", | ||
| "Cache-Control": "public, max-age=0, must-revalidate", | ||
| }, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { CopyButton } from "@/components/copy-button"; | ||
| import { SectionHeading } from "@/components/section-heading"; | ||
| import { BracesIcon, CopyIcon, ExternalLinkIcon } from "@/components/icons"; | ||
| import { buildLlmsIndex } from "@/lib/llms"; | ||
| import { HOOKS } from "@/lib/hooks-registry"; | ||
| import { SITE_URL } from "@/lib/site"; | ||
|
|
||
| /* Landing section that surfaces the AI-friendly surfaces (llms.txt, per-hook | ||
| Copy-for-AI, Open-in-ChatGPT/Claude) so visitors know the docs are built to | ||
| be read by a model, not just a human. Server component: the /llms.txt body | ||
| is built once from the registry and handed to the client CopyButton leaf. */ | ||
|
|
||
| const CARDS = [ | ||
| { | ||
| Icon: BracesIcon, | ||
| title: "One file, the whole library", | ||
| body: ( | ||
| <> | ||
| Point any model at{" "} | ||
| <code className="text-sm">/llms.txt</code> for the map, or{" "} | ||
| <code className="text-sm">/llms-full.txt</code> for every signature, | ||
| usage and source — generated from the same registry the docs use. | ||
| </> | ||
| ), | ||
| }, | ||
| { | ||
| Icon: CopyIcon, | ||
| title: "Copy any hook for AI", | ||
| body: ( | ||
| <> | ||
| Every hook page has a one-click <strong className="text-fg">Copy for AI</strong> — | ||
| clean Markdown, ready to paste into your chat and start building. | ||
| </> | ||
| ), | ||
| }, | ||
| { | ||
| Icon: ExternalLinkIcon, | ||
| title: "Straight into your chat", | ||
| body: ( | ||
| <> | ||
| Jump into ChatGPT or Claude prefilled with the hook's context and a | ||
| live docs link. Your AI pair is in the loop from line one. | ||
| </> | ||
| ), | ||
| }, | ||
| ]; | ||
|
|
||
| export function AiSection() { | ||
| const llmsIndex = buildLlmsIndex(); | ||
| const prompt = `I'm using hookli, a zero-dependency, typed, SSR-safe React hooks library (${HOOKS.length} hooks). Full reference: ${SITE_URL}/llms-full.txt — help me pick and use the right hooks for my app.`; | ||
| const chatgptUrl = `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`; | ||
| const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`; | ||
| const linkClass = | ||
| "inline-flex min-h-11 items-center gap-1.5 text-sm text-gray-body underline-offset-4 transition-colors duration-200 hover:text-fg hover:underline"; | ||
|
Comment on lines
+53
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add visible keyboard focus rings to the new actions.
Proposed fix const linkClass =
- "inline-flex min-h-11 items-center gap-1.5 text-sm text-gray-body underline-offset-4 transition-colors duration-200 hover:text-fg hover:underline";
+ "inline-flex min-h-11 items-center gap-1.5 text-sm text-gray-body underline-offset-4 transition-colors duration-200 hover:text-fg hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
...
<CopyButton
text={llmsIndex}
copyLabel="Copy llms.txt"
label="Copy the hookli llms.txt index to your clipboard"
+ className="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
/>As per coding guidelines, “give visible keyboard focus rings for interactive elements.” Also applies to: 86-86 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| return ( | ||
| <section id="ai" className="mx-auto w-full max-w-5xl scroll-mt-24 px-4 py-20 sm:px-6"> | ||
| <SectionHeading | ||
| eyebrow="AI-ready" | ||
| title="Bring your AI — it speaks hookli" | ||
| subtitle={ | ||
| <> | ||
| Vibe-coding with an LLM? hookli ships a machine-readable map of all{" "} | ||
| {HOOKS.length} hooks, so your assistant recommends and wires them up | ||
| correctly — no stale copy-paste. | ||
| </> | ||
| } | ||
| /> | ||
|
|
||
| <div className="mt-12 grid gap-4 md:grid-cols-3"> | ||
| {CARDS.map(({ Icon, title, body }) => ( | ||
| <div key={title} className="surface flex flex-col gap-4 rounded-xl p-6"> | ||
| <span | ||
| aria-hidden="true" | ||
| className="surface flex size-11 items-center justify-center rounded-lg text-accent" | ||
| > | ||
| <Icon className="size-5" /> | ||
| </span> | ||
| <h3 className="text-base font-semibold text-fg">{title}</h3> | ||
| <p className="text-sm text-gray-body">{body}</p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-3 sm:flex-row sm:flex-wrap"> | ||
| <CopyButton | ||
| text={llmsIndex} | ||
| copyLabel="Copy llms.txt" | ||
| label="Copy the hookli llms.txt index to your clipboard" | ||
| /> | ||
| <a href="/llms.txt" className={linkClass}> | ||
| <BracesIcon className="size-4" aria-hidden="true" /> | ||
| View /llms.txt | ||
| </a> | ||
| <a | ||
| href={chatgptUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={linkClass} | ||
| > | ||
| Open in ChatGPT | ||
| <ExternalLinkIcon className="size-3.5" aria-hidden="true" /> | ||
| </a> | ||
| <a | ||
| href={claudeUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={linkClass} | ||
| > | ||
| Open in Claude | ||
| <ExternalLinkIcon className="size-3.5" aria-hidden="true" /> | ||
| </a> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: devsaifmohamed/hookli
Length of output: 3301
🌐 Web query:
Next.js 16.2.10 RouteContext route handler generated type helper💡 Result:
In Next.js 16.2.10, the RouteContext type helper is a globally available utility designed to provide strong typing for the context parameter in App Router Route Handlers [1][2]. It allows you to define the expected route parameters for a specific route literal [1][3]. Key features of the RouteContext helper: 1. Global Availability: The RouteContext helper is globally available after type generation and does not require an import statement [1][4][2]. 2. Automatic Type Generation: Types are automatically generated by Next.js during development (next dev), build (next build), or by manually running the type generation command (next typegen) [1][2][5]. 3. Usage Pattern: You apply the helper by passing the route string as a generic type argument [1][2]. For example: export async function GET(_req: NextRequest, ctx: RouteContext<'/users/[id]'>) { const { id } = await ctx.params return Response.json({ id }) } In this pattern, the params property inside the context object is resolved as a Promise, reflecting the asynchronous nature of Request-time APIs in modern Next.js versions [1][2][5]. This type helper was introduced as part of the broader effort to enhance type safety and developer experience for App Router routes, alongside similar helpers like PageProps and LayoutProps [6][5][7].
Citations:
🏁 Script executed:
Repository: devsaifmohamed/hookli
Length of output: 3376
Use Next’s generated
RouteContextforparams.Replace the duplicated
Promise<{ slug: string }>type withRouteContext<"/docs/[slug]/llms.txt">. Next.js16.2.10provides this global helper, and itsparamsfield remains asynchronous.🤖 Prompt for AI Agents
Source: MCP tools