от Чудо Оньенма

Сообщество Nuxt наконец-то выпустило модуль Nuxt Content (или просто Content) для Nuxt 3. Content v2 поставляется с несколькими новыми функциями и улучшениями DX, которые делают использование CMS на основе файлов проще простого. Это важно, поскольку позволяет разработчикам управлять контентом и создавать сайты на основе контента, такие как блоги и документация, без необходимости управлять базой данных.

В этой статье мы рассмотрим модуль Content v2, что нового, варианты его использования и приложения, а также то, как мы можем использовать его для поддержки нашего блога Nuxt 3 с помощью Git/CMS на основе файлов. Мы будем создавать простой блог, используя многие фундаментальные и новые функции, предоставляемые Nuxt Content. Вы можете просмотреть развернутую версию того, что мы будем строить здесь.

Чтобы следовать, у вас должно быть:

Jamstack и Git/CMS на основе файлов

Если вы слышали о Jamstack, вы должны быть знакомы с тем, как он изменил то, как мы создаем современные веб-сайты и веб-приложения. Jamstack использует генераторы статических сайтов, чтобы предложить предварительно обработанную разметку, JavaScript для функциональности и API для данных для создания более быстрых веб-сайтов. Подробнее об этом можно узнать на официальном сайте Jamstack.

Есть много способов предоставить контент для сайта Jamstack. Многие веб-сайты используют API для получения контента из удаленной базы данных. В большинстве случаев эти API предоставляет безголовая CMS, такая как Contentful. Инструменты рендеринга на стороне сервера, такие как Gatsby, затем извлекают эти данные во время сборки и отображают страницы в виде файлов HTML.

Вместо безголовой CMS для предоставления контента для сайта мы можем использовать файлы шаблонов, такие как Markdown. Популярные генераторы статических сайтов используют файлы шаблонов для предоставления контента. CMS на основе Git/File предоставляют способ управления содержимым в этих файлах шаблонов и предоставляют его внешнему интерфейсу во время сборки без базы данных. Модуль Nuxt Content действует как CMS на основе Git/File для вашего приложения Nuxt с поддержкой SSR, позволяя вам писать свой контент в Markdown, YML, CSV или JSON и запрашивать его в своих компонентах.

Модуль контента Nuxt

Модуль контента читает каталог content/ в вашем проекте Nuxt, анализирует файлы .md, .yml, .csv и .json, чтобы создать мощный уровень данных для вашего приложения. Это также позволяет использовать компоненты Vue в Markdown с синтаксисом MDC.

Взгляните на некоторые из новых функций Content v2, представленных на официальном сайте.

  • Разработано для Nuxt 3. Воспользуйтесь преимуществами функций Nuxt 3: Vue 3, автоматический импорт, Vite и сервер Nitro.
  • Система управления контентом на основе файлов. Создавайте свой контент в формате Markdown, YML, CSV или JSON и запрашивайте его в своих компонентах.
  • Конструктор запросов. Запрашивайте свой контент с помощью API, похожего на MongoDB, чтобы получить нужные данные в нужное время.
  • Синтаксис MDC: используйте свои компоненты Vue в файлах Markdown, поддерживая пропсы, слоты и вложенные компоненты.
  • Подсветка кода. Отображайте красивые блоки кода на своем веб-сайте с интеграцией Shiki, поддерживающей темы VS Code.
  • Развертывание везде: Nuxt Content поддерживает размещение как на статическом сервере, так и на сервере Node.

Важной функцией, представленной в этой новой версии Nuxt Content, помимо того факта, что теперь она поддерживает Nuxt 3, является добавление синтаксиса MDC, который позволяет вам добавлять компоненты Vue в ваши файлы .md. Nuxt Content v1 поддерживает компоненты Vue, но v2 имеет немного другой синтаксис и больше улучшений.

Мы можем использовать модуль Nuxt Content для создания любого статического сайта, для которого требуется контент. Он отлично работает для веб-сайтов, таких как документация и блоги со статическим содержимым, но не так хорош для веб-сайтов, где большая часть его содержимого является динамической или изменяется в режиме реального времени, поскольку сайт должен быть создан, чтобы изменения в файле вступили в силу. Он также отлично работает для других статических сайтов с динамическим контентом на стороне клиента с помощью JavaScript. Такие сайты, как целевые страницы, портфолио, сайты мероприятий, веб-сайты компаний и многое другое, являются хорошими вариантами использования Nuxt Content.

Давайте углубимся и посмотрим, как мы можем создать собственный сайт с помощью Nuxt Content.

Создание нашего блога

Мы можем создать новый проект контента Nuxt или добавить его в существующий проект Nuxt 3. Вы можете начать новый проект Nuxt Content с:

# with npm
npx nuxi init content-blog -t content
# with pnpm
pnpm dlx nuxi init content-blog -t content

Перейдите во вновь созданную папку ./content-blog и установите зависимости:

# with yarn
yarn install
# with npm
npm install
# with pnpm
pnpm install --shamefully-hoist

Теперь вы сможете запустить свое контент-приложение Nuxt в режиме разработки:

# with yarn
yarn dev
# with npm
npm run dev
# with pnpm
pnpm run dev

Потрясающий! Окно браузера должно автоматически открыться для http://localhost:3000.

Вы можете добавить Nuxt Content в любое время в существующий проект Nuxt 3, установив модуль @nuxt/content:

# with yarn
yarn add --dev @nuxt/content
# with npm
npm install --save-dev @nuxt/content
# with pnpm
pnpm add -D @nuxt/content

Затем добавьте @nuxt/content в раздел modules файла nuxt.config.ts:

// ./nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  content: {
    // https://content.nuxtjs.org/api/configuration
  }
})

⚠️ Для контента v2 требуется Nuxt 3. Если вы используете Nuxt 2, ознакомьтесь с документацией Content v1.

Давайте быстро установим и настроим Tailwind, Tailwind typography и Hero Icons, чтобы стилизовать наш проект. Мы будем использовать модуль [@nuxt/tailwind](https://tailwindcss.nuxtjs.org/). Мы также установим плагин формы попутного ветра:

# with yarn
yarn add --dev @nuxtjs/tailwindcss @tailwindcss/typography @heroicons/vue
# with npm
npm install --save-dev @nuxtjs/tailwindcss @tailwindcss/typography @heroicons/vue
# with pnpm
pnpm add -D @nuxtjs/tailwindcss @tailwindcss/typography @heroicons/vue

Добавьте его в раздел modules в nuxt.config.ts:

// nuxt.config.ts
// ...
export default defineNuxtConfig({
  modules: ['@nuxt/content', '@nuxtjs/tailwindcss'],
  content: {
    // https://content.nuxtjs.org/api/configuration
  }
})

Создайте tailwind.config.js, запустив:

npx tailwindcss init

Добавьте плагин типографики Tailwind в tailwind.config.js

// tailwind.config.js
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
}

Далее создадим наш файл /.assets/css/main.css:

/* ./assets/css/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Просмотреть полный код на GitHub

в файле nuxt.config.ts введите следующее:

// ./nuxt.config.ts
// ...
export default defineNuxtConfig({
  // ...
  tailwindcss: {
    cssPath: '~/assets/css/main.css',
  }
})

Запустить приложение

# with yarn
yarn dev
# with npm
npm run dev
# with pnpm
pnpm dev

🚨 Если вы столкнулись с проблемой, похожей на ту, что описана в этой проблеме, вы можете пройти через диалог, чтобы исправить ее, по крайней мере, до следующего обновления.

Создать первый пост

Вот как должна выглядеть структура нашего проекта на данный момент:

content-blog
├─ .gitignore
├─ app.vue
├─ assets
│  └─ css
│     └─ main.css
├─ content
│  ├─ about.md
│  └─ index.md
├─ nuxt.config.ts
├─ package.json
├─ pages
│  └─ [...slug].vue
├─ pnpm-lock.yaml
├─ README.md
├─ tailwind.config.js
└─ tsconfig.json

Однако, если вы добавляете Nuxt Content в существующий проект Nuxt 3, вам придется вручную создать папку ./content и создать файл ./content/index.md:

<!-- ./content/index.md -->
# Hello Content v2
This page corresponds to the `/` route of your website. You can delete it or create another file in the `content/` directory. 
Try to navigate to \[/what\](/what). These 2 pages are rendered by the `pages/[...slug].vue` component.
---
Look at the \[Content documentation\](https://content-v2.nuxtjs.org/) to learn more.

Далее, для отображения содержимого, создайте ./pages/[…slug].vue, который ловит все маршруты и отрисовывает компонент <ContentDoc />.

<!-- ./pages/[…slug].vue -->
<template>
  <main>
    <article class="prose">
      <ContentDoc />
    </article>
  </main>
</template>

<ContentDoc> и <ContentRenderer> — это два компонента, предоставляемые Content, которые отображают тело документа Markdown в формате RTF. Если мы перейдем к http://localhost:3000/, мы должны получить это:

С <ContentDoc> конечная точка выборки по умолчанию использует текущий маршрут ($route.path). Таким образом, если мы перейдем к /about, он автоматически отобразит содержимое файла ./content/about.md. Создайте файл ./content/about.md:

<!-- ./content/about.md -->
# About page
You can go back to the \[home page\](/).

Теперь, если мы нажмем ссылку /about на домашней странице, она направит и отобразит визуализированный контент из ./content/about.md.

Потрясающий. Теперь, когда мы увидели, как это работает, давайте структурируем наше приложение Nuxt. Сначала замените страницу ./pages/[…slug].vue на ./pages/index.vue, которая будет домашней страницей приложения.

<!-- ./pages/index.vue -->
<template>
  <main>
    <section class="hero-section">
      <header>
        <h1 class="font-black text-8xl">Welcome to my site</h1>
        <p>Take a look at <NuxtLink to="/blog">my blog</NuxtLink></p>
      </header>
    </section>
  </main>
</template>
<style scoped> /* ... */ </style>

Далее создайте новый файл ./pages/blog/[…slug].vue.

<!-- ./pages/blog/[…slug].vue -->
<template>
  <main>
    <article class="prose p-16 max-w-3xl m-auto">
      <ContentDoc />
    </article>
  </main>
</template>

Обратите внимание на класс .prose в <article class="prose">; это дает нам базовые стили типографики для содержания нашей статьи. Теперь давайте создадим нашу первую запись в блоге, ./content/blog/first-post.md.

<!-- ./content/blog/first-post.md -->
# My first blog post
Welcome to my first blog post using content v2 module

Вот как должен выглядеть наш первый пост в блоге:

Как вы можете видеть в Devtools, <title> страницы соответствует первому <h1> страницы (или первому # на странице Markdown). <meta name="description"> соответствует первому <p> на странице. Это значения по умолчанию, полученные из содержимого Markdown. Давайте посмотрим, как мы можем дополнительно настроить это с помощью Markdown в Nuxt Content.

Использование уценки

До сих пор мы видели, что с помощью компонента <ContentDoc /> мы можем преобразовать нашу разметку в допустимый HTML и стилизовать ее с помощью типографики TailwindCSS. Nuxt Content использует компоненты Prose для отображения тегов HTML из синтаксиса Markdown. Эти компоненты могут быть дополнительно настроены по вашему усмотрению. TailwindCSS Typography также ориентируется на эти элементы Prose и соответствующим образом стилизует их.

Компонент [ProseA](https://content.nuxtjs.org/api/components/prose#prosea) можно настроить, создав файл компонента с тем же именем в вашем каталоге ./components/content/. Вы должны использовать те же реквизиты в оригинальном компоненте, чтобы компонент работал.

Давайте добавим в наш пост больше контента — ./content/blog/first-post.md чтобы увидеть некоторые из этих компонентов в действии:

<!-- ./content/blog/first-post.md -->
# My first blog post
    Welcome to my first blog post using \[content v2 module\](https://content.nuxtjs.org/)
    Hey there! 👋🏾
    This is my first blog post learning nuxt content.
    I'm currently building it using the following:
    - Nuxt.js
    - Nuxt Content module
    - TailwindCSS
      - TailwindCSS typography
## Nuxt.js
    \[Nuxt\](https://nuxtjs.org/) is a powerful Vue framework that offers excellent development features such as server-side rendering.
```
npx nuxi init nuxt-app
    cd nuxt-app
    yarn install
    yarn dev -o
```
```
javascript
    // ./nuxt.config.ts
    import { defineNuxtConfig } from 'nuxt'
    export default defineNuxtConfig({
      // My Nuxt config
    })
```
## Nuxt content module
    Empower your NuxtJS application with \[@nuxt/content module\](https://content.nuxtjs.org/): write in a content/ directory and fetch your Markdown, JSON, YAML, XML, and CSV files through a MongoDB-like API, acting as a Git-based Headless CMS.
You can get started with Nuxt Content by installing a new project.
```
npx nuxi init content-app -t content
```
## TailwindCSS
    Rapidly build modern websites without ever leaving your HTML. \[TailwindCSS\](https://tailwindcss.com/) is A utility-first CSS framework packed with classes like `flex`, `pt-4`, `text-center`, and `rotate-90` that can be composed to build any design directly in your markup.
### TailwindCSS Typography
    \[Typography\](https://tailwindcss.com/docs/typography-plugin) is a plugin that provides a set of prose classes you can use to add beautiful typographic defaults to any vanilla HTML you don't control (like HTML rendered from Markdown or pulled from a CMS).

После сохранения мы должны увидеть изменения и отрендеренный HTML с примененным стилем.

Вы можете найти больше подробности о Prose в документации Nuxt Content.

Подсветка синтаксиса

Один очень важный компонент Prose, который мы рассмотрим, — это компонент [ProseCode](https://content.nuxtjs.org/api/components/prose#prosecode) и компонент ProseCodeInline. Nuxt Content дает нам подсветку синтаксиса из коробки с помощью Shiki. Чтобы применить темы к нашим блокам кода, мы должны настроить ./nuxt.config.ts

// ./nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  // ...
  content: {
    // https://content.nuxtjs.org/api/configuration
    highlight: {
      theme: {
        // Default theme (same as single string)
        default: 'material-palenight',
        // Theme used if `html.dark`
        dark: 'github-dark',
      }
    }
  },
  // ...
})

После перезапуска сервера мы видим примененную тему по умолчанию:

Вы можете найти больше тем, поддерживаемых из коробки. Вы также можете добавить другие темы VSCode.

Настройка метаданных страницы с помощью Front-matter

Мы также можем добавлять и изменять метаданные, добавляя блок основной информации YAML в наш файл уценки.

---
    title: 'Nuxt Content v2 is awesome!'
    description: 'This is my first article!'
    img: 'img/cover (1).JPG'
    tags: [Nuxt, Content, Learning]
   ---

Это будет отображаться на странице <head> вместо значений по умолчанию.

Мы можем добиться гораздо большего с помощью Front-matter, в который мы скоро погрузимся.

Компоненты Vue с MDC

MDC означает компоненты MarkDown. Это синтаксис, который мы можем использовать для вставки компонентов Vue непосредственно в наш Markdown. Любой компонент в каталоге components/content/ нашего приложения или глобально доступный может использоваться в файлах Markdown. Для начала создайте новый компонент в ./components/content/InfoBox.vue

<!-- ./components/content/InfoBox.vue -->
<script setup>
// import icons from HeroIcons
import { InformationCircleIcon, ExclamationIcon, BanIcon } from "@heroicons/vue/solid";
// define props in <script>
const props = defineProps(["type"]);
</script>
<template>
  <!-- Access `type` prop in Dynamic class  -->
  <div class="info-box not-prose" :class="[type]">
    <!-- Conditionally render icons based on prop -->
    <ExclamationIcon v-if="type == 'warning'" class="icon solid" />
    <BanIcon v-else-if="type == 'error'" class="icon solid" />
    <InformationCircleIcon v-else class="icon solid" />
    <details>
      <summary>
        <!-- Unamed Slot to render component content -->
        <slot />
      </summary>
      <div class="details pt-2">
        <!-- Named markdown component to render rich-text -->
        <Markdown :use="$slots.details" unwrap="p"></Markdown>
      </div>
    </details>
  </div>
</template>
<style scoped> /* ... */ </style>

Посмотреть полный код со стилями здесь

Здесь у нас есть простой компонент, который принимает type в качестве реквизита. В <template> мы назначаем свойство type динамическому классу в элементе div.info-box. При этом любая строка, которую мы передаем в реквизит type из нашего Markdown, будет действовать в нашем компоненте.
У нас также есть несколько значков, условно отображаемых на основе значения реквизита type.

Чтобы отобразить содержимое компонента, компонент должен содержать:

  • <slot /> для приема необработанного текста или другого компонента.
  • Компонент <Markdown /> для приема форматированного текста

Теперь мы используем безымянный элемент <slot/> для рендеринга контента прямо внутри элемента <summary/>. Затем мы используем именованный компонент <Markdown /> для рендеринга дополнительного контента прямо внутри элемента div.details. Мы используем идентификатор **::** для использования компонента в нашем файле Markdown. В файле ./content/blog/first-post.md добавьте следующий код:

<!-- ./content/blog/first-post.md -->
    <!-- ... -->
    ::InfoBox{type="error"}
    Here's a handy bit of information for you!
#details
    This will be rendered inside the `description` slot. _It's important_ to see how this **works**.
    \[More information can be found here\](#)
    ::
    <!-- ... -->

Просмотреть код можно здесь

Здесь вы можете увидеть, что:

  • Идентификатор {} кратко передает реквизиты компонентам, используя синтаксис key=value.
  • Слот по умолчанию отображает содержимое верхнего уровня внутри блочного компонента.
  • именованные слоты используют идентификатор # для отображения содержимого.

Вот наш компонент в действии:

Но это только верхушка айсберга того, что вы можете сделать с MDC. Вы можете найти больше полезных функций, таких как Вложенные компоненты, Встроенные компоненты и промежутки, Реквизиты YAML и многие другие на странице синтаксиса MDC в документах Nuxt Content.

API контента

Новый контент Nuxt позволяет вам создать целый веб-сайт полностью из файлов Markdown с новым режимом на основе документов. Режим управления документами создает прямую привязку между вашим каталогом content/ и вашими страницами. Контент теперь может делать запросы из посредников маршрутизации. Эти запросы разрешаются до того, как ваша страница отобразится, и предоставят доступ к компонуемым [useContent()](https://content.nuxtjs.org/api/composables/use-document-driven) в любом месте вашего приложения. Вы можете найти дополнительную информацию о режиме управления документами в документах Nuxt Content Docs.

Однако в нашем случае, чтобы добавить блог на наш сайт в тех случаях, когда мы можем попытаться добавить блог на существующий сайт, мы будем использовать функцию queryContent() для динамического извлечения контента из каталога ./content/ на любой странице или компоненте нашего сайта. application.
У нас также есть доступ к конечным точкам REST API, автоматически сгенерированным Nuxt Content для извлечения документов из каталога ./content/. Корневой путь API — /api/_content/query. Он принимает такие параметры, как:

  • path - /api/_content/query?path=/hello
    Получает документ с определенными path - ./content/hello.md
  • only - /api/_content/query?only=title
    Получить названия документов
  • sort - /api/_content/query?sort=size:1
    Сортировка списка документов
  • without - /api/_content/query?without=body
    Получение документов без включения body в ответ

Мы будем использовать больше функции queryContent() для получения наших документов. Давайте изменим ./pages/blog/[…slug.vue], чтобы использовать queryContent() для получения наших статей:

<!-- ./pages/blog/[…slug.vue] -->
<script setup>
const { path } = useRoute();
const { data } = await useAsyncData(`content-${path}`, async () => {
// fetch document where the document path matches with the cuurent route
  let article = queryContent().where({ _path: path }).findOne();
// get the surround information,
  // which is an array of documeents that come before and after the current document
  let surround = queryContent().only(["_path", "title", "description"]).sort({ date: 1 }).findSurround(path);
  return {
    article: await article,
    surround: await surround,
  };
});
// destrucure `prev` and `next` value from data
const [prev, next] = data.value.surround;
// set the meta
useHead({
  title: data.value.article.title,
  meta: [
    { name: "description", content: data.value.article.description },
    {
      hid: "og:image",
      property: "og:image",
      content: `https://site.com/${data.value.article.img}`,
    },
  ],
});
</script>
<template>
  <main id="main" class="article-main">
    <header v-if="data.article" class="article-header">
      <div class="img-cont h-72 mb-12">
        <img :src=`/${data.article.img}` :alt="data.article.title" class=" rounded-2xl" />
      </div>
      <h1 class="heading">{{ data.article.title }}</h1>
      <p class="supporting">{{ data.article.description }}</p>
      <ul class="article-tags">
        <li class="tag" v-for="(tag, n) in data.article.tags" :key="n">{{ tag }}</li>
      </ul>
    </header>
    <hr />
    <section class="article-section">
      <aside class="aside">
        <!-- Toc Component -->
      </aside>
      <article class="article">
        <!-- render document coming from query -->
        <ContentRenderer :value="data.article">
          <!-- render rich text from document -->
          <MarkdownRenderer :value="data.article" />
          <!-- display if document content is empty -->
          <template #empty>
            <p>No content found.</p>
          </template>
        </ContentRenderer>
      </article>
    </section>
    <!-- PrevNext Component -->
  </main>
</template>
<style scoped> /* ... */ </style>

Полный код со стилями можно посмотреть здесь

Как видите, мы больше не используем компонент <ContentDoc /> для рендеринга нашего документа. В <script> мы используем useAsyncData для получения документа с queryContent() на основе текущего пути, полученного от useRoute(). Внутри useAsyncData мы делаем запросы для двух вещей:

  • article — это содержимое документа
  • surround — это массив из двух документов, которые идут до и после текущего документа. Мы будем использовать это для создания нашей предыдущей и следующей функциональности статьи.

Установить мета-страницу с помощью useHead()

Кроме того, важно отметить, что теперь мы сами заполняем страницу <head>, используя useHead(). Что-то <ContentDoc /> сделал автоматически. Теперь мы можем использовать метод Nuxt useHead() для добавления метаданных в наш файл <head>.

// set the meta
useHead({
  title: data.value.article.title,
  meta: [
    { name: "description", content: data.value.article.description },
    {
      hid: "og:image",
      property: "og:image",
      content: `https://site.com/${data.value.article.img}`,
    },
  ],
});

Посмотреть код здесь

Вы можете увидеть это в действии здесь:

Повтор сеанса с открытым исходным кодом

OpenReplay – это пакет для воспроизведения сеансов с открытым исходным кодом, который позволяет вам видеть, что пользователи делают в вашем веб-приложении, помогая вам быстрее устранять неполадки. OpenReplay размещается на собственном сервере для полного контроля над вашими данными.

Начните получать удовольствие от отладки — начните использовать OpenReplay бесплатно.

Построить компонент оглавления

Nuxt Content также предоставляет аккуратную и настраиваемую таблицу данных контента или просто toc. Для сборки компонента создайте новый файл ./components/Toc.vue.

<!-- ./components/Toc.vue -->
<script setup>
// define links prop
defineProps(["links"]);
// flatten TOC links nested arrays to one array
const flattenLinks = (links) => {
  let _links = links
    .map((link) => {
      let _link = [link];
      if (link.children) {
        // recursively flatten children links
        let flattened = flattenLinks(link.children);
        _link = [link, ...flattened];
      }
      return _link;
    })
    .flat(1);
  return _links;
};
</script>
<template>
  <nav class="toc">
    <header class="toc-header">
      <h3 class="text-xl font-bold">Table of contents</h3>
    </header>
    <ul class="toc-links">
      <!-- render each link with depth class -->
      <li v-for="link of flattenLinks(links)" :key="link.id" :class=`toc-link _${link.depth}`>
        <a :href=`#${link.id}`>
          {{ link.text }}
        </a>
      </li>
    </ul>
  </nav>
</template>
<style scoped> /* ... */ </style>

Просмотреть полный код со стилями здесь

По умолчанию объект toc, возвращаемый queryContent(), содержит вложенные элементы внутри ключа children. Вот пример:

{
    "title": "",
    "searchDepth": 5,
    "depth": 5,
    "links": [
        {
            "id": "my-first-blog-post",
            "depth": 2,
            "text": "My first blog post"
        },
        {
            "id": "nuxtjs",
            "depth": 2,
            "text": "Nuxt.js"
        },
        {
            "id": "nuxt-content-module",
            "depth": 2,
            "text": "Nuxt content module"
        },
        {
            "id": "tailwindcss",
            "depth": 2,
            "text": "TailwindCSS",
            "children": [
                {
                    "id": "tailwindcss-typography",
                    "depth": 3,
                    "text": "TailwindCSS Typography"
                }
            ]
        }
    ]
}

Вы можете видеть, что объект ссылки «tailwindcss» содержит «дочерние элементы», которые содержат массив ссылок. Чтобы свести весь массив toc.links в один плоский массив, мы создали простую вспомогательную функцию flattenLinks(). Теперь мы можем подключить его к странице. На странице ./pages/blog/[…slug].vue добавьте компонент и передайте свойство links.

<!-- ./pages/blog/[…slug.vue] -->
<!-- ... -->
<template>
  <main id="main" class="article-main">
    <header v-if="data.article" class="article-header"> <!-- ... --> </header>
    <hr />
    <section class="article-section">
      <aside class="aside">
        <!-- Toc Component -->
        <Toc :links="data.article.body.toc.links" />
      </aside>
      <article class="article"> <!-- ... --> </article>
    </section>
  </main>
</template>

Посмотреть код здесь

Вот как должен выглядеть наш компонент

Построить компонент «Предыдущая и следующая статья»

Давайте посмотрим, как мы можем создать еще один компонент, чтобы помочь пользователям легко перемещаться между сообщениями. Создайте новый файл ./components/PrevNext.vue

<!-- ./components/PrevNext.vue -->
<script setup>
// import icons
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/vue/outline";
// define prev and next props
defineProps(["prev", "next"]);
</script>
<template>
  <ul class="prev-next-cont">
    <li class="link-item prev">
      <NuxtLink v-if="prev" :to="prev._path">
        <ArrowLeftIcon class="icon stroke" />
        <span> {{ prev.title }} </span>
      </NuxtLink>
    </li>
    <li class="link-item next">
      <NuxtLink v-if="next" :to="next._path">
        <span> {{ next.title }} </span>
        <ArrowRightIcon class="icon stroke" />
      </NuxtLink>
    </li>
  </ul>
</template>
<style scoped> /* ... */ </style>

Посмотреть полный код здесь

Теперь мы можем передать данные prev и next компоненту в ./pages/[slug].vue.

<!-- ./pages/blog/[…slug.vue] -->
<template>
  <main id="main" class="article-main">
    <header v-if="data.article" class="article-header"> <!-- ... --> </header>
    <hr />
    <section class="article-section"> <!-- ... --> </section>
    <!-- PrevNext Component -->
    <PrevNext :prev="prev" :next="next" />
  </main>
</template>

Посмотреть код здесь

Давайте создадим еще несколько статей и удалим документы, которые больше не нужны, чтобы увидеть это в действии. Вот как теперь должна выглядеть наша папка ./content:

├─ content
│  └─ blog
│     ├─ first-post.md
│     ├─ second-post.md
│     └─ third-post.md

А вот и компонент <PrevNext /> во втором посте нашего блога. Вы можете видеть, что предыдущий пост является нашим первым постом, а следующий пост — нашим третьим постом.

🎖 Контент Nuxt также позволяет упорядочивать контент, добавляя к файлам числовые префиксы. Подробнее о заказе читайте в документации Nuxt Content

Список всех статей

Чтобы просмотреть все статьи, создайте новый файл ./pages/blog/index.vue. Мы будем использовать компонент <ContentList />, предоставленный Nuxt Content, для вывода списка всех документов по пути ./content/blog.

<!-- ./pages/blog/index.vue -->
<script setup>
// set meta for page
useHead({
  title: "All articles",
  meta: [{ name: "description", content: "Here's a list of all my great articles" }],
});
</script>
<template>
  <main>
    <header class="page-heading">
      <div class="wrapper">
        <h1 class="text-5xl font-extrabold">All articles</h1>
        <p class="font-medium text-lg">Here's a list of all my great articles</p>
      </div>
    </header>
    <section class="page-section">
      <!-- Render list of all articles in ./content/blog using `path` -->
      <!-- Provide only defined fields in the `:query` prop -->
      <ContentList
        path="/blog"
        :query="{
          only: ['title', 'description', 'tags', '_path', 'img'],
        }"
      >
        <!-- Default list slot -->
        <template v-slot="{ list }">
          <ul class="article-list">
            <li v-for="article in list" :key="article._path" class="article">
              <NuxtLink :to="article._path">
                <div class="wrapper">
                  <div class="img-cont w-32">
                    <img :src=`/${article.img}` :alt="article.title" class="rounded-lg max-h-[8rem]" />
                  </div>
                  <header>
                    <h1 class="text-2xl font-semibold">{{ article.title }}</h1>
                    <p>{{ article.description }}</p>
                    <ul class="article-tags">
                      <li class="tag !py-0.5" v-for="(tag, n) in article.tags" :key="n">{{ tag }}</li>
                    </ul>
                  </header>
                </div>
              </NuxtLink>
            </li>
          </ul>
        </template>
<!-- slot to display message when no content is found -->
        <template #not-found>
          <p>No articles found.</p>
        </template>
      </ContentList>
    </section>
  </main>
</template>
<style scoped> /* ... */ </style>

Просмотреть полный код со стилями здесь

Здесь мы передаем два параметра в <ContentList />:

  • path: путь к контенту для загрузки из источника контента. Здесь мы указали /blog, так как хотим отображать содержимое из каталога /blog.
  • query: запрос для передачи queryContent(). Поскольку этот компонент использует queryContent() внутри, мы можем передавать запросы для точной настройки результатов. Например, мы использовали запрос only для получения только нескольких полей, возвращаемых компонентом.

Мы использовали <template *v-slot*="{ list }"> для отображения содержимого и <template *#not-found*> для резервного копирования, когда содержимое не найдено. У нас должно получиться что-то вроде этого:

Потрясающий. Теперь давайте быстро создадим компонент <SiteHeader /> для нашего блога, который поможет нам легко перейти на эту страницу.

Создать компонент заголовка сайта

Создайте новый файл ./components/SiteHeader.vue

<!-- ./components/SiteHeader.vue -->
<template>
  <header class="site-header">
    <div class="wrapper">
      <NuxtLink to="/" class="no-underline">
        <figure class="site-logo">
          <h1>Site</h1>
        </figure>
      </NuxtLink>
      <nav class="site-nav">
        <ul class="links">
          <li class="link">
            <NuxtLink to="/blog">Blog</NuxtLink>
          </li>
        </ul>
      </nav>
    </div>
  </header>
</template>
<style scoped> /* ... */ </style>

Просмотреть полный код со стилями здесь

Теперь добавьте компонент в ./app.vue

<!-- ./app.vue -->
<template>
  <div>
    <SiteHeader />
    <NuxtPage />
  </div>
</template>

Посмотреть код здесь

Отображение статей по тегам

Последнее, что мы можем добавить в наш блог, — это возможность отображать статьи при определенных условиях. Мы создадим динамическую страницу tags, которую будем использовать для отображения статей по их тегам.
Создайте новую страницу-слаг ./pages/blog/tags/[slug].vue

<!-- ./pages/blog/tags/[slug].vue -->
<script setup>
// get current route slug
const {
  params: { slug },
} = useRoute();
// get array of filters by generating array from separating slug`,`
const filter = slug.split(",");
// set meta for page
useHead({
  title: `All articles with ${slug}`,
  meta: [{ name: "description", content: "Here's a list of all my great articles" }],
});
</script>
<template>
  <main>
    <header class="page-heading">
      <div class="wrapper">
        <h1 class="text-5xl font-extrabold">All articles with "{{ slug }}"</h1>
        <p class="font-medium text-lg">Here's a list of all my great articles</p>
      </div>
    </header>
    <section class="page-section">
      <!-- Render list of all articles in ./content/blog using `path` -->
      <!-- Provide only defined fieldsin the `:query` prop -->
      <ContentList
        path="/blog"
        :query="{
          only: ['title', 'description', 'tags', '_path', 'img'],
          where: {
            tags: {
              $contains: filter,
            },
          },
          $sensitivity: 'base',
        }"
      >
        <!-- Default list slot -->
        <template v-slot="{ list }">
          <ul class="article-list">
            <li v-for="article in list" :key="article._path" class="article-item">
              <NuxtLink :to="article._path">
                <div class="wrapper">
                  <div class="img-cont w-32">
                    <img :src=`/${article.img}` :alt="article.title" class="rounded-lg max-h-[8rem]" />
                  </div>
                  <header>
                    <h1 class="text-2xl font-semibold">{{ article.title }}</h1>
                    <p>{{ article.description }}</p>
                    <ul class="article-tags">
                      <li class="tag" v-for="(tag, n) in article.tags" :key="n">
                        <NuxtLink :to=`/blog/tags/${tag}` class="underline"> {{ tag }} </NuxtLink>
                      </li>
                    </ul>
                  </header>
                </div>
              </NuxtLink>
            </li>
          </ul>
        </template>
        <!-- Not found slot to display message when no content us is found -->
        <template #not-found>
          <p>No articles found.</p>
        </template>
      </ContentList>
    </section>
  </main>
</template>

Посмотреть полный код здесь

Эта страница очень похожа на нашу страницу ./pages/blog/index.vue. На этой странице мы добавили реквизит :query и передали этот запрос:

{
  only: ['title', 'description', 'tags', '_path', 'img'],
  where: {
    tags: {
      $contains: filter,
    },
  },
  $sensitivity: 'base',
}

Посмотреть код здесь

Этот запрос позволяет нам перечислять статьи с тегами, содержащими тег, указанный в filter.
Теперь, чтобы пользователи могли легко перемещаться по веб-сайту и изучать каждый тег, нам нужен способ получить все теги из всех статей. .
Для этого нам нужно создать компонент <Tags />.

Создайте компонент «Теги»

Этот компонент будет использовать queryContent() для извлечения тегов из каждой статьи и отображения их в виде ссылок, которые будут перенаправлены на страницу /tags/[slug] с тегом в качестве ярлыка. Создайте новый компонент ./components/Tags.vue:

<!-- ./components/Tags.vue -->
<script setup>
// import icon
import { TagIcon } from "@heroicons/vue/solid";
// tag list state
const expanded = ref(false);
// helper function to flatten tags array
const flatten = (tags, key) => {
  let _tags = tags
    .map((tag) => {
      let _tag = tag;
      if (tag[key]) {
        let flattened = flatten(tag[key]);
        _tag = flattened;
      }
      return _tag;
    })
    .flat(1);
  return _tags;
};
// function to toggle expanded state
const toggleExpand = () => {
  expanded.value = !expanded.value;
};
// get only tags data from `/blog`
const { data } = await useAsyncData("tags", () => queryContent("blog").only(["tags"]).find());
// generate array without duplicates from flattened array
const articleTags = [...new Set(flatten(data.value, "tags"))];
</script>
<template>
  <div class="tag-list" :class="{ active: expanded }">
    <!-- Button to toggle expand -->
    <button @click="toggleExpand" class="cta w-icon">
      <TagIcon class="icon solid" />
      <span>Tags</span>
    </button>
    <ul class="article-tags" :class="{ expanded: expanded }">
      <!-- list out tags with links -->
      <li v-for="(tag, n) in articleTags" :key="n" class="tag">
        <NuxtLink :to=`/blog/tags/${tag}` class="font-semibold"> {{ tag }} </NuxtLink>
      </li>
    </ul>
  </div>
</template>
<style scoped>/* ... */</style>

Просмотреть полный код со стилями здесь

Здесь мы используем queryContent() для извлечения тегов из всех статей, сглаживаем их с помощью вспомогательной функции flatten(), генерируем массив из Set сглаженного массива для удаления дубликатов и назначаем его articleTags.
Затем в <template>, мы отображаем список <NuxtLink />, которые направляются к /blog/tags/${tag}. Если мы нажмем на тег Nuxt, он направит нас к /blog/tags/Nuxt, в котором перечислены все статьи с этим тегом на этой странице.

Добавьте этот компонент на страницу ./pages/blog/index.vue

<!-- ./pages/blog/index.vue -->
<template>
  <main>
    <header class="page-heading"> <!-- ... --> </header>
    <section class="page-section">
      <Tags />
      <!-- ... -->
    </section>
  </main>
</template>

Посмотреть код здесь

и страница ./pages/blog/tags/[slug].vue.

<!-- ./pages/blog/tags/[slug].vue -->
<template>
  <main>
    <header class="page-heading"> <!-- ... --> </header>
    <section class="page-section">
      <Tags />
      <!-- ... -->
    </section>
  </main>
</template>

Посмотреть код здесь

Большой! Давайте посмотрим на это в действии.

Отлично!
Посмотреть развернутую версию можно здесь.

Заключение

На данный момент нам удалось создать блог с помощью Nuxt 3 и модуля Nuxt Content v2. Мы создали маршрут /blog, на котором размещены все записи и статьи нашего блога.

В настоящее время все наши документы находятся в папке ./content/. Контент, отображаемый в маршруте /blog нашего веб-сайта, отображает документы из папки ./content/blog нашего проекта.

Мы также можем создать больше страниц на нашем веб-сайте, создав новую подпапку в папке ./content/.
Допустим, мы хотим иметь маршрут /snippets, где у нас есть короткие документы о некоторых полезных фрагментах кода, мы можем легко создать новую подпапку ./content/snippets/ и создавать документы в этой папке. Затем мы можем сделать что-то подобное для маршрута /snippets, как мы сделали для маршрута /blog нашей страницы, и создать динамическую страницу, например ./pages/snippets/[slug].vue. Мы можем использовать компонент <ContentDoc /> для рендеринга контента для каждого документа или напрямую использовать queryContent().

Это показывает, как мы можем легко отображать контент на нашем сайте без необходимости управлять внешней базой данных или API для управления контентом.

Мы можем дополнительно добавить больше функций и возможностей в наш проект на базе Nuxt Content, ознакомившись с документацией по API, руководствами, конфигурацией и примерами.

Расширенный раздел в документации Nuxt Content полезен, поскольку он показывает, как хуки позволяют вам дополнительно контролировать и настраивать опыт.

Пример в расширенном разделе показывает как использовать первое изображение в документе в качестве обложки.

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

Ресурсы и дополнительная литература

Вы можете просмотреть код проекта и живой пример:

Что касается дальнейшего чтения,

Первоначально опубликовано на blog.openreplay.com 13 июля 2022 г.