# Pages from Content

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Content
Source: https://www.devtutorials.ai/learn/astro/content/pages-from-content

> Creating a page for each collection entry, listing pages, pagination with paginate() and the page prop, nested pagination for tags, and references between collections with reference(), getEntry() and getEntries().

## Pages from Content

- A collection holds Asha's posts. Now each post needs its own page.
- She also needs a list of all posts, split into pages of 5, and a page for each tag.
- All of these pages are made from the same collection, with dynamic routes.
- In this lesson, you will combine **Routing** and **Content Collections** to build a real blog.

**Simple lesson:** one collection, a few dynamic page files, and Astro builds every blog page for you.

### A Real-World Example

- Asha adds a new Markdown file to `src/content/blog`.
- She does not touch any page file.
- At the next build, the new post gets its own page, appears in the list, and shows up on its tag pages.

Press **▶ Run**. Open a post from the list. Then look at the page list in the address bar.

<h1>{post.data.title}</h1>
<p><small>{post.data.pubDate.toDateString()}</small></p>
`,
    "src/pages/blog/index.astro": `---

const posts = (await getCollection("blog")).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---

<h1>All posts</h1>
<ul>
    {posts.map((post) => (
      <li><a href={"/blog/" + post.id}>{post.data.title}</a></li>
    ))}
</ul>
`,
  }}
/>

What happened:

- `getStaticPaths()` asked the collection for every post.
- Each post became one path: its `id` in `params`, the whole entry in `props`.
- The page rendered the entry with `render()`.

### What You Will Learn

In this lesson, you will learn:

1. How to build one page per entry.
2. How to build a listing page.
3. How to split a long list with `paginate()`.
4. How to use the `page` prop and its links.
5. How to build tag pages, with pagination inside them.
6. How to link collections with `reference()`.

## Generating Pages from Content

### Creating a Page for Each Entry

1. Make a dynamic route file, such as `src/pages/blog/[id].astro`.
2. In `getStaticPaths()`, call `getCollection()`.
3. Return one item per entry: `params: { id: post.id }`, `props: { post }`.
4. In the page, read `Astro.props.post` and `render()` it.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-pages" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-pages">Animation: each entry in the blog collection passes through the [id].astro page file and comes out as its own HTML page.</title>
  <defs><marker id="pg-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="40" width="150" height="120" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="95" y="62" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>blog collection</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fill: "var(--ngc-blue)" }}>
    <text x="40" y="88">why-astro</text><text x="40" y="112">collections</text><text x="40" y="136">markdown-tips</text>
  </g>
  <rect x="250" y="70" width="140" height="60" rx="10" style={{ fill: "var(--ngc-orange)" }} />
  <text x="320" y="98" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>[id].astro</text>
  <text x="320" y="116" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--sans)", fontSize: 11 }}>getStaticPaths()</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="460" y="34" width="160" height="30" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;1;1" keyTimes="0;0.3;1" dur="4s" repeatCount="indefinite" /></rect>
    <text x="540" y="54" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;1;1" keyTimes="0;0.3;1" dur="4s" repeatCount="indefinite" />/blog/why-astro</text>
    <rect x="460" y="85" width="160" height="30" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.4;0.55;1" dur="4s" repeatCount="indefinite" /></rect>
    <text x="540" y="105" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.4;0.55;1" dur="4s" repeatCount="indefinite" />/blog/collections</text>
    <rect x="460" y="136" width="160" height="30" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.65;0.8;1" dur="4s" repeatCount="indefinite" /></rect>
    <text x="540" y="156" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.65;0.8;1" dur="4s" repeatCount="indefinite" />/blog/markdown-tips</text>
  </g>
  <path d="M170 100 H246" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#pg-arrow)" />
  <path d="M390 100 H456" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#pg-arrow)" />
</svg>
<figcaption>One page file, one page per entry.</figcaption>
</figure>

### Building a Listing Page

- A listing page is a normal page, such as `src/pages/blog/index.astro`.
- It calls `getCollection()`, sorts the posts, and links to each one with `"/blog/" + post.id`.
- A listing often shows a short summary. Add a `description` field to the schema for it.

{posts.map((post) => (
    <article>
      <h2><a href={"/blog/" + post.id}>{post.data.title}</a></h2>
      <p><small>{format(post.data.pubDate)}</small></p>
      <p>{post.data.description}</p>
    </article>
))}
`,
  }}
/>

Use the entry's `id` for the address, so the link on the listing page and the page's real address always match.

**Simple lesson:** a dynamic route with `getStaticPaths()` makes the post pages. A normal page lists them.

## Pagination

### paginate()

- **Pagination** = splitting a long list over several pages: page 1, page 2, page 3.
- `getStaticPaths()` receives a helper called `paginate`.
- `return paginate(posts, { pageSize: 5 })` makes one page per 5 posts.
- The file name needs a page parameter, usually a rest parameter: `src/pages/blog/[...page].astro`.
- Page 1 is at `/blog`. Page 2 is at `/blog/2`, and so on.

### The page Prop

Each page gets a prop called `page`:

- `page.data` = the posts on this page.
- `page.currentPage` and `page.lastPage` = page numbers.
- `page.total` = how many posts in all.
- `page.url.prev` and `page.url.next` = links to the next and previous pages, or `undefined`.
- `page.url.first` and `page.url.last` = links to the first and last pages.

Press **▶ Run**. Seven posts, 3 per page, make 3 pages. Click **Next**.

<ol start={page.start + 1}>
    {page.data.map((title) => <li>{title}</li>)}
</ol>
<nav>
    {page.url.prev && <a href={page.url.prev}>← Previous</a>}
    {page.url.next && <a href={page.url.next}>Next →</a>}
</nav>
<p><small>{page.total} posts in all</small></p>
`,
  }}
/>

- `page.start` is the position of the first item on this page, counting from 0.
- In a real blog, `posts` comes from `getCollection()`, sorted newest first.

### [TS] Typing getStaticPaths with paginate

- Import `GetStaticPaths` and `Page` from `astro`:

```ts
import type { GetStaticPaths, Page } from "astro";

export const getStaticPaths = (async ({ paginate }) => {
  return paginate(["a", "b", "c"], { pageSize: 2 });
}) satisfies GetStaticPaths;

const { page } = Astro.props as { page: Page<string> };
```

**Simple lesson:** `paginate(list, { pageSize })` in a `[...page]` route. The `page` prop holds the items and the links.

## Tag Pages and Nested Pagination

### A Page per Tag

- Collect every tag from every post, without repeats: `new Set(posts.flatMap((p) => p.data.tags))`.
- Make one path per tag in `src/pages/tags/[tag].astro`.
- Pass the matching posts as a prop.

### Nested Pagination

- **Nested pagination** = pagination inside another dynamic route, such as page 2 of the "astro" tag.
- The file is `src/pages/tags/[tag]/[...page].astro`.
- Call `paginate()` once per tag, with `params: { tag }`. Then join the results with `flatMap()`.

Press **▶ Run**. The tag list links to each tag page. The "astro" tag has more than 2 posts, so it has two pages.

<ul>
    {tags.map((tag) => <li><a href={"/tags/" + tag}>#{tag}</a></li>)}
</ul>
`,
    "src/pages/tags/[tag]/[...page].astro": `---

    const posts = await getCollection("blog");
    const tags = [...new Set(posts.flatMap((post) => post.data.tags))];

    return tags.flatMap((tag) => {
      const tagged = posts.filter((post) => post.data.tags.includes(tag));
      return paginate(tagged, { params: { tag }, pageSize: 2 });
    });
}

const { tag } = Astro.params;
const { page } = Astro.props;
---

<h1>#{tag} (page {page.currentPage} of {page.lastPage})</h1>
<ul>{page.data.map((post) => <li>{post.data.title}</li>)}</ul>
{page.url.next && <a href={page.url.next}>Next →</a>}
<p><a href="/tags">All tags</a></p>
`,
  }}
/>

**Simple lesson:** collect the tags, then paginate once per tag with `params: { tag }`.

## References Between Collections

### reference()

- A post has an author. The author's details live in another collection.
- In the schema, write `author: reference("authors")`.
- In the post's frontmatter, write only the author's ID: `author: asha`.
- Astro checks that the ID exists, and stores a small **reference**: `{ collection: "authors", id: "asha" }`.

### Resolving Referenced Entries

- A reference is not the author's data yet. Look it up:
  - one reference: `await getEntry(post.data.author)`,
  - a list of references: `await getEntries(post.data.related)`.

<p>Written by {author?.data.name} from {author?.data.city}</p>
<h2>Related posts</h2>
<ul>{related.map((r) => <li>{r?.data.title}</li>)}</ul>
`,
  }}
/>

A reference keeps data in one place. When Ravi moves city, Asha changes one line in `authors.json`, and every post shows the new city.

A typo in a referenced ID may only show up when you look it up: `getEntry()` can return `undefined`. Use `?.` or check for it, so one typo does not break the whole page.

**Simple lesson:** `reference("authors")` stores an ID. `getEntry()` and `getEntries()` turn it into full entries.

## TL;DR

1. **Page per entry** = `[id].astro` + `getStaticPaths()` returning `params: { id }` and `props: { post }`.
2. **Listing page** = `getCollection()`, sort, link with `"/blog/" + post.id`.
3. **paginate()** = `paginate(list, { pageSize })` in a `[...page].astro` route. Page 1 has no number in its address.
4. **page prop:**
   - `page.data` = items on this page,
   - `page.currentPage`, `page.lastPage`, `page.total`,
   - `page.url.prev`, `page.url.next`, `page.url.first`, `page.url.last`.
5. **Tag pages** = collect tags with a `Set`, one path per tag.
6. **Nested pagination** = `paginate(tagged, { params: { tag }, pageSize })` per tag, joined with `flatMap()`.
7. **reference("authors")** = an ID in the schema. **getEntry() / getEntries()** = resolve it.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `params: { slug: post.id }` in `[id].astro` | The param name does not matter | Use the name in the brackets: `id` | Build error about a missing param |
| Not passing the entry as a prop | The page can find it | Pass `props: { post }` | `post` is `undefined` |
| `paginate()` in `[page].astro` | Any param works | Use `[...page]` so page 1 has no number | `/blog` is missing |
| Linking to `/blog/1` | Page 1 has a number | Page 1 is at `/blog` | The link shows the 404 page |
| Using `post.data.author.name` | References hold the data | Call `getEntry(post.data.author)` first | `name` is `undefined` |
| Repeated tags in the tag list | Tags are already unique | Use `new Set()` | The same tag page is listed twice |

</div>

## Check Yourself

## Exercises

## What to Remember

- `[id].astro` with `getStaticPaths()` gives every entry its own page.
- A listing page sorts the collection and links with each entry's `id`.
- `paginate()` splits a list into pages. The `page` prop has the items and the links.
- Tag pages collect tags with a `Set`. Nested pagination passes `params: { tag }`.
- `reference()` links collections. `getEntry()` and `getEntries()` resolve the links.

Next: **Images and Fonts**, where Asha's posts get optimised cover images and a custom font.
