Перейти к содержимому

Автоматическая генерация списка записей в Starlight

Я использую Starlight + плагин Starlight-blog для моего блога. У меня есть собственно страница блога с маленькими заметками и 2 раздела, которые содержат внутри себя набор заметок: книжный дневник и гайды по разработке.

Для книжного дневника мне захотелось сделать страничку с оглавлением: собрать названия и ссылки на все записи из моего книжного раздела в одном месте. Добавлять руками каждый раз при добавлении записи про книгу - лень, так что надо сделать так, чтобы из набора записей про книги генерился список для оглавления со ссылками.

Querying Collections в Astro

В Astro есть возможность запрашивать коллекции, и это выглядело как отличный вариант: мне нужно запросить коллекцию записей в content/docs/content_list/reading - и выводить на страничке список всех этих записей.

Звучит отлично, но: Astro умеет работать только с коллекциями на первом уровне

A content collection is any top-level directory inside the reserved src/content project directory, such as src/content/newsletter and src/content/authors. (документация)

A Starlight уже создал одну коллекцию верхнего уровня - docs, в которой я размещаю свои коллекции.

Что ж, начинаем отбирать только нужные нам записи.

Фильтруем коллекции в Starlight

В getCollection() можно передать коллбек с функцией-фильтром. Чтобы проще было фильтровать, начнём присваивать заметкам категорию - по сути, это раздел или тема заметки.

Добавляем в описание схемы опциональное свойство category

src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
import { blogSchema } from 'starlight-blog/schema';
const customFields = z.object({
// for quering entries in collections
category: z.string().optional(),
})
export const collections = {
docs: defineCollection({ schema: docsSchema({
extend: (context) => {
const baseSchema = blogSchema(context);
return baseSchema.merge(customFields);
}
}) }),
i18n: defineCollection({ type: 'data', schema: i18nSchema({}),
}) })
};

Создаем компонент, которы получает все записи для коллекции docs и отфильтровывает только помеченные как category: reading

src/components/ReadingList.astro
---
const books = await getCollection('docs', (entry) => {
return entry.data.category === 'reading';
});
---
<ul>
{books.map(book => <li><a href={ `/blog/${book.slug}`}>{book.data.title}</a></li>)}
</ul>

В записи для книжного дневника добавляем category: reading

src/content/docs/content_list/reading/bookTitle.mdx
---
title: 'Title of book'
category: 'reading'
---

Отлично, работает!

А что насчёт разных локализованных версий?

Идем в русскую версию… и понимаем, что у нас проблемы. Потому что нам надо не задублировать записи и при этом вывести правильные для каждой локализации.

Что делаем? Кастомизируем наш компонент для вывода списка и делаем более конкретной систему категорий, учитывающую версии контента на разных языках (в todo записываем, что надо будет перейти с просто string на конечный список категорий, чтобы не расплодить ерунды и были подсказки).

Теперь наш компонент для сбора и вывода списка заметок по категории выглядит так:

src/components/ContentList.astro
type Props = {
category: string;
}
const {category} = Astro.props;
const items = await getCollection('docs', (entry) => {
return entry.data.category === category;
});
---
<ul>
{items.map(item => <li><a href={ `/blog/${item.slug}`}>{item.data.title}</a></li>)}
</ul>

В файлах для каждого языка указываем нужную категорию

src/content/docs/content_list/reading/bookTitle.mdx
---
title: 'Title of book'
category: 'reading-en'
---
src/content/docs/ru/content_list/reading/bookTitle.mdx
---
title: 'Название книги'
category: 'reading-ru'
---

Готово! Результат можно посмотреть на странице списка книг.