DEV Community

Cover image for How to add a blog using as a CMS to a Next.js website
James Wallis
James Wallis

Posted on • Updated on • Originally published at

How to add a blog using as a CMS to a Next.js website

For a shorter introduction (about half the length) check out "I completely rewrote my personal website using as a CMS".


I've been posting on for a few months now. I love the platform, the editor, the ability to draft, edit and publish an article making it available to the millions of users.

Recently, I decided that I wanted to present them on my own website. After researching different ways to achieve this, I concluded using the API to create the blog section of my website would be the perfect solution. I decided that articles would only show up on my website if I'd added a canonical URL to the article on - meaning my website is seen as the source of the article (even though it was written on

Continuing to use also means that I don't need to configure storage for saving the articles or any images used. Additionally, I can take advantage of the built-in RSS feed which other blogging sites can read to automatically import my articles.

I came up with the following list of requirements:

  1. Use the API to fetch all my articles and display them on my website.
  2. Fetch and render each article at build time to ensure the website would be fast and to ensure good SEO for the individual blog pages. Using dynamic pages would make the website load slower as it would query the API on the client-side and also mean that I would have the same SEO data, such as page title, for each blog page.
  3. Set the canonical URL of an article on and have that be the article's URL on my website. I wanted to continue to use the editor to write and manage my articles, so they should only show on my website once I've added a canonical URL.
  4. Have a nice URL for the blog posts on my website that I would be in complete control of. Neither the post ID nor the path to the article.
  5. Rebuild each time an article is created or updated. This was crucial as the blog would be static - I didn't want to press the rebuild each time I changed something.

I was able to achieve all of this using a combination of Next.js dynamic pages, Vercel deploy hooks and the public API.

Setting up the project

Key technologies used

  1. TypeScript - if you prefer plain JavaScript for code examples, this GitHub repository has the same functionality as described below but is purely JavaScript.
  2. Next.js, React.js etc (required to create a Next.js app).
  3. Tailwind CSS, Tailwind CSS Typography plugin (for styling).
  4. Remark Markdown parser and plugins such as remark-html to convert the Markdown returned by the API to HTML. Other plugins I use enable features such as code highlighting, GitHub flavour Markdown compatibility (for strikethrough etc) and stripping out Front Matter from the displayed HTML.
  5. The API and it's endpoint.
  6. Vercel deploy hooks. I use Vercel to host my Next.js site and their deploy hooks allow me to rebuild my website automatically when an article is added or edited on

To see all the packages I'm currently using on my website, check out the package.json on GitHub.

The two Next.js functions that run my website

My personal website is built using Next.js. To ensure that all content continued to be generated at build time, I used two built-in Next.js functions that can be used to fetch data for pre-rendering. These are:

  • getStaticProps - fetch data from a source (think API or file) and pass it into the component via props.
  • getStaticPaths- provides the ability to use dynamic routes with a static site.

I'll be using both functions to make the dynamic article page called [slug].ts - the square brackets denote that it is a Next.js dynamic page and the name slug is the name of the parameter that will be passed into getStaticProps from getStaticPaths.

How do I determine which articles appear on my website?

For articles to appear on my website they have to have a canonical URL pointing at

Whenever I refer to the page slug I'm referring to the last section of the canonical URL (after /blog). When reading the canonical URL from the API I use the following function to convert the URL to the slug.

const websiteURL = '';

// Takes a URL and returns the relative slug to your website
export const convertCanonicalURLToRelative = (canonicalURL) => {
    return canonicalURL.replace(websiteURL, '');
Enter fullscreen mode Exit fullscreen mode

When I pass to convertCanonicalURLToRelative it will return the slug a-new-article.

How to add a blog with using as a backend

The individual article pages (/blog/${slug})


Each individual article page is generated at build time using the getStaticPaths Next.js function that fetches all my published articles, and saves them to a cache file. getStaticProps then fetches an individual article from the cache and passes it into the page component via its props.

A cache file must be used because Next.js doesn't allow passing data from getStaticPaths to getStaticProps - aside from the page slug. For this reason, the page slug is used to fetch an article from the cache file.

Flow Diagram

The diagram below should explain the process that is followed when creating dynamic pages through Next.js using the getStaticPaths and getStaticProps functions. It outlines the most important function calls, briefly explains what they do, and what is returned.

Article Page Diagram


View on GitHub

Below you will find the code that dynamically creates each article page.

import fs from 'fs';
import path from 'path';

import Layout from '../../components/Layout';
import PageTitle from '../../components/PageTitle';
import IArticle from '../../interfaces/IArticle';
import { getAllBlogArticles, getArticleFromCache } from '../../lib/devto';

const cacheFile = '.dev-to-cache.json';

interface IProps {
    article: IArticle

const ArticlePage = ({ article }: IProps) => (
    <Layout title={article.title} description={article.description}>
            alt={`Cover image for ${article.title}`}
            className="md:mt-6 lg:mt-10 xl:mt-14 h-40 sm:h-48 md:h-52 lg:h-64 xl:h-68 2xl:h-80 mx-auto"
        <PageTitle title={article.title} center icons={false} />
        <section className="mt-10 font-light leading-relaxed w-full flex flex-col items-center">
            <article className="prose dark:prose-dark lg:prose-lg w-full md:w-5/6 xl:w-9/12" dangerouslySetInnerHTML={{ __html: article.html }} />


export async function getStaticProps({ params }: { params: { slug: string }}) {
    // Read cache and parse to object
    const cacheContents = fs.readFileSync(path.join(process.cwd(), cacheFile), 'utf-8');
    const cache = JSON.parse(cacheContents);

    // Fetch the article from the cache
    const article: IArticle = await getArticleFromCache(cache, params.slug);

    return { props: { article } }

export async function getStaticPaths() {
    // Get the published articles and cache them for use in getStaticProps
    const articles: IArticle[] = await getAllBlogArticles();

    // Save article data to cache file
    fs.writeFileSync(path.join(process.cwd(), cacheFile), JSON.stringify(articles));

    // Get the paths we want to pre-render based on posts
    const paths ={ slug }) => {
        return {
            params: { slug },

    // We'll pre-render only these paths at build time.
    // { fallback: false } means other routes should 404.
    return { paths, fallback: false }

export default ArticlePage
Enter fullscreen mode Exit fullscreen mode

The flow diagram above combined with the comments throughout the code should enable a full understanding of the code. If you have any questions, comment below.

You'll notice that two functions are called from the lib/dev.ts file. getArticleFromCache does what it suggests, it finds an article in the cache and returns it. getAllBlogArticles, on the other hand, is the function that fetches all my articles from and converts the supplied markdown into HTML - using functions from lib/markdown.ts.

import axios, { AxiosResponse } from 'axios';
import IArticle from '../interfaces/IArticle';
import ICachedArticle from '../interfaces/ICachedArticle';
import { convertMarkdownToHtml, sanitizeDevToMarkdown } from './markdown';

const username = 'jameswallis'; // My username
const blogURL = ''; // Prefix for article pages

// Takes a URL and returns the relative slug to your website
export const convertCanonicalURLToRelative = (canonical: string) => {
    return canonical.replace(blogURL, '');

// Takes the data for an article returned by the API and:
// * Parses it into the IArticle interface
// * Converts the full canonical URL into a relative slug to be used in getStaticPaths
// * Converts the supplied markdown into HTML (it does a little sanitising as allows markdown headers (##) with out a trailing space
const convertDevtoResponseToArticle = (data: any): IArticle => {
    const slug = convertCanonicalURLToRelative(data.canonical_url);
    const markdown = sanitizeDevToMarkdown(data.body_markdown);
    const html = convertMarkdownToHtml(markdown);

    const article: IArticle = {
        // parse into article object
    return article;

// Filters out any articles that are not meant for the blog page
const blogFilter = (article: IArticle) => article.canonical.startsWith(blogURL);

// Get all users articles from
// Use the authenticated article route to get the article markdown included
export const getAllArticles = async () => {
    const params = { username, per_page: 1000 };
    const headers = { 'api-key': process.env.DEVTO_APIKEY };
    const { data }: AxiosResponse = await axios.get(``, { params, headers });
    const articles: IArticle[] =;
    return articles;

// Get all articles from meant for the blog page
export const getAllBlogArticles = async () => {
    const articles = await getAllArticles();
    return articles.filter(blogFilter);

// Get my latest published article meant for the blog (and portfolio) pages
export const getLatestBlogAndPortfolioArticle = async () => {
    const articles = await getAllArticles();
    const [latestBlog] = articles.filter(blogFilter);
    const [latestPortfolio] = articles.filter(portfolioFilter); // ignore this! It's meant for another page (see the GitHub repository for more information)
    return [latestBlog, latestPortfolio];

// Gets an article from using the ID that was saved to the cache earlier
export const getArticleFromCache = async (cache: ICachedArticle[], slug: string) => {
    // Get minified post from cache
    const article = cache.find(cachedArticle => cachedArticle.slug === slug) as IArticle;
    return article;
Enter fullscreen mode Exit fullscreen mode

The key points to note about the devto.ts file is:

  1. I've used the authenticated endpoint to fetch all my articles from This endpoint is the only one that returns all my articles (ok, 1000 max...) and includes the article markdown. Authenticating also gives a slightly higher API limit.

    • Previously I used the built-in HTML returned in the{id} but I kept hitting the API limit as each build made as many API calls as I had articles.
    • Get a API Token following the instructions on the API docs.
  2. The convertDevtoResponseToArticle function converts the markdown into HTML using a function from the lib/markdown.ts.

import unified from 'unified';
import parse from 'remark-parse';
import remarkHtml from 'remark-html';
import * as highlight from 'remark-highlight.js';
import gfm from 'remark-gfm';
import matter from 'gray-matter';
import stripHtmlComments from 'strip-html-comments';

// Corrects some Markdown specific to
export const sanitizeDevToMarkdown = (markdown: string) => {
    let correctedMarkdown = '';

    // sometimes turns "# header" into "#&nbsp;header"
    const replaceSpaceCharRegex = new RegExp(String.fromCharCode(160), "g");
    correctedMarkdown = markdown.replace(replaceSpaceCharRegex, " ");

    // allows headers with no space after the hashtag (I don't use # on due to the title)
    const addSpaceAfterHeaderHashtagRegex = /##(?=[a-z|A-Z])/g;
    return correctedMarkdown.replace(addSpaceAfterHeaderHashtagRegex, '$& ');

// Converts given markdown into HTML
// Splits the gray-matter from markdown and returns that as well
export const convertMarkdownToHtml = (markdown: string) => {
    const { content } = matter(markdown);

    const html = unified()
        .use(gfm) // Allow GitHub flavoured markdown
        .use(highlight) // Add code highlighting
        .use(remarkHtml) // Convert to HTML

    return String(html);
Enter fullscreen mode Exit fullscreen mode

This file is pretty simple; the comments should explain everything, so I won't add anything more. If you'd like to learn more about using Remark converts with Next.js, you can read my blog titled "How to use the Remark Markdown converters with Next.js projects".


Phew, that was a lot. Hopefully, I didn't lose you in the code examples and explanations!

Everything above explains how I've built the dynamic article pages on my website. I've included all the code that you'll need to create the dynamic blog pages on your own website.

By the way, when the code above is compiled it produces an article page such as

article page screenshot

Let's move onto the blog overview page (

The article overview page (/blog)

Building a page for each of your articles at build time is great but how will a user find them without an overview page?! They probably won't!


The overview page is much simpler than the dynamic article pages and only uses functions from the lib/devto.ts file introduced above. So this section will be shorter than the last.

Flow Diagram

As before, I've made a diagram to display the process followed when displaying all the article summaries on the overview page. You'll notice that this time I'm only using getStaticProps rather than getStaticProps and getStaticPaths. This is because I'm only loading data for one page rather than creating dynamic pages (which is what getStaticPaths allows you to do).

Overview page diagram


View on GitHub

import Layout from '../components/Layout'
import PageTitle from '../components/PageTitle'
import Section from '../components/Section'
import ArticleCard from '../components/ArticleCard'
import IArticle from '../interfaces/IArticle'
import { getAllBlogArticles } from '../lib/devto'

interface IProps {
    articles: IArticle[]

const title = "Blog ✍️"
const subtitle = "I share anything that may help others, technologies I\'m using and cool things I\'ve made."

const BlogPage = ({ articles }: IProps) => (
    <Layout title={title} description={subtitle}>

        <Section linebreak>
            {{ title, description, publishedAt, tags, canonical }) => (

export async function getStaticProps() {
    // Get all the articles that have a canonical URL pointed to your blog
    const articles = await getAllBlogArticles();

    // Pass articles to the page via props
    return { props: { articles } };

export default BlogPage
Enter fullscreen mode Exit fullscreen mode

Essentially the above code:

  1. Loads the articles from the API
  2. Passes them into the component
  3. Maps over each article and creates a summary card for each which links to the dynamic article page created in the previous step.

The overview page looks like this:
Overview Page screenshot


Amazing, that's the overview page complete! If you're following along you should now have:

  1. Blog pages being created dynamically
  2. An overview page that links to the dynamic blog pages

Rebuild each time an article is created or updated

The final step that I took to create my powered website is to set up a Vercel deploy hook. My website is hosted on Vercel so I am able to use a deploy hook to programmatically trigger a rebuild, refreshing the article content in the process.

Deploy Hooks allow you to create URLs that accept HTTP POST requests in order to trigger deployments and re-run the Build Step.

To trigger the deploy hook, I have created a API webhook that calls it each time an article is created or updated.

Configuring the automatic rebuild

A prereq for this section is that you're website needs to be deployed onto Vercel. I've created instructions on how to do this.

To create a deploy hook, follow the Vercel documentation - it's a lot more simple than you'd think.

Once you have the deploy URL we can use the API to create a webhook to trigger it.

You can do this using curl (make sure you add your API_KEY and change the target_url to be your Vercel deploy hook URL):

curl -X POST -H "Content-Type: application/json" \
  -H "api-key: API_KEY" \
  -d '{"webhook_endpoint":{"target_url":"","source":"DEV","events":["article_created", "article_updated"]}}' \
Enter fullscreen mode Exit fullscreen mode

For more information, see the API docs.


Nice one, now your website will automatically redeploy each time you create or update an article on!

Next steps

I love my website right now and using to manage most of its content has made adding content much more efficient than previously. However, there are a couple of things I want to improve in the future:

  • If a user is viewing a blog on and it links to another of my articles, the user should stay on But if they're on, they should stay on it rather than being taken to
  • Another user made a comment in another of my articles and made the point that if suddenly turned off, I'd lose my articles. However unlikely, I want to set up a system to take daily backups of my articles to mitigate the risk of losing them.

Round up

In this article, I've taken you through the code that allows to power my website. If you venture onto my GitHub you'll see that in addition to having a blog section (, I also use to display my portfolio entries (

If you want more background on why and how I've used the API to power my website, read my initial post discussing it.

If you found this article interesting or it has helped you to use Next.js and the API to build your own website using as a CMS, drop me a reaction or let me know in the comments!

Anything I can improve? Let me know in the comments.

Thanks for reading!

PS, I'm currently deciding whether I should create a tutorial series that will take you through building a powered blog from scratch - is this something you would read/follow?

Top comments (19)

joshmedeski profile image
Josh Medeski

Great post James!

Have you thought of migrating to the Next.js Incremental Static Regeneration (ISR) feature? You can reduce the amount of times you have to rebuild the whole site but just allowing each page to regenerate it's static state on the revalidation time you set.

Docs for ISR:

jameswallis profile image
James Wallis

Hi Josh, thanks!
Yes in hindsight it would've been a lot easier to use ISR!
I originally decided that I wouldn't use it because that first user that hits it will not see the latest post - in reality, that user is going to be me aha. In any case it was fun to mess around with Vercel's deploy hooks.

siddharthroy profile image
Siddharth Roy • Edited

The webhook api is no longer working

jameswallis profile image
James Wallis

Aha great! Thanks for bringing this to my attention.

If you see this comment and are using the API with Next.js then Incremental Static Regeneration is the way forward. I'll have to migrate my own site to this in the future!

Side note: I'm guessing the PR to remove webhooks was this one:

jastuccio profile image

Thanks James! I went to look at your code. One of your "view on github" links is broken:

It seems the link should point to a tsx file instead of ts

works for me:

jameswallis profile image
James Wallis

Great spot, I’ve fixed it! Thanks

zakhargz profile image
Zak Hargreaves

Hey! This is a great article! I've been trying to achieve the same thing - On the last part, I'd love to see another tutorial series on building a powered blog from scratch. If this is something you're looking into, and not yet started, would you like to collaborate?

jameswallis profile image
James Wallis

Hi Zak, thanks! I have an almost finished tutorial that has been saved in my drafts for weeks. I just need to find some time to finish it and split it into separate articles (as it's a bit long). I'd love to collaborate but I can't suggest anything other than proofreading and suggesting changes/additions - which would be appreciated but are not very exciting. I can share the draft and repo with you if you DM me?

zakhargz profile image
Zak Hargreaves • Edited

Hey James - That sounds ace, I don't seem to have the ability to DM you from here, but I have followed you on Twitter, I'll message you on there.

Catch you soon.

Thread Thread
jameswallis profile image
James Wallis

Sounds good!

edo78 profile image
Federico "Edo" Granata

I'm thinking about the opposite ... using my website as the only source and have get the post from there. I'd prefere to own my contents

jameswallis profile image
James Wallis

Yeah, I understand your viewpoint 100%.

Using for me is more about being able to take advantage of their tools (editor, publishing workflow, webhooks to redeploy site) without having to configure external tooling such as a CMS. I'll be building a backup system just in case decides to close down their servers!

Also, you own the rights to anything you post on

Yes, you own the rights to the content you create and post on and you have the full authority to post, edit, and remove your content as you see fit.

They also add that they have the right to store, display, reformat and distribute it.

edo78 profile image
Federico "Edo" Granata

I understand your point too. I was just intrigued by a diametrically opposite approach

Thread Thread
jameswallis profile image
James Wallis

For sure, it will be interesting to see what direction you take

Thread Thread
edo78 profile image
Federico "Edo" Granata

I haven't officialy started yet but I'm planning to use eleventy for my site and create an rss feed to use with the "Publishing to DEV Community from RSS" feature of to import my post

Thread Thread
jameswallis profile image
James Wallis

Nice, I've seen lots of Eleventy but I haven't actually looked into it yet.
My hesitation with using an RSS feed to import my posts is that I don't think they get updated if you make any changes on your website.

juliang profile image
Info Comment hidden by post author - thread only accessible via permalink
Julian Garamendy

Nice! I did the pretty much same but without the need to rebuild on every change.
I'm using incremental static regeneration instead.

aadityasiva profile image

This is awesome!!!

jameswallis profile image
James Wallis

Thank you!

Some comments have been hidden by the post's author - find out more