Vercel Logo

Add llms.txt and Markdown Access

A menu posted outside a restaurant tells people what is available before they enter.

Your API needs a discoverable index that serves the same purpose.

An agent cannot discover documentation at /api/docs unless it knows the route exists. Without a conventional index, each prompt or integration must supply the URL.

The llms.txt standard provides a file at a well-known path that describes the API and links to its documentation.

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

  1. Fill in the llms.txt content in app/llms.txt/route.ts (stub provided in starter)
  2. Fill in the complete docs in app/llms-full.txt/route.ts (stub provided in starter)
  3. Fill in the markdown docs in app/api/docs.md/route.ts (stub provided in starter)
  4. 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 provides

The format includes:

  • 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.

Why plain text?

Serve llms.txt as text/plain. Agents can fetch the raw text and parse its Markdown structure without relying on a Markdown-specific content type.

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:

app/llms.txt/route.ts
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",
    },
  });
}

Next.js serves the llms.txt route folder at /llms.txt. Its response includes the project name, a blockquote summary, a description, and sections linking to the API resources.

The /api/docs link doesn't work yet

The /api/docs route still contains a placeholder. Section 3 replaces it with output generated by your skill. Linking it now establishes the route where clients will find the generated documentation. Until then, /api/docs.md is the working endpoint.

Add llms-full.txt

The llms.txt file is an index of available resources. Some clients instead need the complete documentation in one response.

llms-full.txt provides that complete response. Vercel's docs site offers llms.txt for selective browsing and llms-full.txt for loading the full documentation.

The size difference is small for this API. As an API grows, clients can use llms.txt to fetch one resource or llms-full.txt to load the complete reference.

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:

app/llms-full.txt/route.ts
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",
    },
  });
}

The longer response is expected because llms-full.txt includes every endpoint. It uses the same text/plain content type as llms.txt with a broader scope.

Index vs. full: two access patterns

Use llms.txt as an index for selective fetching and llms-full.txt for the complete documentation. Offering both supports either access pattern.

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:

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 identifies the response as Markdown. The starter's /api/docs route remains a placeholder until the skill replaces it in Section 3, so /api/docs.md is the working endpoint for now.

Why write docs by hand if the skill will generate them?

These hand-written docs provide the reference output used to evaluate the skill in Section 3. Once the skill works, it can maintain the generated route.

Try It

Start your dev server and test both new endpoints.

Fetch the llms.txt file:

curl http://localhost:3000/llms.txt

You 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.txt

This 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.md

This should return the full API documentation in markdown.

Verify the content types are correct:

curl -I http://localhost:3000/llms.txt

Look for Content-Type: text/plain; charset=utf-8 in the headers.

curl -I http://localhost:3000/api/docs.md

Look 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, and docs.md inside app/ and app/api/ respectively. The folder name becomes the URL path.
  • If the content type is wrong, double-check the Content-Type header string in your NextResponse. A typo like text/plains will 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.txt returns a plain-text markdown file with H1, blockquote, description, and H2 sections linking to your endpoints
  • Hitting /llms-full.txt returns the complete API documentation in a single plain-text response
  • Hitting /api/docs.md returns the full API documentation in markdown
  • The /llms.txt and /llms-full.txt responses have Content-Type: text/plain; charset=utf-8
  • The /api/docs.md response has Content-Type: text/markdown; charset=utf-8
  • The llms.txt content includes links to /api/docs, /api/docs.md, and /llms-full.txt

Solution

The exercise above contains the complete implementations for these files:

  • app/llms.txt/route.ts: Returns the project index as text/plain with links to the docs and endpoints
  • app/llms-full.txt/route.ts: Returns the complete API documentation as text/plain
  • app/api/docs.md/route.ts: Returns the endpoint documentation as text/markdown without the project overview

Was this helpful?

supported.