Add the articles from Contentful to Nuxt3

Posted
Return to overview

The functionality of the slug pages is very similar to what we want here, so we're going to take it as a basis. Copy the `[slug].vue` file to a folder called `articles` in the `pages` folder (which creates a path for the router). In the file, replace the `<PageBody />` component with a `<ArticleBody />` component. We're basically using the capabilities of the file routing system to tell Nuxt that on the `/articles/slug-of-article-title` route, it should load the corresponding vue file with its component.

For the record, this is the entire component (change underlined):

<script lang="ts"> definePageMeta({ layout: "default", }); </script> <template> <ArticleBody :slug="$route.params.slug" /> </template>

Since we're counting on an `ArticleBody` component to exist, we should create one! Again, we take the basis of the `PageBody.vue` in the `components` folder and just rename it to `ArticleBody.vue`, then we need to replace the `Page` references to `Article` references, like so:

<script lang="ts" setup> /** * Define props */ interface Props { slug: string; } const props = defineProps<Props>(); // ---------------------------------------------------------------------------- const { title, body } = await useArticle(props.slug); </script> <template> <article> <header> <h1> {{ title }} </h1> </header> <div> {{ body }} </div> </article> </template>

We're basically only changing the composable to a new type. Also, we need to create a new composable, so that we can use it. Again, the similarities between `Pages` and `Articles` are striking.

In any real case scenario you could consider abstracting the common features to another level (or composable), but I've experienced that over time, different types of content tend to divert in feature support, so I think it's best to create a clear separation, but it's completely up to you!

The composable `useArticles` will abstract some bits though, we'll get to that. Add this to the `composables/contentful.ts` file:

//... I usually place all the interfaces at the top of the document interface Article { title: string; body: object; heroImage?: Image; }; /* Here begins the `useArticles` composable, a bit different from the `usePage`, but still similar */ export const useArticles = async (slug: string): Promise<Article> => { const key = `article` const { data } = await useAsyncData(key, async (nuxtApp) => { const { $contentfulClient } = nuxtApp return $contentfulClient.getEntries({ content_type: 'articles', 'fields.slug[in]': slug, limit: 1, }) }) const { title, body, heroImage } = data.value.items[0].fields; return { title, body, heroImage } }

After you've done all of this, you should be able to, from the local server, see the contents of that page on something like `http://localhost:3000/articles/slug-of-article`.

While we're at it, we can do a bit better, we can reuse the composable to also give us all of the `Articles` to create an overview. Bear with the changes in the composable:

export const useArticles = async (slug: string = ''): Promise<Article | Article[]> => { // Determine whether we requested one item by slug or all by no slug: const fetchAll = (slug === ''); // Set the key (for Nuxt caching the response (unique per content): const key = fetchAll ? `articles` : `article` // Set the query to provide to the contentful API: const query = fetchAll ? {} : { 'fields.slug[in]': slug, limit: 1, } const { data } = await useAsyncData(key, async (nuxtApp) => { const { $contentfulClient } = nuxtApp return $contentfulClient.getEntries({ content_type: 'post', ...query, }) }) // On this condition, the output of the query is more than one item, we want to return all items (but not all properties yet, so we map them: if (fetchAll) { return data.value.items.map((item: any) => { const { title, slug } = item.fields; return { title, slug } }) } // If we only requested one by slug, return only the one item: const { title, body, heroImage } = data.value.items[0].fields; return { title, body, heroImage } }

Do you see what we're doing? Since we're already calling `articles` from one point, we can distinguish between fetching a specific item, or all items via the `slug` property and adjust the call accordingly. So in the call we're making the `slug` optional and also describe the return value of the composable as either an `Article` interface, or an array of Articles. Neat! But you can also see some drawbacks, like having to define two possible return on the function, splitting the actual type of return in code. That's why you don't always have to write the least code to make something functional.

Next, let's add a component to list all of the existing Articles (similar to `NavBar.vue` component). Create a `ArticlesOverview.vue` component and add the following:

<script lang="ts" setup> const articles = await useArticles(); </script> <template> <nav> <a v-for="item in articles" :key="item.slug" :href="`/articles/${item.slug}`" :title="item.title" >{{ item.title }} </a> </nav> </template>

We need to add this component to a page. It's a new `index.vue` file, at the root of the `articles` folder, so the router knows that there's a route on `/articles`. Where going to simply add the `ArticlesOverview` component to that page:

<script lang="ts"> definePageMeta({ layout: "default", }); </script> <template> <ArticleOverview /> </template>

What we now should have is an overview of all Articles on `http://localhost:3000/articles` and a page belonging to that prefix with the articles' slug: `http://localhost:3000/articles/slug-of-article`.

As a final change, we should add a link the the articles section to the `NavBar.vue`. We'll add it manually for now by expanding the NavItems array:

<script lang="ts" setup> const NavPages = await usePagesNav(); const NavItems = [...NavPages, { slug: "articles", title: "Articles" }]; </script> <template> <nav> <a v-for="item in NavItems" :key="item.slug" :href="`/${item.slug}`" :title="item.title" >{{ item.title }} </a> </nav> </template>

That's it. Next up will we using article tags!

Return to overview