blog posting

Create Next.js Blog

Create your own blog using Burdy CMS and Next.js.

In this blog, we will show you how we have built a simple blog with Next.js and Burdy. One of the main reasons why we have decided to build a Burdy was to be able to organize content and assets in a way other Headless CMS solutions do not offer and also have the ability to bundle Burdy together with Next.js allowing us to achieve WordPress like capabilities where Burdy will provide Admin, API, and Database while Next.js will provide Customer facing pages all in the same project.

TLDR

If you do not want to read the entire article you can just start your own blog immediately by just running:

npx create-burdy-app my-blog -t next-blog

or alternatively, you can clone our starter pack from our burdy-starter-next-blog GitHub repository run npm install and npm run dev.

Enjoy!

Single Blog Project

As stated previously instead of trying to create two separate projects such as Next.js and Headless CMS we will show you how we can create a single project where you will be able to access Admin, API, and Pages from a single application for example:

<site>/admin/* - burdy admin
<site>/api/* - burdy api
<site>/* - next.js

Connecting Burdy and Next.js

First, we will start by installing Next.js with a typescript.

npx create-next-app my-blog --ts

Secondly, in the project, we will install burdy and some dependencies that will help us build a blog.

npm i burdy burdy-web-utils axios express classnames date-fns react-jss react-bootstrap typeorm

Furthermore lets change npm scripts in the package.json

"dev": "next dev" change to "burdy dev"
"build": "next build" change to "next build && burdy build"
"start": "next start" change to "burdy start"

And now, let's connect burdy and Nextjs.

As you might already know, the entry point for the burdy backend is project/index.ts file. We can use server/init hook that will expose the root Express App of the Burdy and add nextApp render capabilities.

import next from 'next';
...
const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({dev});
const nextPublic = path.join(process.cwd(), 'public');

Hooks.addAction('server/init', async (app) => {
await nextApp.prepare();

app.use(express.static(nextPublic));

app.get('*', asyncMiddleware((req, res) => {
return nextApp.render(req, res, req.path);
}));
});

We have connected Burdy and Next.js! By running npm run dev it will start Burdy and Next.js project at the same time!

Configuring Burdy

To be able to access our posts we first need to configure Burdy and add Blogs as a content type.

Go to Admin (http://localhost:4000/admin) and access Content Types. Create new Content Type of type Post, add new field of type Rich Text and click Create.

Upon successful creation, new section blogs will appear in the side menu. On the blogs page click new and let's create our first blog.

Burdy Content Type

Upon successful creation, we can select and click Edit from the menu.

New Blog

Now we just need to write our content and press save. We have successfully configured blogs in the Burdy Admin.

Edit Blog

Extending Burdy's API

For our simple blog, we will add 2 new endpoints to the Burdy. The first endpoint (/api/blogs) will be used to retrieve list of blogs for our home page and the second endpoint (/api/blogs/:slug) will be used to retrieve the content of a single blog by its slug.

To extend API we will use api/init hook in our project/index.ts file.

Hooks.addAction('api/init', async (app) => {
app.get('/blogs', asyncMiddleware(async (req, res) => {
const postRepository = getEnhancedRepository(Post);
const qb = postRepository.createQueryBuilder('post')
.leftJoinAndSelect('post.contentType', 'contentType')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.meta', 'meta')
.leftJoinAndSelect('post.tags', 'tags')
.leftJoinAndSelect('tags.parent', 'tags.parent')
.where('post.type = :type', {type: 'post'})
.andWhere('contentType.name = :name', {name: 'blogs'})
.addOrderBy('post.updatedAt', 'DESC');

const posts = await qb.getMany();
const compiledPosts = await Promise.all(posts.map(post => compilePost(post)));
res.send(compiledPosts);
}));

app.get('/blogs/:slug', asyncMiddleware(async (req, res) => {
const postRepository = getEnhancedRepository(Post);
const qb = postRepository.createQueryBuilder('post')
.leftJoinAndSelect('post.contentType', 'contentType')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.meta', 'meta')
.leftJoinAndSelect('post.tags', 'tags')
.leftJoinAndSelect('tags.parent', 'tags.parent')
.where('post.slug = :slug', {slug: req.params.slug})
.andWhere('post.type = :type', {type: 'post'})
.andWhere('contentType.name = :name', {name: 'blogs'})
const post = await qb.getOne();
const compiled = await compilePost(post);
res.send(compiled);
}));
});

Building the blog

We will not put effort into how to build and style a blog but rather how to work with data.

For this example, we will use Next.js Server-Side Rendering (SSR) capabilities to dynamically access data.

In the pages/index.tsx we will add getServerSideProps, retrieve blogs using axios and display them on the page. Our file should look something like this:

import type { GetServerSideProps, NextPage } from 'next';
import { Container } from 'react-bootstrap';
import { createUseStyles } from 'react-jss';
import axios from 'axios';
import BlogPost from '../components/blog-post';

const useStyles = createUseStyles({
container: {
paddingTop: 40
},
blogs: {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridGap: '2rem',
'@media(max-width: 1080px)': {
gridTemplateColumns: 'repeat(2, 1fr)'
},
'@media(max-width: 767px)': {
gridTemplateColumns: '1fr'
}
},
heading: {
borderBottom: '1px solid rgba(0, 0, 0, 0.1)',
paddingBottom: '0.5rem'
}
});

const Home: NextPage = ({blogs}) => {
const classes = useStyles();

return (
<>
<Container className={classes.container}>
<h1>My Creative Corner</h1>
{blogs.length === 0 ? (
<h5>
It appears that there are no blogs here, start by creating them on&nbsp;
<a href={'http://localhost:4000/admin'} target="_blank">http://localhost:4000/admin</a>.
</h5>
) : (
<section className={classes.blogs}>
{blogs.map(blog => (
<BlogPost key={blog.id} blog={blog}/>
))}
</section>
)}
</Container>
</>
);
};

export const getServerSideProps: GetServerSideProps = async (context) => {
const blogs = await axios.get('http://localhost:4000/api/blogs');
return {
props: {
blogs: blogs.data,
}
}
};

export default Home;
We are using some components and logic that are not in this example, you can see full example on our GitHub

Next, we will create a new dynamic page that will open when the user clicks on one of the blogs displayed on the home page.

Let's create a file pages/[id].tsx and add new server-side rendering function. Your file should look something like this:

import { GetServerSideProps } from 'next';
import axios from 'axios';
import { createUseStyles } from 'react-jss';
import Image from '../components/image';
import { richtextToHtml } from 'burdy-web-utils';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { format } from 'date-fns';

const useStyles = createUseStyles({
container: {
paddingTop: 100,
},
term: {
borderRadius: 12,
padding: '4px 16px',
background: 'rgba(0, 0, 0, 0.06)',
transition: '.2s ease-in-out',
'&:hover': {
background: 'rgba(0, 0, 0, 0.08)',
}
},
termContainer: {
display: 'grid',
gridAutoFlow: 'column',
gridGap: 8,
gridAutoColumns: 'max-content',
marginBottom: 16
}
});

const BlogPage: React.VoidFunctionComponent = ({blog}) => {
const classes = useStyles();
const content = blog.meta.content;
const html = useMemo(() => richtextToHtml(content.content), [content.content]);
const terms = blog.tags.filter(tag => tag.slugPath.startsWith('term'));

return (
<>
<div className="richtext">
<div className={classNames('richtext-container', 'richtext', classes.container)}>
<div className="meta">
<div className="info">
<h1>{content.title}</h1>
<p className="subtitle">{content.description}</p>
<div className={classes.termContainer}>
{terms.map(term => (
<div key={term.id} className={classes.term}>{term.name}</div>
))}
</div>
<div className="author">
<div className="authorInfo">
<div className="authorName">
{blog?.author?.firstName ?? 'Unknown'} {blog?.author?.lastName ?? 'Author'}
</div>
<div className="authorSub">
{format(new Date(blog.updatedAt), 'dd LLL y')}
</div>
</div>
</div>
</div>
<Image image={content.featured[0]} className="image"/>
</div>
<main className="article" dangerouslySetInnerHTML={{__html: html}}/>
</div>
</div>
</>
);
};


export const getServerSideProps: GetServerSideProps = async (context) => {
const id = (context.params as any).id;
const {data: blog} = await axios.get(`http://localhost:4000/api/blogs/${id}`);

return {
props: {
blog
}
};
};

export default BlogPage;

You will now be able to access pages by visiting

  • http://localhost:4000
  • http://localhost:4000/my-first-blog

Next steps

Congratulations! You have learned how we have created a simple blog by using Burdy and Next.js. For a more advanced solution where we have blogs, pagination, categories, and more please visit our GitHub repository burdy-starter-next-blog and enjoy!

Or just run our command

npx create-burdy-app my-blog -t next-blog

If you have any questions or suggestions, please send us feedback on our email team@burdy.io.

Copyright © Burdy Technologies. All rights reserved.