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
- Creating the project
- Installing the dependencies
- Configuring Nuxt
- Creating the first blog post
- Configuring the RSS feed
- Configuring the sitemap
- Creating the blog post list UI
- Creating the blog post content UI
- GitHub repository
- Configuring hosting with Cloudflare Pages
- Adding SEO to your website
- Adding Dependabot to your repository
Introduction
Jamstack?
What isJAM 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:

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/content@^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.
@nuxtjs/feed)
Feed Module (This feed module will help us with the creation of an RSS feed for our blog.
npm install @nuxtjs/feed@^2.0.0
@nuxtjs/sitemap)
Nuxt Sitemap Module (Nuxt Sitemap will generate a sitemap for our website so that it can be indexed by search engines.
npm install @nuxtjs/sitemap@^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:
-
Create a file named
.env
on the project's root folder. -
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

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

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.

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.

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.
- Go to your Cloudflare dashboard.
- Click on
Pages
. - Click on
Create a project
andConnect to Git
. - Add your GitHub account and select your repository.
- Select
Nuxt.js
underFramework preset
. - Make sure the
Build command
isnuxt generate
. - Under
Environment variables (advanced)
make sure to add your variables from your.env
file. - Click on
Save and deploy
and your website will start to build.

Example of the build configuration
If you forgot to define the environment variables you can configure them like this:
- Go to the
Settings
tab of your Cloudflare Pages project. - Click on
Environment variables
. - Add the variables from the
.env
file.

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.

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.

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.
- Please consult the pricing for more accurate information https://pages.cloudflare.com/#pricing.↩