Thursday, April 25, 2024
HomeJavaScriptConstruct a Weblog with Subsequent.js and Webiny Headless CMS

Construct a Weblog with Subsequent.js and Webiny Headless CMS


Introduction

On this tutorial, we’ll construct a weblog website with Subsequent.js and Webiny Headless CMS. We’ll have a look at tips on how to arrange Webiny and Subsequent.js venture. We’ll create content material fashions & knowledge in Webiny CMS and learn to eat this knowledge within the Subsequent.js utility. Earlier than you proceed, it could be nice to have the next prerequisite data.

  • Have a fundamental understanding of object destructuring operations in JavaScript
  • Know tips on how to fetch knowledge from an API
  • Have a fundamental information of React
  • Have an understanding of how knowledge is shared between elements as props

Now that you’ve got an thought of the conditions of what you’d must learn this text. Let’s begin by establishing Webiny CMS.

Setting Up a Webiny Undertaking

Stipulations

To comply with together with this tutorial, you should have the next:

Let’s create a Webiny venture, and you are able to do that by typing the command beneath in your terminal.

npx create-webiny-project webiny-cms

Whenever you run the command, you’ll be prompted to reply some questions within the terminal, one in every of which is said to the kind of storage choices you need to use.

The 2 choices obtainable are Dynamo DB and DynamoDB + Elasticsearch. We’ll be choosing the primary choice because the use case of the venture we’re constructing doesn’t require a big database like Dynamo DB + Elasticsearch

The command will set up the venture domestically. However, to have the ability to use the service, you should deploy the venture with the command beneath into your AWS account.

yarn webiny deploy

As soon as the venture is deployed, you’ll get details about your venture within the terminal. The knowledge consists of the hyperlink to the admin app, the preview web site, and so on.

In case you didn’t discover the data on the primary trial, you may at all times get them by utilizing the command beneath.

yarn webiny data

You’ll get a consequence like this within the terminal.

Surroundings: dev
-------------------------
➜ Most important GraphQL API: https://your-unique-id.cloudfront.web/graphql
➜ Headless CMS GraphQL API:
- Handle API: https://your-unique-id.cloudfront.web/cms/handle/{LOCALE_CODE}
- Learn API: https://your-unique-id.cloudfront.web/cms/learn/{LOCALE_CODE}
- Preview API: https://your-unique-id.cloudfront.web/cms/preview/{LOCALE_CODE}
➜ Admin app: https://your-unique-id.cloudfront.web
➜ Public web site:
- Web site URL: https://your-unique-id.cloudfront.web
- Web site preview URL: https://your-unique-id.cloudfront.web
-------------------------

When the deployment is completed, you may click on on the admin Admin app will redirect you to the admin space the place you’d be required to create an admin person. The admin web page appears just like the picture beneath.




Creating an admin user

Within the admin space, you’ll even be requested to put in some extra providers or features like 18N, which is brief for Internationalization, File Supervisor, Web page Builder, Type Builder, and the Headless CMS.

When all of the set up is completed, you’ll be redirected to the dashboard, it appears just like the picture beneath.




Webiny's dashboard

You’ll be able to click on on the NEW CONTENT MODEL button within the Headless CMS card, and it’ll take you to the world the place you may create your content material fashions. You’ll be able to learn extra concerning the fields that you’ll add as you create your content material fashions right here.

The content material mannequin of the authors appears just like the picture beneath. You’ll be able to select to make it precisely like this or customise it in a manner that fits your want.




Creating the author model

That is what the posts content material mannequin appears like beneath.




Creating the posts content model

Whenever you’re finished with the processes above, we’ll want to attach our Subsequent.js utility to the Headless CMS, so we will at all times carry out requests to the GraphQL API. To start out with this step, you’ll head to the ACCESS MANAGEMENT tab within the dashboard.




Access Management section

Click on on API Keys, and also you’ll be redirected to a brand new UI just like the one beneath.




Generating a new API key

You’ll be able to fill the shape fields with all the mandatory data. Within the Headless CMS part, you must choose Customized Entry choice within the Entry Degree dropdown. Since you chose the “Customized Entry” choice, you’ll be capable to customise the varieties of GraphQL operations you need to carry out.




Headless API config

Within the Entry Scope dropdown of the CONTENT MODEL GROUPS, choose All teams. You’ll repeat the identical factor for the CONTENT MODELS, then you definately’ll choose All fashions within the Entry Scope dropdown. Within the CONTENT ENTRIES, choose All entries within the dropdown.

The picture beneath reveals all the suitable settings for the File Supervisor




File Manager API config

Whenever you’re finished, you may click on on the SAVE API KEY button and replica it to a protected location. We’ll use this later to authenticate with the GraphQL API.

Putting in Subsequent.js

With that step out of the best way, allow us to start constructing the weblog by establishing a Subsequent.js venture. Shortly earlier than we begin, I am assuming that you have gone via the method of spinning a operating dev surroundings of the Webiny platform, which supplies you entry to the admin panel the place you may create your content material fashions and check them within the GraphQL playground.

To put in Subsequent.js, we’ll use the command beneath to get us up and operating. The command does the heavy lifting of establishing a venture folder — webiny-blog — for us with the mandatory recordsdata.

npx create-next-app webiny-blog

Overview of the Recordsdata within the Undertaking

Now that we’ve got a venture construction already. Let me stroll you thru the folders and the recordsdata that we’ll be working with on this venture. Check out the listing construction beneath.

.
├── pages/
│ ├── creator/
│ │ └── .js
│ ├── _app.js
│ ├── [slug].js
│ └── index.js
├── src/
│ └── elements/
│ ├── BlogCard/
│ │ ├── type/
│ │ └── index.js
│ ├── Header/
│ │ └── index.js
│ └── utils/
│ └── helpers.js
└── types

Because the aim of this venture is to construct a weblog, the index route — index.js — will render all of the weblog posts or articles which have ever been written on the weblog.

The [slug].js file is a dynamic route, as its position or no matter data that’s rendered on the web page modifications primarily based on the info that’s handed to it. One thing of the shape beneath

https://localhost:3000/hello-world

Any article that has a slug with the string “hello-world” is rendered on the web page. We’ll get into this in full as we progress.

_app.js is the foundation folder in a Subsequent.js venture. Its perform is a little bit bit much like Create-React-App. The distinction right here is that as an alternative of appending the React app to a DOM node known as "root", Subsequent.js receives all the opposite pages, elements, types, and so on., as props. Check out the syntax beneath.

// pages/_app.js
import "../types/globals.scss";

perform MyApp({ Part, pageProps }) {
return <Part {...pageProps} />;
}

export default MyApp;

The creator route is sort of completely different from all the opposite recordsdata within the pages route, as it’s each a static and dynamic route. When you navigate to “https://localhost:3000/creator” you may get a 404 error as a result of there is no static index route however a dynamic route current within the listing.

So, navigating to that route with out appending the creator’s slug to the URL would lead to that error. As a substitute of doing that, you may simply navigate to the creator’s route by doing one thing related beneath.

This method of getting a string, “creator”, within the URL parameter of the weblog web page improves the UX of your utility in conditions the place folks would not have to marvel the place a sure hyperlink is taking them as a result of the URL is a little bit bit self-descriptive

https://webiny-blog.netlify.app/creator/codebeast

src/ comprises the reusable — BlogCard and Header — elements that we’ll use all through the codebase.

utils/ comprises all of the asynchronous GraphQL question features and the fetcher perform that sends the queries as a POST request to the API endpoint.

types/ is the place the worldwide styling and CSS variables reside.

Now that you’ve got seen the fundamental features of the recordsdata within the construction let’s begin by having a look on the fetcher and GraphQL question features.

Overview of the GraphQL Queries and Fetcher

Within the final part, we walked via the structure of this venture. Now, we’ll begin by understanding how the fetcher() and every question perform work.

We noticed within the earlier part that the fetcher perform makes a POST request to the API endpoint, which in flip, returns the info from our content material fashions which we have created within the Webiny Admin panel.

// src/utils/helpers.js
async perform fetcher(question, { variables } = {}) {
const res = await fetch(course of.env.NEXT_PUBLIC_WEBINY_API_URL, {
methodology: "POST",
headers: {
"Content material-Kind": "utility/json",
Authorization: `Bearer ${course of.env.WEBINY_API_SECRET}`,
},
physique: JSON.stringify({
question,
variables,
}),
});

const json = await res.json();

if (json.errors) {
console.error(json.errors);
throw new Error("Didn't fetch API");
}

return json.knowledge;
}

The snippet above reveals the fetcher perform. Let’s take a second to interrupt down what’s going on in that perform block. Initially, the perform itself takes in two arguments, question and variables.

The variables argument receives a JavaScript object which is used once we need to get dynamic knowledge, for instance, a particular article or the slug of an creator.

The variable is a little bit completely different for acquiring the profile of a specific creator or a particular weblog put up. The syntax stays the identical although, check out an instance beneath.

// src/utils/helpers.js
{
variables: {
PostsGetWhereInput: {
slug: slug;
}
}
}

The fetcher perform makes use of the native fetch API of JavaScript to get the info as an alternative of Axios or another third-party knowledge fetching library as a result of Subsequent.js already polyfills the fetch API by default, which in flip permits us to have the ability to use the fetch API on the consumer aspect and for server-side operations too.

Taking a better have a look at the response variable beneath, you may discover that we’re utilizing surroundings variables to retailer our API key and the URL to the API endpoint.

// env.native
course of.env.NEXT_PUBLIC_WEBINY_API_URL

course of.env.WEBINY_API_SECRET

Moreover, we’re sending the queries and the variables {that a} question comprises (if there are any), as payload to the physique methodology within the fetcher.

If there’s any error throughout the means of getting this knowledge, we’ll catch it within the if() block and log it to the console within the terminal. Since we’ll be utilizing the data-fetching strategies of Subsequent.js, all console.logs will probably be displayed within the terminal.

Now, let’s check out the queries. The snippet beneath reveals all of the queries, the primary one will get all of the slugs within the weblog put up from the content material mannequin that we created within the admin panel.

// src/utils/helpers.js

// fetch all put up/article slugs that'll be used to
// generate dynamic routes of every article when it's clicked upon
export async perform getAllPostSlugs() {
const slugs = await fetcher(`
question slugs {
listPosts {
knowledge {
slug
}
}
}
`);

return slugs.listPosts.knowledge;
}

export async perform getAuthorSlugs() {
const slugs = await fetcher(`
question slugs {
listAuthors {
knowledge {
slug
}
}
}
`);

return slugs.listAuthors.knowledge;
}

// get all articles
export async perform getArticles() {
const articles = await fetcher(`
{
listPosts(kind: createdOn_DESC) {
knowledge {
title
slug
excerpt
featuredImage
createdOn
creator {
identify
}
}
}
}
`);

return articles.listPosts.knowledge;
}

// the perform beneath is a helper that will get all of the
// authors on our weblog
export async perform getAuthorByName(slug) {
const authors = await fetcher(
`
question authorbyName($AuthorsGetWhereInput: AuthorsGetWhereInput!) {
listAuthors: getAuthors(the place: $AuthorsGetWhereInput) {
knowledge {
identify
slug
authorsBio
image
articles {
title,
excerpt
}
}
}
}
`,
{
variables: {
AuthorsGetWhereInput: {
slug: slug,
},
},
}
);

return authors.listAuthors.knowledge;
}

// helper perform that will get an article by its distinctive slug param
export async perform getArticleBySlug(slug) {
const knowledge = await fetcher(
`question PostBySlug($PostsGetWhereInput: PostsGetWhereInput!) {
listPosts: getPosts(the place: $PostsGetWhereInput) {
knowledge {
title
excerpt
featuredImage
createdOn,
creator {
identify
slug
}
physique
}
}
}`,
{
variables: {
PostsGetWhereInput: {
slug: slug,
},
},
}
);

return knowledge.listPosts.knowledge;
}

You may discover that the second perform can be much like the primary one, because it will get all of the slugs of authors on the weblog. The aim of doing that is to have the ability to map these slugs and move them as URL parameters within the getStaticPaths() data-fetching methodology of Subsequent.js.

Coming right down to the remaining queries, you may see that there is a distinction between the earlier queries. The primary distinction could be seen within the getArticles() perform, you may see that the record of articles once they’re rendered on the web page will probably be in descending order, relying on the date they have been revealed.

// src/utils/helpers.js
listPosts(kind: createdOn_DESC) {
knowledge {
// different knowledge
}
}

The perform — getArticleBySlug() — can be distinctive within the sense that it receives an argument, slug, that’s handed as a variable to the fetcher perform. You may additionally discover that the PostBySlug() question receives an argument, PostsGetWhereInputs , as you may see beneath. it goes additional to test if this variable is current within the question.

// src/utils/helpers.js
export async perform getArticleBySlug(slug) {
const knowledge = await fetcher(`
question PostBySlug($PostsGetWhereInput: PostsGetWhereInput!) {
listPosts: getPosts(the place: $PostsGetWhereInput) {
knowledge {
// different knowledge
}
}
},
{
variables: {
PostsGetWhereInput: {
slug: slug
}
}
}
`);
}

This identical course of is repeated for the getAuthorByName() perform. However this time round, the syntax of the variables object will probably be just like the one beneath

//src/utils/helpers.js
{
variables: {
AuthorsGetWhereInput: {
slug: slug;
}
}
}

Now that we have got an thought of the features of our queries, let’s transfer on to create a UI part that may maintain the info we’re getting from the API.

Constructing the BlogCard Part

The final part walked us via the method of writing and understanding the features of our queries and the fetcher perform itself.

With that out of the best way, let’s check out the construction of the weblog card part and see how we might map the content material from the API endpoint onto the weblog web page.

// src/elements/BlogCard

import Hyperlink from "subsequent/hyperlink";
import Picture from "subsequent/picture";
import propTypes from "prop-types";
import { Card } from "./type/blogcard.styled";
import dayjs from "dayjs";

const BlogCard = ({
knowledge: {
slug,
featuredImage,
title,
excerpt,
creator: { identify },
createdOn,
},
}) => {
return (
<Hyperlink href={`/${slug}`} passHref>
<Card>
<div className="featured-image">
<Picture
src={featuredImage}
top={230}
width={320}
alt={title}
placeholder="blur"
blurDataURL
/>
</div>
<div className="card-info">
<p className="title">{title}</p>
<p className="description">{excerpt}</p>
<div className="author-date">
<p className="date">
<time dateTime={createdOn}>
{dayjs(createdOn).format("MMMM, D, YYYY")}
</time>
</p>
<p className="creator">{identify}</p>
</div>
</div>
</Card>
</Hyperlink>
);
};

export default BlogCard;

The snippet above represents the structure of the weblog card, and the very first thing you could discover is the best way we’re passing the info we’ll get from the API as props to the part itself.

The destructuring project of JavaScript helped us to beat the ache of performing some like knowledge.title, knowledge.excerpt repeatedly.

As a substitute of that, we destructured the consequence. Initially, some builders would’ve handed solely the knowledge prop within the part, however we used the method beneath as an alternative.

// src/elements/BlogCard
knowledge: {
slug,
featuredImage,
title,
excerpt,
creator: { identify },
createdOn,
}

The createdOn key must be transformed into human-readable textual content, that is why we’re utilizing the dayjs library, and we’re formatting the worth we get from the API endpoint.

dayjs(createdOn).format("MMMM, D, YYYY");

As a result of we’ve got a file named [slug.js], the Hyperlink part of Subsequent.js will take the person to the article that’s clicked on — we’ll check out how that occurs shortly. If the filename is just not [slug].js, say for instance, it’s known as [post].js, the href attribute of the Hyperlink part will seem like one thing beneath

<Hyperlink href={`/${put up}`} passHref>
<Card>
card content material
</Card>
</Hyperlink>

The propTypes module that we imported can be utilized to validate the kind of knowledge that’s being despatched to the part. The snippet beneath carries out a test on the part’s prop. If the kind of knowledge that it receives is just not typeOfArray, React throws an error.

BlogCard.propTypes = {
knowledge: propTypes.array.isRequired,
};

Constructing the Index Web page

Now that we have seen the construction of the BlogCard part. Let’s examine how we will map the info from the API endpoint onto the index web page.

To try this, we’ll be utilizing the data-fetching strategies of Subsequent.js to get the consequence from our API endpoint and move it as props to the index web page.

// pages/index.js

import { getArticles } from "../src/utils/helper";

export async perform getStaticProps() {
let articles = await getArticles();

const posts = articles;

return {
props: {
posts,
},
};
}

The snippet above reveals how we’re utilizing the getStaticProps() data-fetching methodology to acquire knowledge from the endpoint. Keep in mind how we created a perform that holds the question which we’re utilizing to get the record of articles? Now, we’re utilizing it on this part, and we’re additionally returning the info as props within the snippet beneath.

// pages/index.js

import React from "react";
import Head from "subsequent/head";
import styled from "styled-components";
import BlogCard from "../src/elements/blogCard";
import Header from "../src/elements/Header";

export const Container = styled.div`
.playing cards {
padding: 30px 60px;
show: flex;
flex-wrap: wrap;
}
`;

perform BlogPage({ posts }) {
return (
<React.Fragment>
<Head>
<title>Subsequent.js Weblog with Webiny CMS</title>
</Head>
<Container>
<Header />
<div className="playing cards">
{posts?.map((put up, index) => {
return <BlogCard knowledge={put up} key={index} />;
})}
</div>
</Container>
</React.Fragment>
);
}

export default BlogPage;

Within the snippet above, you may see that we have obtained the posts props, and we’re utilizing them within the <Container/>, which is a flex-container, primarily based on the types that we have indicated within the styled-component block. We’re additionally utilizing the default Head part of Subsequent.js to wrap the meta-data of the index web page.

The Head part lets us add web optimization to our weblog. Though the scope of this text doesn’t revolve across the web optimization of a Subsequent.js weblog, you may study it right here.

Within the subsequent few traces beneath, we’re mapping via the consequence from the API endpoint with the posts prop and assigning that consequence to the brand new array to the knowledge props within the BlogCard part.

<div className="playing cards">
{posts?.map((put up, index) => {
return <BlogCard knowledge={put up} key={index} />;
})}
</div>

With that step out of the best way, you may check out what the index web page appears like beneath.




Blog list page

Constructing the Slug Web page

The slug web page of the weblog takes the identical method that we used throughout the means of constructing the index web page. The one distinction right here is that we’ll be needing one other data-fetching methodology — getStaticPaths() — of Subsequent.js to implement the performance of this web page.

What we’ll be doing right here is to render a novel article at any time when it’s clicked on from the homepage, or somebody from any a part of the world visits the hyperlink to a specific web page, it ought to statically generate the content material of that web page. The following step would be the utilization of the suitable data-fetching methodology, which is getStaticPaths()

// pages/[slug].js

// get the slugs from all articles, map them dynamically as routes*
export async perform getStaticPaths() {
const articles = await getAllPostSlugs();

return {
paths: articles.map((articles) => {
return {
params: {
slug: articles.slug,
},
};
}),

fallback: true,
};
}

Keep in mind how the subsequent question in our helper.js file within the utils/ listing is the one which returns all of the slugs of articles on the weblog. Within the snippet above, we’re utilizing the info obtained from that question as paths that we will map via and return as parameters for the getStaticProps().

The snippet beneath reveals how that path parameter is obtained through the context argument with a destructuring project.

// pages/[slug].js

// fetch and renders the content material of the article primarily based*
// on the present slug/route dynamically*
export async perform getStaticProps(context) {
const { params } = context;

const article = await getArticleBySlug(params.slug);

return {
props: {
put up: {
...article,
},
},
};
}

Whenever you check out the snippet carefully, you may see that we’re passing the slug — params.slug — as an argument to the getArticlesBySlug() perform. In the long run, we’re returning the posts props, and since we’d like the remaining content material of the posts object, we’ll use JavaScript’s unfold operator so as to add the opposite properties like so:

// pages/[slug].js

const article = await getArticleBySlug(params.slug);

return {
props: {
put up: {
...article,
},
},
};

With this course of out of the best way, we might simply must render the content material of the posts prop on the web page.

Webiny has a Wealthy textual content part that helps us with formatting the content material of the article from the physique object that we’re receiving as props.

// pages/[slug].js

export default perform Article({
put up: {
title,
physique,
createdOn,
creator: { identify, slug },
},
}) {
return (
<React.Fragment>
<Head>
<title>{title} | Webiny CMS Weblog </title>
</Head>
<PostContainer>
<Header />
<div className="content material">
<h2 className="article-title">{title}</h2>
<div className="author-published-date">
<Hyperlink href={`/creator/${slug}`}>
<p className="author-name">{identify}</p>
</Hyperlink>
<p>{dayjs(createdOn).format("MMMM, D, YYYY")}</p>
</div>
<RichTextRenderer knowledge={physique} />
</div>
</PostContainer>
</React.Fragment>
);
}

You could be questioning why the Hyperlink part’s href attribute is not pointing to /creator/${creator} since that’s the identify (.js) of the dynamic route within the pages folder. The reason being that we’re already receiving the prop from the content material mannequin that we created already, and it’s obtainable to us via the API endpoint.

The wealthy textual content part takes within the physique object as a prop and renders it properly for us on the web page

<RichTextRenderer knowledge={physique} />

The picture beneath reveals what the slug web page appears like.




Single page

Constructing the Creator’s Web page

As we mentioned beforehand within the first part, the creator’s route is exclusive, however it is usually much like the slug web page. Since we’ll be repeating the identical course of, it will be greatest to only add the snippets of the data-fetching strategies beneath. Check out them beneath.

// pages/creator/.js

import { getAuthorByName, getAuthorSlugs } from "../../src/utils/helper";

export async perform getStaticPaths() {
const authors = await getAuthorSlugs();

return {
paths: authors.map((creator) => {
return {
params: {
creator: creator.slug,
},
};
}),

fallback: true,
};
}

export async perform getStaticProps(context) {
const { params } = context;

const creator = await getAuthorByName(params.creator);

return {
props: {
creator: {
...creator,
},
},
};
}

You’ll be able to determine to construct any structure that feels proper or no less than anybody that you just’re comfy with making. That is what my very own creator’s web page appears like when it was constructed. Check out it beneath.




Authors page

The picture above reveals the authors’ web page with out the variety of articles they’ve revealed. You’ll be able to determine so as to add that to your content material mannequin if you wish to.

Wrapping Up.

Having a CMS that helps you handle how content material is structured is of an important benefit while you got down to construct your weblog, and Webiny’s CMS is a superb device to begin with.

If you wish to deploy the venture on a platform like Netlify, don’t forget so as to add your surroundings variables within the dashboard so the venture can work superb in a manufacturing surroundings.

I hope this text has helped you achieve some basic information of how JavaScript can be utilized to construct sensible and real-life options to issues. Thanks for studying!

Full supply code: https://github.com/webiny/write-with-webiny/tree/major/tutorials/nextjs-blog


This text was written by a contributor to the Write with Webiny program. Would you want to write down a technical article like this and receives a commission to take action? Try the Write with Webiny GitHub repo.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments