Blue and purple background graphics

2. List view

April 12, 2020 · Markdown Blog with Nuxt

-

2. List view

Before we get started, let's populate the blog with some dummy posts.

Content

The text content of our blog posts must live somewhere.

We'll be leveraging the Nuxt Content module for this feature:

Write in a content/ directory and fetch your Markdown files through an API, acting as a Git-based Headless CMS.

A bonus to this is that we can avoid depending on third-party services. Our blog posts live within the source code.

We'll create three dummy posts inside the content/articles/ directory.

Dummy blog post with placeholder text

List view

The list view is an index page, which will display a link to each blog post.

Card

We'll start with the individual post card. It's a simple box with a title and description.

These snippets are using Tailwind, a brilliant utility-first CSS framework.

Single card with title and description

<li class="mb-2 px-1">
  <div class="bg-gray-900 px-5 py-4">
    <p class="font-semibold text-white">Post title</p>
    <p class="text-gray-100 mt-3">This is the post's description.</p>
  </div>
</li>

Card grid

In order to display multiple posts on the screen, we'll arrange these cards into a grid.

Using flex-wrap and then wrapping the grid in a -mx-1, we'll get a neatly-spaced grid of cards that overflows to the next row once it gets too narrow.

Grid of three blog post cards

<ul class="flex flex-wrap -mx-1">
  <li class="mb-2 px-1">
    <div class="bg-gray-900 px-5 py-4">
      <p class="font-semibold text-white">Post title</p>
      <p class="text-gray-100 mt-3">This is the post's description.</p>
    </div>
  </li>
  <li class="mb-2 px-1">
    <div class="bg-gray-900 px-5 py-4">
      <p class="font-semibold text-white">Post title</p>
      <p class="text-gray-100 mt-3">This is the post's description.</p>
    </div>
  </li>
  <li class="mb-2 px-1">
    <div class="bg-gray-900 px-5 py-4">
      <p class="font-semibold text-white">Post title</p>
      <p class="text-gray-100 mt-3">This is the post's description.</p>
    </div>
  </li>
</ul>

We'll quickly tweak the cards' widths, so that they fill the page.

Using Tailwind's breakpoints, we can allow more cards per-row as the browser's width gets larger.

Grid of placeholder cards with fixed widths

<li class="w-full md:w-1/2 xl:w-1/3 mb-2 px-1">
  <div class="bg-gray-900 px-5 py-4">
    <p class="font-semibold text-white">Post title</p>
    <p class="text-gray-100 mt-3">This is the post's description.</p>
  </div>
</li>

Displaying content

We now need to make these cards reflect our content.

We'll grab our articles with the $content instance, making the list of them accessible as a data property.

Grid of cards with content from dummy posts

<script>
export default {
  async asyncData({ $content, params }) {
    const articles = await $content('articles')
      .only(['title', 'description'])
      .sortBy('createdAt', 'asc')
      .fetch()
    return {
      articles,
    }
  },
}
</script>
<ul class="flex flex-wrap -mx-1">
  <li
    v-for="article of articles"
    :key="article.title"
    class="w-full md:w-1/2 xl:w-1/3 mb-2 px-1"
  >
    <div class="bg-gray-900 bg-opacity-50 px-5 py-4">
      <p class="font-semibold text-white">{{ article.title }}</p>
      <p class="text-gray-100 mt-3">{{ article.description }}</p>
    </div>
  </li>
</ul>

I've inserted this component into my site, and added bg-opacity-50 for some neat transparency.

Grid of clickable, linked cards

Source code

Finally, I refactored this into modular components.

Here's the code in full:

<!-- components/Card.vue -->

<template>
  <li class="w-full md:w-1/2 xl:w-1/3 mb-2 px-1">
    <div
      class="bg-gray-900 bg-opacity-50 hover:bg-opacity-70 rounded px-5 py-4"
    >
      <h1 class="font-semibold text-white">{{ title }}</h1>
      <p class="text-gray-100 mt-3">{{ description }}</p>
    </div>
  </li>
</template>

<script>
export default {
  props: {
    title: { type: String, default: 'Title' },
    description: { type: String, default: 'Description' },
  },
}
</script>
<!-- components/CardGrid.vue -->

<template>
  <ul class="flex flex-wrap -mx-1">
    <Card
      v-for="post in posts"
      :key="post.title"
      :title="post.title"
      :description="post.description"
    />
  </ul>
</template>

<script>
export default {
  props: {
    posts: {
      type: Array,
      default() {
        return []
      },
    },
  },
}
</script>
<!-- pages/articles/index.vue -->

<template>
  <div class="container mx-auto px-5">
    <h1 class="text-4xl text-gray-100 uppercase font-black mt-5">Articles</h1>
    <p class="text-xl text-gray-200 mt-3">
      My explorations into tech, software and engineering.
    </p>
    <div class="mt-5">
      <CardGrid :posts="posts" />
    </div>
  </div>
</template>

<script>
export default {
  async asyncData({ $content, params }) {
    const posts = await $content('articles')
      .only(['title', 'description'])
      .sortBy('createdAt', 'asc')
      .fetch()
    return {
      posts,
    }
  },
}
</script>