Add llms.txt and Markdown Access
Most restaurants have a menu posted outside the door. You can stand on the sidewalk and know exactly what they serve, what the prices are, and whether they have that one dish you came for. No need to awkwardly walk in, sit down, and ask the waiter to list everything. The menu is the invitation. It tells you: here's what we've got, here's how to get it.
Your API needs one of those menus.
Right now, even if you had perfect docs at /api/docs, an agent wouldn't know that endpoint exists unless someone told it. There's no standard place to look, no convention to follow. The agent has to guess, or the developer has to hardcode the URL into every prompt.
The llms.txt standard fixes this. It's a single file at a well-known path that tells agents what your API offers and where to find the details.
Outcome
Add /llms.txt, /llms-full.txt, and /api/docs.md routes to the feedback API so agents can discover, skim, and read your documentation in machine-readable formats.
Fast Track
- Fill in the llms.txt content in
app/llms.txt/route.ts(stub provided in starter) - Fill in the complete docs in
app/llms-full.txt/route.ts(stub provided in starter) - Fill in the markdown docs in
app/api/docs.md/route.ts(stub provided in starter) - Verify all three endpoints return the correct content types
The llms.txt standard
The llms.txt spec (from llmstxt.org) defines a simple markdown format that lives at the root of your site. The structure looks like this:
# Project Name
> A one-line summary of what this project does.
A slightly longer description with context.
## Section Name
- [Link Title](https://example.com/path): Description of the resource
- [Another Link](https://example.com/other): What this resource providesThe key pieces:
- H1 with the project name (required)
- Blockquote with a brief summary
- Description paragraph with more context
- H2 sections with markdown link lists pointing to your endpoints and docs
Vercel uses this pattern for their own docs. You can see the index at vercel.com/docs/llms.txt. We'll build both an llms.txt index and an llms-full.txt that bundles everything into a single response.
The llms.txt file is served as text/plain, not text/markdown. This is intentional. Agents fetch it as raw text and parse the markdown structure themselves. The text/plain content type keeps it simple and universally accessible.
Build the llms.txt endpoint
The starter already has app/llms.txt/route.ts with a TODO stub. Open it up and replace the placeholder content with the real llms.txt markup:
import { NextResponse } from "next/server";
const llmsTxt = `# Cooking Course Feedback API
> API for submitting and retrieving student feedback on cooking course lessons.
This API serves feedback data for a cooking course platform. Students can submit ratings and comments on individual lessons, retrieve feedback filtered by course or rating, and view aggregate statistics.
## API Documentation
- [API Docs](/api/docs): Full endpoint reference with parameters, examples, and error cases
- [API Docs (Markdown)](/api/docs.md): Same documentation in .md format
- [Full Documentation](/llms-full.txt): Complete API docs in a single file
## Endpoints
- [List feedback](/api/feedback): GET all feedback entries, with optional filtering
- [Get feedback by ID](/api/feedback/:id): GET a single feedback entry
- [Submit feedback](/api/feedback): POST a new feedback entry
- [Feedback summary](/api/feedback/summary): GET aggregate statistics
`;
export async function GET() {
return new NextResponse(llmsTxt, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
}The route folder name is literally llms.txt, which means Next.js will serve it at /llms.txt. The content follows the spec: H1 with the project name, a blockquote summary, a description, and H2 sections with links to everything an agent might need.
The llms.txt file links to /api/docs, but that route is still a stub with placeholder content. That's intentional. In Section 3, the skill you build will generate the real /api/docs route handler automatically. For now, the link establishes the contract: this is where agents will find the full documentation once it exists. The .md variant you're about to fill in serves as the working docs endpoint until then.
Add llms-full.txt
The llms.txt file is an index. It tells agents what exists and where to look. But sometimes an agent doesn't want to follow links. It wants everything in one shot.
That's what llms-full.txt is for. Where llms.txt is the table of contents, llms-full.txt is the whole book. One request, one response, every endpoint documented. Vercel's docs site serves both: llms.txt for agents that want to browse selectively, and llms-full.txt for agents that want to load everything into context at once.
For a small API like ours, the full version isn't dramatically longer than the index. But the pattern matters. As your API grows, agents that only need one endpoint can start with llms.txt and follow a single link. Agents that need the full picture fetch llms-full.txt and get it all.
The starter has app/llms-full.txt/route.ts with a TODO stub. Fill it in with the project overview and the complete API documentation:
import { NextResponse } from "next/server";
const llmsFullTxt = `# Cooking Course Feedback API
> API for submitting and retrieving student feedback on cooking course lessons.
This API serves feedback data for a cooking course platform. Students can submit ratings and comments on individual lessons, retrieve feedback filtered by course or rating, and view aggregate statistics.
## Endpoints
### List feedback
\`\`\`
GET /api/feedback
\`\`\`
Returns all feedback entries. Supports optional query parameters for filtering.
**Query parameters:**
| Parameter | Type | Description |
|--------------|--------|--------------------------------------|
| courseSlug | string | Filter by course slug |
| lessonSlug | string | Filter by lesson slug |
| minRating | number | Only return entries rated >= value |
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback?courseSlug=knife-skills"
\`\`\`
**Example response:**
\`\`\`json
[
{
"id": "fb-001",
"courseSlug": "knife-skills",
"lessonSlug": "the-claw-grip",
"rating": 5,
"comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
"author": "Priya Sharma",
"createdAt": "2026-03-01T10:30:00Z"
}
]
\`\`\`
### Get feedback by ID
\`\`\`
GET /api/feedback/:id
\`\`\`
Returns a single feedback entry.
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback/fb-001"
\`\`\`
**Example response:**
\`\`\`json
{
"id": "fb-001",
"courseSlug": "knife-skills",
"lessonSlug": "the-claw-grip",
"rating": 5,
"comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
"author": "Priya Sharma",
"createdAt": "2026-03-01T10:30:00Z"
}
\`\`\`
**Error response (404):**
\`\`\`json
{
"error": "Feedback with id \\"fb-999\\" not found"
}
\`\`\`
### Submit feedback
\`\`\`
POST /api/feedback
\`\`\`
Creates a new feedback entry. The \`id\` and \`createdAt\` fields are generated automatically.
**Request body (JSON):**
| Field | Type | Required | Description |
|-------------|--------|----------|---------------------------------|
| courseSlug | string | yes | Slug of the course |
| lessonSlug | string | yes | Slug of the lesson |
| rating | number | yes | Rating from 1 to 5 |
| comment | string | yes | Feedback text |
| author | string | yes | Name of the person |
**Example request:**
\`\`\`bash
curl -X POST "http://localhost:3000/api/feedback" \\
-H "Content-Type: application/json" \\
-d '{
"courseSlug": "bread-baking",
"lessonSlug": "scoring-dough",
"rating": 5,
"comment": "The lame technique demo was incredibly helpful.",
"author": "Alex Turner"
}'
\`\`\`
**Example response (201):**
\`\`\`json
{
"id": "fb-011",
"courseSlug": "bread-baking",
"lessonSlug": "scoring-dough",
"rating": 5,
"comment": "The lame technique demo was incredibly helpful.",
"author": "Alex Turner",
"createdAt": "2026-03-06T12:00:00Z"
}
\`\`\`
**Error response (400), missing fields:**
\`\`\`json
{
"error": "Missing required fields: courseSlug, lessonSlug, rating, comment, author"
}
\`\`\`
**Error response (400), invalid rating:**
\`\`\`json
{
"error": "Rating must be a number between 1 and 5"
}
\`\`\`
### Get summary
\`\`\`
GET /api/feedback/summary
\`\`\`
Returns aggregate statistics across all feedback. Optionally filter by course.
**Query parameters:**
| Parameter | Type | Description |
|-------------|--------|------------------------|
| courseSlug | string | Filter by course slug |
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback/summary"
\`\`\`
**Example response:**
\`\`\`json
{
"totalEntries": 10,
"averageRating": 4.2,
"ratingDistribution": {
"1": 0,
"2": 1,
"3": 1,
"4": 3,
"5": 5
},
"courses": [
{
"courseSlug": "knife-skills",
"totalEntries": 5,
"averageRating": 4.4
},
{
"courseSlug": "bread-baking",
"totalEntries": 4,
"averageRating": 4.0
},
{
"courseSlug": "pasta-from-scratch",
"totalEntries": 1,
"averageRating": 4.0
}
]
}
\`\`\`
## Schema
### Feedback
| Field | Type | Description |
|-------------|--------|------------------------------------------|
| id | string | Unique identifier (e.g. "fb-001") |
| courseSlug | string | Slug of the course |
| lessonSlug | string | Slug of the lesson |
| rating | number | Integer from 1 to 5 |
| comment | string | Feedback text |
| author | string | Name of the person |
| createdAt | string | ISO 8601 timestamp |
## Workflows
### Investigate low-rated feedback for a course
1. \`GET /api/feedback/summary?courseSlug=knife-skills\` — check the average rating and total entries
2. \`GET /api/feedback?courseSlug=knife-skills&minRating=1\` — pull all low-rated entries
3. \`GET /api/feedback/fb-003\` — get the full details on a specific entry
### Submit and verify new feedback
1. \`POST /api/feedback\` — submit the feedback entry with all required fields
2. \`GET /api/feedback/:id\` — fetch the newly created entry using the \`id\` from the POST response
3. \`GET /api/feedback/summary?courseSlug=bread-baking\` — check updated stats for the course
### Compare feedback across courses
1. \`GET /api/feedback/summary\` — get aggregate stats for all courses
2. \`GET /api/feedback?courseSlug=knife-skills\` — pull all feedback for the top-rated course
3. \`GET /api/feedback?courseSlug=bread-baking\` — pull all feedback for comparison
`;
export async function GET() {
return new NextResponse(llmsFullTxt, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
}This is a lot of content, but that's the point. The llms-full.txt file is the "give me everything" option. Notice it uses the same text/plain content type as llms.txt. Same spec, different scope.
llms.txt is for agents that want to browse. They read the index, pick the links they need, and fetch only what's relevant. llms-full.txt is for agents that want to load everything into context at once. Both patterns are useful. Small APIs can get away with just llms-full.txt, but offering both is the convention.
Add markdown access to the docs
Vercel serves all their docs pages as .md too. If you can read /docs/functions, you can also read /docs/functions.md. This pattern makes it easy for agents to request documentation in a format they parse well.
We'll do the same thing. The starter already has app/api/docs.md/route.ts with a TODO stub. Open it and fill in the full API documentation.
Replace the placeholder in app/api/docs.md/route.ts:
import { NextResponse } from "next/server";
const docs = `# Feedback API
Base URL: \`/api/feedback\`
## Endpoints
### List feedback
\`\`\`
GET /api/feedback
\`\`\`
Returns all feedback entries. Supports optional query parameters for filtering.
**Query parameters:**
| Parameter | Type | Description |
|--------------|--------|--------------------------------------|
| courseSlug | string | Filter by course slug |
| lessonSlug | string | Filter by lesson slug |
| minRating | number | Only return entries rated >= value |
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback?courseSlug=knife-skills"
\`\`\`
**Example response:**
\`\`\`json
[
{
"id": "fb-001",
"courseSlug": "knife-skills",
"lessonSlug": "the-claw-grip",
"rating": 5,
"comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
"author": "Priya Sharma",
"createdAt": "2026-03-01T10:30:00Z"
}
]
\`\`\`
### Get feedback by ID
\`\`\`
GET /api/feedback/:id
\`\`\`
Returns a single feedback entry.
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback/fb-001"
\`\`\`
**Example response:**
\`\`\`json
{
"id": "fb-001",
"courseSlug": "knife-skills",
"lessonSlug": "the-claw-grip",
"rating": 5,
"comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
"author": "Priya Sharma",
"createdAt": "2026-03-01T10:30:00Z"
}
\`\`\`
**Error response (404):**
\`\`\`json
{
"error": "Feedback with id \\"fb-999\\" not found"
}
\`\`\`
### Submit feedback
\`\`\`
POST /api/feedback
\`\`\`
Creates a new feedback entry. The \`id\` and \`createdAt\` fields are generated automatically.
**Request body (JSON):**
| Field | Type | Required | Description |
|-------------|--------|----------|---------------------------------|
| courseSlug | string | yes | Slug of the course |
| lessonSlug | string | yes | Slug of the lesson |
| rating | number | yes | Rating from 1 to 5 |
| comment | string | yes | Feedback text |
| author | string | yes | Name of the person |
**Example request:**
\`\`\`bash
curl -X POST "http://localhost:3000/api/feedback" \\
-H "Content-Type: application/json" \\
-d '{
"courseSlug": "bread-baking",
"lessonSlug": "scoring-dough",
"rating": 5,
"comment": "The lame technique demo was incredibly helpful.",
"author": "Alex Turner"
}'
\`\`\`
**Example response (201):**
\`\`\`json
{
"id": "fb-011",
"courseSlug": "bread-baking",
"lessonSlug": "scoring-dough",
"rating": 5,
"comment": "The lame technique demo was incredibly helpful.",
"author": "Alex Turner",
"createdAt": "2026-03-06T12:00:00Z"
}
\`\`\`
**Error response (400), missing fields:**
\`\`\`json
{
"error": "Missing required fields: courseSlug, lessonSlug, rating, comment, author"
}
\`\`\`
**Error response (400), invalid rating:**
\`\`\`json
{
"error": "Rating must be a number between 1 and 5"
}
\`\`\`
### Get summary
\`\`\`
GET /api/feedback/summary
\`\`\`
Returns aggregate statistics across all feedback. Optionally filter by course.
**Query parameters:**
| Parameter | Type | Description |
|-------------|--------|------------------------|
| courseSlug | string | Filter by course slug |
**Example request:**
\`\`\`bash
curl "http://localhost:3000/api/feedback/summary"
\`\`\`
**Example response:**
\`\`\`json
{
"totalEntries": 10,
"averageRating": 4.2,
"ratingDistribution": {
"1": 0,
"2": 1,
"3": 1,
"4": 3,
"5": 5
},
"courses": [
{
"courseSlug": "knife-skills",
"totalEntries": 5,
"averageRating": 4.4
},
{
"courseSlug": "bread-baking",
"totalEntries": 4,
"averageRating": 4.0
},
{
"courseSlug": "pasta-from-scratch",
"totalEntries": 1,
"averageRating": 4.0
}
]
}
\`\`\`
## Schema
### Feedback
| Field | Type | Description |
|-------------|--------|------------------------------------------|
| id | string | Unique identifier (e.g. "fb-001") |
| courseSlug | string | Slug of the course |
| lessonSlug | string | Slug of the lesson |
| rating | number | Integer from 1 to 5 |
| comment | string | Feedback text |
| author | string | Name of the person |
| createdAt | string | ISO 8601 timestamp |
## Workflows
### Investigate low-rated feedback for a course
1. \`GET /api/feedback/summary?courseSlug=knife-skills\` — check the average rating and total entries
2. \`GET /api/feedback?courseSlug=knife-skills&minRating=1\` — pull all low-rated entries
3. \`GET /api/feedback/fb-003\` — get the full details on a specific entry
### Submit and verify new feedback
1. \`POST /api/feedback\` — submit the feedback entry with all required fields
2. \`GET /api/feedback/:id\` — fetch the newly created entry using the \`id\` from the POST response
3. \`GET /api/feedback/summary?courseSlug=bread-baking\` — check updated stats for the course
### Compare feedback across courses
1. \`GET /api/feedback/summary\` — get aggregate stats for all courses
2. \`GET /api/feedback?courseSlug=knife-skills\` — pull all feedback for the top-rated course
3. \`GET /api/feedback?courseSlug=bread-baking\` — pull all feedback for comparison
`;
export async function GET() {
return new NextResponse(docs, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
});
}The .md extension in the URL makes it explicit that this is markdown, which is a signal agents recognize. The /api/docs route still has placeholder content from the starter. In Section 3, the skill you build will generate the real implementation of /api/docs automatically. For now, /api/docs.md is the working docs endpoint.
Good question. You need to know what correct output looks like before you can evaluate what a skill produces. The docs you're writing here become your reference point in Section 3. Once the skill is working, you won't maintain these strings by hand again.
Try It
Start your dev server and test both new endpoints.
Fetch the llms.txt file:
curl http://localhost:3000/llms.txtYou should see the plain-text markdown with the project name, summary, and links to your API docs and endpoints (including the new llms-full.txt link).
Now fetch the full documentation:
curl http://localhost:3000/llms-full.txtThis should return the complete API documentation in a single response. It's the same content as /api/docs.md but with the project overview prepended.
Now fetch the markdown docs:
curl http://localhost:3000/api/docs.mdThis should return the full API documentation in markdown.
Verify the content types are correct:
curl -I http://localhost:3000/llms.txtLook for Content-Type: text/plain; charset=utf-8 in the headers.
curl -I http://localhost:3000/api/docs.mdLook for Content-Type: text/markdown; charset=utf-8 in the headers.
Troubleshooting:
- If you get a 404, make sure the folder names are exactly
llms.txt,llms-full.txt, anddocs.mdinsideapp/andapp/api/respectively. The folder name becomes the URL path. - If the content type is wrong, double-check the
Content-Typeheader string in yourNextResponse. A typo liketext/plainswill silently serve the wrong type.
Commit
git add -A
git commit -m "feat(docs): add llms.txt, llms-full.txt, and markdown docs access"Done-When
- Hitting
/llms.txtreturns a plain-text markdown file with H1, blockquote, description, and H2 sections linking to your endpoints - Hitting
/llms-full.txtreturns the complete API documentation in a single plain-text response - Hitting
/api/docs.mdreturns the full API documentation in markdown - The
/llms.txtand/llms-full.txtresponses haveContent-Type: text/plain; charset=utf-8 - The
/api/docs.mdresponse hasContent-Type: text/markdown; charset=utf-8 - The llms.txt content includes links to
/api/docs,/api/docs.md, and/llms-full.txt
Solution
The complete implementations are shown in the exercise above. Here are the three files:
app/llms.txt/route.ts— Returns the project index astext/plainwith H1, blockquote summary, and links to docs and endpointsapp/llms-full.txt/route.ts— Returns the complete API documentation (all endpoints, schema, workflows) astext/plainapp/api/docs.md/route.ts— Returns the same endpoint documentation astext/markdown, without the project overview header
Was this helpful?