I think sometimes

Building a Jamstack blog with free Cloudflare hosting and SEO

If you want to go straight to the sample code and skip the tutorial you can find it here. Please feel free to contribute.

Table of Contents

Introduction

What is Jamstack?

JAM stands for Javascript, APIs, and Markup. With Jamstack we can have an architecture that delivers a website statically whilst still providing dynamic content through Javascript.

Sometimes we need to build websites that are interactive but do not require a lot of processing of information or complex authentication services. Think of simple sites like a business landing page or a blog site in this case. Since most of the content of these websites won't change too frequently we can deliver the content statically, which means that our website for the most part is going to be pre-generated or pre-rendered by our framework.

Jamstack offers many benefits, from performance to low cost, following this tutorial should be completely free. To get more information about Jamstack you can visit this website.

Framework

The framework or static site generator that we are going to be using for this tutorial is Nuxt.js 2. This framework will handle the generation of the static files that we will be hosting for our website.

You can explore different options and create your own Jamstack with your preferred framework with this website.

Note: Make sure that you use Nuxt 2 since Nuxt 3 is not compatible with the other dependencies we are going to use.

Hosting

Thanks to Cloudflare Pages we can host static sites for free1. You can also configure a custom domain if you have one, but since this tutorial is trying to keep everything free I won't get into that.

Creating the project

Let's start by creating our Nuxt.js project. You'll need to have Node.js and npm installed on your computer.

Run the following command and follow the interactive installation wizard:

npm init nuxt-app davidservn-jamstack-blog-sample

Make sure to pick these options in the wizard:

  • Programming language: Javascript
  • Rendering mode: Universal (SSR / SSG)
  • Deployment target: Static (Static / Jamstack hosting)

Now run the following command to start the development environment:

npm run dev

If you go to localhost:3000 you should see something like this:

nuxt-js-app

NuxtJS welcome page

Installing the dependencies

Let's install the dependencies we are going to need.

Nuxt Content (@nuxt/content)

Nuxt Content will help us manage the blog posts. It is a file-based CMS, which means that our blog posts are going to be written as markdown files on our project and this library will create a pseudo-database with our blog content so that we can fetch the data and create the display for the posts and blog index.

npm install @nuxt/[email protected]^1.15.1

Note: The version of this dependency is very important, please install version 1.15.1 since newer versions only work with Nuxt 3.

Feed Module (@nuxtjs/feed)

This feed module will help us with the creation of an RSS feed for our blog.

npm install @nuxtjs/[email protected]^2.0.0

Nuxt Sitemap Module (@nuxtjs/sitemap)

Nuxt Sitemap will generate a sitemap for our website so that it can be indexed by search engines.

npm install @nuxtjs/[email protected]^2.4.0

Configuring Nuxt

Now that we have a Nuxt project with the dependencies installed we can start to configure our project.

Let's configure our environment variables:

  1. Create a file named .env on the project's root folder.

  2. Add your Base URL variable like this:

    BASE_URL=https://davidservn-jamstack-blog-sample.pages.dev/
    

Note: You can use your custom domain or wait for the hosting configuration, Cloudflare Pages will assign a free domain for your project, something like project-name.pages.dev

Open nuxt.config.js and make sure to have the following configurations:

export default {
  //...

  target: 'static',
  modules: ['@nuxt/content', '@nuxtjs/feed', '@nuxtjs/sitemap'],
  router: {
    mode: 'abstract', // HOTFIX: Double back issue, check: https://github.com/nuxt/nuxt.js/issues/9111
  },
  content: {
    liveEdit: false, // Remove live editing of files
  },

  //...
}

Creating the first blog post

Our blog posts are going to live under the content folder. Here we are going to create markdown .md files like this one:

---
title: My first post
summary: This is my first blog post.
createdAt: 2022-01-21 GMT-6
---

## Introduction

Hi this is my first blog post.

Here is a link to my [website](https://davidservn.com/).

The first fields are used as metadata for the nuxt/content library. We can access those fields when fetching the blog posts (we will see that later when creating the UI).

The name of the file will be used as the route, for example: my-first-post.md will be available at base_url/my-first-post.

Configuring the RSS feed

Open nuxt.config.js and add the following configurations:

const createFeedBlog = async (feed) => {
  const baseUrlBlog = process.env.BASE_URL
  const { $content } = require('@nuxt/content')

  feed.options = {
    title: 'The blog title',
    description: 'The blog description',
    link: baseUrlBlog,
    language: 'en',
  }

  const posts = await $content().fetch()
  posts.forEach((post) => {
    const url = baseUrlBlog + post.slug
    feed.addItem({
      title: post.title,
      id: url,
      link: url,
      description: post.summary,
      date: new Date(post.createdAt),
    })
  })
}

export default {
  //...

  feed: [
    {
      path: '/feed.xml',
      create: createFeedBlog,
      cacheTime: 1000 * 60 * 15,
      type: 'rss2',
    },
    {
      path: '/feed.json',
      create: createFeedBlog,
      cacheTime: 1000 * 60 * 15,
      type: 'json1',
    },
  ],

  //...
}

Using $content we can access the generated routes for each one of our blog posts so that we can add them to the feed list. For further customization you can reference the library's documentation.

The feeds are going to be available at the specified paths base_url/feed.xml

nuxt-feed

The website's RSS feed

Configuring the sitemap

Open nuxt.config.js and add the following configurations:

const createSitemapRoutes = async () => {
  let routes = []
  const { $content } = require('@nuxt/content')
  let posts = await $content().fetch()
  for (const post of posts) {
    routes.push(post.slug)
  }
  return routes
}

export default {
  //...

  sitemap: {
    hostname: process.env.BASE_URL,
    gzip: true,
    routes: createSitemapRoutes,
  },

  //...
}

Again, we use $content to access the generated routes for each one of our blog posts and we add them to the sitemap routes list. For further customization you can reference the library's documentation.

The sitemap by default is going to be available at base_url/sitemap.xml

nuxt-sitemap

The website's sitemap

Creating the blog post list UI

Let's open the pages/index.vue file and add the following code to add a simple list with all our blog posts:

<template>
  <div>
    <p>
      RSS Feed (
      <a target="_blank" href="/feed.xml">XML</a>
      /
      <a target="_blank" href="/feed.json">JSON</a>
      )
    </p>
    <div id="blog-list">
      <div v-for="post in posts" :key="post.slug">
        <article>
          <h1>
            <a :href="'/' + post.slug"> {{ post.title }} </a>
          </h1>
          <p>{{ post.summary }}</p>
        </article>
        <hr />
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    async asyncData({ $content }) {
      const posts = await $content()
        .only(['slug', 'title', 'summary']) // Only fetch the fields that we require to improve performance.
        .sortBy('createdAt', 'desc')
        .sortBy('title')
        .fetch()

      return {
        posts,
      }
    },
  }
</script>

In the asyncData method we can access the $content property and fetch our blog posts. In this case we use the only() method to only return a small number of fields from each post, in this case since we are displaying a list we don't need the whole body of our posts and this way we can improve the performance of our blog index.

slug is going to represent the URL for our blog post, for example, for my-first-post.md the slug is going to be my-first-post.

If we go to localhost:3000 we should see how our list looks, it's very simple but you can add some CSS styling to better reflect your personality.

blog-list

The blog post list

Creating the blog post content UI

Let's create the individual blog post display page. Here we are going to fetch a blog post depending on the URL we are loading (this is why we need the post's slug property) and we will display the post's contents.

We need to create a file named pages/_.vue and add the following code to render the body of our blog post:

<template>
  <div>
    <div id="blog">
      <article>
        <h1>{{ post.title }}</h1>
        <nuxt-content :document="post" />
      </article>
    </div>
    <p>
      Follow me on
      <a target="_blank" href="https://twitter.com/DavidServn">Twitter</a> to be
      notified about new posts or through RSS (
      <a target="_blank" href="/feed.xml">XML</a> /
      <a target="_blank" href="/feed.json">JSON</a> ).
    </p>
    <li>
      <a href="/"><span>More posts</span></a>
    </li>
  </div>
</template>

<script>
  export default {
    async asyncData({ $content, params, error }) {
      const [post] = await $content().where({ slug: params.pathMatch }).fetch()

      if (!post) {
        return error({ statusCode: 404, message: 'Post not found' })
      }

      return {
        post,
      }
    },
  }
</script>

In the asyncData method we can access the params property to read the URL path and fetch the blog post with a matching slug using the where() method.

<nuxt-content :document="post" /> will load the body of the post into our page. This content is generated by nuxt/content, it uses a remark plugin to turn our markdown files into HTML code.

If we go to localhost:3000/my-first-post we should be able to see the contents of our content/my-first-post.md file renderized in HTML.

blog-content

The blog post content

And that's it, you have a functioning blog site with an index and a way to display each blog post with generated routes. Now we can move on to hosting our site on the internet for free.

GitHub repository

In order to host our site we will first need to upload our code to a GitHub or GitLab repository in order to connect it with Cloudflare Pages and have our site automatically deployed.

Since every repository is different and we don't need any special configuration I think it's better for you to follow an official guide, here is one for GitHub.

Configuring hosting with Cloudflare Pages

We can very easily connect our GitHub repository to Cloudflare Pages and whenever we send changes to our main branch our site will be automatically deployed.

  1. Go to your Cloudflare dashboard.
  2. Click on Pages.
  3. Click on Create a project and Connect to Git.
  4. Add your GitHub account and select your repository.
  5. Select Nuxt.js under Framework preset.
  6. Make sure the Build command is nuxt generate.
  7. Under Environment variables (advanced) make sure to add your variables from your .env file.
  8. Click on Save and deploy and your website will start to build.
cloudflare-config

Example of the build configuration

If you forgot to define the environment variables you can configure them like this:

  1. Go to the Settings tab of your Cloudflare Pages project.
  2. Click on Environment variables.
  3. Add the variables from the .env file.
cloudflare-env-config

Example of the environment variables configuration

Once your site successfully builds it should automatically be deployed to the assigned free domain project-name.pages.dev and your site is now live on the internet for free!

Adding SEO to your website

Now that we have a website on the internet we can add some metadata known as SEO in order to improve our online presence and search engine rankings.

There are multiple SEO experts online with different tips and tricks, there's a whole world out there for you to explore. To keep this brief I'll only show you the bare minimum to get you started.

robots.txt

This file will help with the discoverability of your site by search engine crawlers.

Create a file named robots.txt inside the static folder with the following content:

User-agent: *
Disallow:

Open Graph tags

Open Graph tags make your site pop more when it is linked on social media. Think of those posts on Facebook or Twitter that shows you a linked website with an image and a headline.

Open nuxt.config.js and add the following metadata with your website's information:

export default {
  //...

  head: {
    title: "DavidServn's Blog",
    htmlAttrs: {
      lang: 'en',
    },
    meta: [
      { charset: 'utf-8' },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1, user-scalable=no',
      },
      {
        hid: 'robots',
        name: 'robots',
        content: 'index,follow',
      },
      {
        hid: 'googlebot',
        name: 'googlebot',
        content: 'index,follow',
      },
      {
        hid: 'description',
        name: 'description',
        content: 'This is my blog.',
      },
      { hid: 'og:title', name: 'og:title', content: "DavidServn's Blog" },
      {
        hid: 'og:description',
        name: 'og:description',
        content: 'This is my blog.',
      },
      { hid: 'og:url', name: 'og:url', content: process.env.BASE_URL },
      { hid: 'og:type', name: 'og:type', content: 'website' },
      {
        hid: 'og:image',
        name: 'og:image',
        content: process.env.BASE_URL + 'images/bg.png',
      },
      {
        hid: 'og:image:alt',
        name: 'og:image:alt',
        content: "DavidServn's Blog",
      },
      { hid: 'og:image:width', name: 'og:image:width', content: '2449' },
      { hid: 'og:image:height', name: 'og:image:height', content: '1632' },
      {
        hid: 'twitter:card',
        name: 'twitter:card',
        content: 'summary_large_image',
      },
      { hid: 'twitter:site', name: 'twitter:site', content: '@davidservn' },
      {
        hid: 'twitter:creator',
        name: 'twitter:creator',
        content: '@davidservn',
      },
      {
        hid: 'theme-color',
        name: 'theme-color',
        content: '#1b1f22',
      },
    ],
    link: [
      {
        hid: 'canonical',
        rel: 'canonical',
        href: process.env.BASE_URL,
      },
    ],
  },

  //...
}

You can go to this site to test your Open Graph tags. Add the URL of your site and the information of your website should appear on the preview.

open-graph-meta-tags-1

Example of Open Graph meta tags for the blog site

We have added the general information of our site but we should override that information when we link a blog post directly with its own information. Since we have a page for displaying the post's content we only need to open pages/_.vue and add the following metadata:

export default {
  //...

  head() {
    return {
      title: this.post.title,
      meta: [
        { hid: 'og:title', name: 'og:title', content: this.post.title },
        {
          hid: 'og:image:alt',
          name: 'og:image:alt',
          content: this.post.title,
        },
        {
          hid: 'description',
          name: 'description',
          content: this.post.summary,
        },
        {
          hid: 'og:description',
          name: 'og:description',
          content: this.post.summary,
        },
        {
          hid: 'og:url',
          name: 'og:url',
          content: process.env.BASE_URL + this.post.slug,
        },
        {
          hid: 'og:type',
          name: 'og:type',
          content: 'article',
        },
        {
          hid: 'article:published_time',
          name: 'article:published_time',
          content: this.post.createdAt,
        },
        {
          hid: 'og:image',
          name: 'og:image',
          content:
            process.env.BASE_URL +
            'images/blog/meta/' +
            this.post.slug +
            '.png',
        },
        {
          hid: 'og:image:width',
          name: 'og:image:width',
          content: '1350',
        },
        {
          hid: 'og:image:height',
          name: 'og:image:height',
          content: '900',
        },
      ],
      link: [
        {
          hid: 'canonical',
          rel: 'canonical',
          href: process.env.BASE_URL + this.post.slug,
        },
      ],
    }
  },

  //...
}

If you need more data per post you can add custom fields in the header of the content markdown files like this:

---
myCustomField: Hello
---

And we are able to access that field on the pages/_.vue file like this this.post.myCustomField.

If we now load a URL of a blog post like https://davidservn-jamstack-blog-sample.pages.dev/poem-dark-sight/ we should see the post's information on the preview.

open-graph-meta-tags-2

Example of Open Graph meta tags for an individual blog post

Adding Dependabot to your repository

Dependabot is a very useful GitHub bot that will automatically create pull requests for your project whenever a dependency receives an update. This will ensure that your project is always up to date.

If you want to configure dependabot for your project you'll need to create a folder called .github on the root of your project and inside create the following dependabot.yml file:

version: 2
updates:
  # Fetch and update latest `npm` packages
  - package-ecosystem: npm
    directory: '/'
    schedule:
      interval: monthly
      time: '00:00'
    open-pull-requests-limit: 10
    reviewers:
      - YOUR_GITHUB_USERNAME_HERE
    assignees:
      - YOUR_GITHUB_USERNAME_HERE
    commit-message:
      prefix: fix
      prefix-development: chore
      include: scope

Note: Make sure to replace YOUR_GITHUB_USERNAME_HERE with your GitHub username.


  1. Please consult the pricing for more accurate information https://pages.cloudflare.com/#pricing.

Follow me on Twitter to be notified about new posts or through RSS ( XML / JSON ).

X