Content, State, Communication

Content Collections


Learning Objectives

  • You understand the need to separate content from the presentation layer.
  • You know what content management systems are and know of headless CMSs.
  • You know how to use Astro’s content collections.

Content is king

In 1996, Bill Gates popularized the statement “Content is king” as a part of an essay that focused on the opportunities of the internet. The essay included the following passages:

One of the exciting things about the Internet is that anyone with a PC and a modem can publish whatever content they can create. In a sense, the Internet is the multimedia equivalent of the photocopier. It allows material to be duplicated at low cost, no matter the size of the audience.

The Internet also allows information to be distributed worldwide at basically zero marginal cost to the publisher. Opportunities are remarkable, and many companies are laying plans to create content for the Internet.

The essay did not limit content to written text or images, but also included software:

When it comes to an interactive network such as the Internet, the definition of “content” becomes very wide. For example, computer software is a form of content-an extremely important one.

And highlighted the need to provide up-to-date and interactive content:

If people are to be expected to put up with turning on a computer to read a screen, they must be rewarded with deep and extremely up-to-date information that they can explore at will. They need to have audio, and possibly video. They need an opportunity for personal involvement that goes far beyond that offered through the letters-to-the-editor pages of print magazines.

The essay also continues to highlight the possibility of being paid for the content, discussing that advertising is not yet ready but can work in the long term, while the short-term solution could be micropayments:

For the Internet to thrive, content providers must be paid for their work. The long-term prospects are good, but I expect a lot of disappointment in the short-term as content companies struggle to make money through advertising or subscriptions. It isn’t working yet, and it may not for some time.

[…]

In the long run, advertising is promising. An advantage of interactive advertising is that an initial message needs only to attract attention rather than convey much information.

[…]

But within a year the mechanisms will be in place that allow content providers to charge just a cent or a few cents for information. If you decide to visit a page that costs a nickel, you won’t be writing a check or getting a bill in the mail for a nickel. You’ll just click on what you want, knowing you’ll be charged a nickel on an aggregated basis.

While in the end, micropayments did not become the norm, the essay correctly predicted the rise of content on the internet and the need for up-to-date and interactive content.

Loading Exercise...

Separating content from presentation

As applications have more content, building the content into the application codebase becomes less and less maintainable. To keep an application with much content maintainable, the content should be separated from the presentation layer (e.g., Svelte and Astro components).

This separation allows content to be updated without having to make changes to the codebase. It also makes it easier to manage content across multiple platforms and devices. Separating content from the presentation layer is a common practice in web development and is often done using content management systems (CMS) or headless CMSs.

Content management systems are software applications that allow users to create, manage, and publish digital content. They are often used to create websites, blogs, and other online platforms. Content management systems typically include features such as content creation, content editing, and content publishing. A headless CMS is a type of content management system that separates the content from the presentation layer. This allows developers to use the content in different ways, such as in web applications, mobile apps, and other digital platforms.

It is also possible to separate the content from presentation by simply storing the content in a structured format, such as JSON, YAML, or Markdown. This allows the content to be easily accessed and updated without having to make changes to the codebase.

Loading Exercise...

Content collections in Astro

In Astro, structured content is stored in content collections. Here, we walk through creating and using a content collection in Astro.

Markdown eXtended

Here, we look into using MDX as content. MDX (Markdown eXtended) is a format that lets you include JSX code in Markdown documents, which allows e.g. embedding Svelte components to the files.

MDX pages consist of a frontmatter section, enclosed in three dashes (---). The frontmatter section contains metadata about the page, such as the title. The content of the page follows the frontmatter section.

Here’s an example of an MDX page with a title as metadata and some content:

---
title: Hello, World!
---

# Hello, World!

Hello again!

The above would correspond roughly to the following HTML:

<h1>Hello, World!</h1>
<p>Hello again!</p>

Adding MDX support

First, in the client folder of the walking skeleton, run the following command:

deno install npm:@astrojs/mdx

This installs the Astro MDX plugin, which allows you to use MDX files as content in Astro. Then, modify the astro.config.mjs file to include the MDX plugin:

import { defineConfig } from "astro/config";
import svelte from "@astrojs/svelte";
import mdx from "@astrojs/mdx";
import deno from "@deno/astro-adapter";

export default defineConfig({
  adapter: deno(),
  integrations: [svelte(), mdx()],
});

Adding MDX content

Next, create a folder called content in the src folder. Inside the content folder, create another folder called blog. Inside the blog folder, create a file called hello-world.mdx with the following content:

---
title: Hello, World!
---

# Hello, World!

Hello, my beautiful blog!

And another file called my-impressive-entry.mdx with the following content:

---
title: My Impressive Entry
---

# My Impressive Entry

This is my impressive entry. It's so impressive!

Now, the folder src/content/blog should have two files: hello-world.mdx and my-impressive-entry.mdx.

Defining a content collection

To use the content from the blog folder, we need to define it as a content collection. Create a file called content.config.js and place it to the src folder of the client project. Add the following content to the file:

import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.mdx", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
  }),
});

export const collections = { blog };

The above definition creates a collection called blog, reading all the .mdx files from the src/content/blog folder. The schema defines that each file in the collection should have a title field of type string.

The schema definition is optional, but it’s a good practice to define it to ensure that the content is in the expected format. The schema is defined using Zod, which we briefly looked into in the Web Software Development Course.

Although here, we use content from the local file system, the data could also be loaded from a third-party service, such as a headless CMS.

Creating pages from a content collection

In Astro, pages are defined under src/pages. The pages and folders under src/pages do not have to reflect the structure of content collections, as the pages can be created dynamically. However, for simplicity and ease of debugging, it’s often a good idea to keep the structure similar.

Create a folder called blog under src/pages. Inside the blog folder, create a file called [id].astro. We’ll use the file to dynamically create pages for each blog entry. Add the following content to the file:

---
import { getCollection, render } from "astro:content";

export const getStaticPaths = async () => {
  const blogEntries = await getCollection("blog");

  return blogEntries.map((entry) => ({
    params: {
      id: entry.id?.replace(".mdx", ""),
    },
    props: {
      entry,
    },
  }));
};

const { Content } = await render(Astro.props.entry);
---

<Content />

The above code exposes a getStaticPaths function that returns a list of blog entries consisting of parameters and properties. The parameters are used for creating the content paths (the id parameter corresponds to the id in filename [id].astro), while the properties are used for the concrete content. We replace the “.mdx” suffix from the id to get a cleaner URL — this should be handled by Astro, but at least in 5.3.0 this seems to be needed.

We looked into the getStaticPaths function in the previous part of the course, when exploring how astro handles routes and rendering.

The properties are available for each generated page, where the code for the each individual page is under the getStaticPaths function. In the above example, the page-specific functionality includes rendering the content of the blog entry (received as a property from the getStaticPaths function) using the render function.

Loading Exercise...

The render function returns the content of the blog entry, in a Content component, which is then rendered in the page.

With the above functionality in place, you can now access the blog entries by their filenames (ids), e.g. http://localhost:8000/blog/hello-world and http://localhost:8000/blog/my-impressive-entry.

Figure 1 below shows the Hello, World! blog entry.

Fig. 1 -- Blog entry "Hello, World!" available at path /blog/hello-world.

Fig. 1 — Blog entry “Hello, World!” available at path /blog/hello-world.

The frontmatter of the MDX files are in the data property of the entry. For example, if we would want to display the frontmatter entry title, we would access it through Astro.props.entry.data.title on the page.

---
import { getCollection, render } from "astro:content";

export const getStaticPaths = async () => {
  // returning a collection of blog entries for Astro
};

const { Content } = await render(Astro.props.entry);
---

<Content />

<p>The title in frontmatter was {Astro.props.entry.data.title}.</p>

The frontmatter could be passed to a layout, which the content would be rendered into. The layout could then e.g. include the title in the head element of the page. Or, the page itself could contain the HTML “template”, as follows.

---
import { getCollection, render } from "astro:content";

export const getStaticPaths = async () => {
  // returning a collection of blog entries for Astro
};

const { Content } = await render(Astro.props.entry);
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{Astro.props.entry.data.title}</title>
</head>
<body>
  <Content />
</body>
</html>

Listing entries in a content collection

In the above example, we created individual pages out of the content collection with the help of the getStaticPaths function. Often, one might also want to list the entries in a collection, for example, to create an index page. In such a case, we do not need to create individual pages for each entry, but instead, we can list the entries in a single page.

Create a file called index.astro and place it to the src/pages/blog folder. Add the following content to the file:

---
import { getCollection } from "astro:content";

const blogEntries = await getCollection("blog");
---

<h1>Blog entries</h1>

<ul>
  {
    blogEntries.map((entry) => (
      <li>
        <a href={`/blog/${entry.id?.replace(".mdx", "")}`}>
          {entry.data.title}
        </a>
      </li>
    ))
  }
</ul>

Above, we read the collection and then iterate over the entries, creating a list of links to the individual blog entries. The entry.id is used to create the path to the individual blog entries, while entry.data.title from the frontmatter is used as the link text.

The syntax for looping is similar to JavaScript, but with the difference that the code is enclosed in curly braces {} and the output is wrapped in parentheses ().

With the above, the page at http://localhost:8000/blog will list the blog entries, with links to the individual entries. Figure 2 below shows the list of blog entries.

Fig. 2 -- Blog entries listed at path /blog.

Fig. 2 — Blog entries listed at path /blog.
Loading Exercise...