---
title: "Add llms.txt"
description: "Add markdown access for the feedback API, publish an llms.txt index that links to the canonical docs, and provide llms-full.txt as an optional single-request representation."
canonical_url: "https://vercel.com/academy/agent-friendly-apis/add-llms-txt"
md_url: "https://vercel.com/academy/agent-friendly-apis/add-llms-txt.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-08-26T21:03:36.481Z"
content_type: "lesson"
course: "agent-friendly-apis"
course_title: "Agent-Friendly APIs"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Add llms.txt

# Add llms.txt and Markdown Access

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

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

An agent cannot fetch documentation at `/api/docs` unless it first learns that the route exists. It might receive the URL in a prompt, follow a link, or find it through a search index.

An `llms.txt` file gives agents one described list of the API's important resources. The path alone does not guarantee discovery, so treat the file as a supplement to accurate first-party documentation.

## Outcome

Add `/api/docs.md`, `/llms.txt`, and `/llms-full.txt` routes so agents can read the canonical API documentation as Markdown and use a concise index when they reach it.

## 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 proposal

The `llms.txt` proposal (from [llmstxt.org](https://llmstxt.org)) defines a simple markdown format that lives at the root of your site. The structure looks like this:

```markdown
# 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 its own docs. You can see the canonical index at [vercel.com/llms.txt](https://vercel.com/llms.txt). The current llms.txt proposal defines the index format; `llms-full.txt` is a useful community convention rather than a required part of that proposal. We'll build both forms.

\*\*Note: llms.txt is a supplement\*\*

In the Vercel and Ora trace study, 86% of attributed llms.txt fetches followed a link rather than guessing the path. Of the scorable runs that reached the file, 36% later fetched a page it listed. Publish llms.txt as a lean map to canonical docs. When this API becomes part of a developer portal, link the file from the pages developers and agents already visit.

\*\*Note: 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:

```ts title="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.

\*\*Note: 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. This course includes the full form to compare the two access patterns, not because every API must publish it.

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:

```ts title="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 an integer 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 entries for the course, then identify the low-rated ones
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.

\*\*Note: Index vs. full: two access patterns\*\*

Use `llms.txt` as an index for selective fetching and `llms-full.txt` for the complete documentation. The canonical docs remain the source of truth in either case.

## 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`:

```ts title="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 an integer 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 entries for the course, then identify the low-rated ones
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.

\*\*Note: 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:

```bash
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:

```bash
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:

```bash
curl http://localhost:3000/api/docs.md
```

This should return the full API documentation in markdown.

Verify the content types are correct:

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

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

```bash
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

```bash
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


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
