Managing Content on Small Websites: Guide for 2024 & Beyond
22 Nov 2022
For this Astro blog tutorial, I will use Blog Starter as a design and structure guideline. Before you continue with this tutorial you will need to setup BCMS and populate it with data. I covered this in the BCMS Blog Structure tutorial.
In addition to the BCMS project, you will need your computer to be setup for Astro development. With all this done, you can run `npm create astro@latest` to initialize a new Astro project.
Now you can open the project in your favorite code editor. First, there are a few BCMS-related dependencies that you need to install and those are:
npm i --save @thebcms/cli @thebcms/client @thebcms/types @thebcms/utils @thebcms/components-react
As you can see you will use React utility components and because of this you will need to set up Astro to be able to use them. To do this you can run: npx astro add react
BCMS can generate TypeScript types based on the content inside of it. To the advantage of this, you will use the BCMS CLI by calling it before dev, start, build and preview
scripts, like this:
"scripts": { "dev": "bcms pull types lng ts && astro dev", "start": "bcms pull types lng ts && astro dev", "build": "bcms pull types lng ts && astro check && astro build", "preview": "bcms pull types lng ts && astro preview", "astro": "astro" },
You will also use the TailwindCSS and Prettier but this is completely optional and you can use any tools you like. If you want to follow our setup you can use:
To fetch data from the BCMS you will use @thebcms/client
library which abstracts Auth and REST API calls to the BCMS backend.
To initialize the client library you need an API Key. You will create 2 keys, public and private, by going to the BCMS project and navigating to Administration -> Settings -> API keys.
Here is the time to create 2 keys:
The first will be called "Public" to which you won't give any additional permission (see Figure 1)
The second will be called "Private" which will have permission to get all Entries for the Astro Blog Template (see Figure 2).
Important: You have created 2 keys because 1 will be used on the client while the other will be used on the server. As the name suggests, you do not mind exposing the public key to uses of a website.
At the root of the project, you will create .env
file and place API key information inside of it:
PUBLIC_BCMS_ORG_ID=620528baca65b6578d29868d PUBLIC_BCMS_INSTANCE_ID=67445023c8abc5e921e44650 PUBLIC_BCMS_API_KEY_ID=67446368c8abc5e921e4465e PUBLIC_BCMS_API_KEY_SECRET=91aba2f6d045b5f0a4de368a66a785dd63d2063b4270e553be92736ba2c3622f BCMS_API_KEY_ID=67446383c8abc5e921e4465f BCMS_API_KEY_SECRET=a8ea9e8518b97268596b59f30fe949ad159d9355774eba653851cca08da7710d
After this, you can create bcms.config.cjs
in the root of the project and add configuration for the BCMS CLI:
// bcms.config.cjs /** * @type {import('@thebcms/cli/config').BCMSConfig} */ module.exports = { client: { orgId: process.env.PUBLIC_BCMS_ORG_ID, instanceId: process.env.PUBLIC_BCMS_INSTANCE_ID, apiKey: { id: process.env.PUBLIC_BCMS_API_KEY_ID, secret: process.env.PUBLIC_BCMS_API_KEY_SECRET, } } }
Now you can go back to the code editor and inside of the `src` you will create 2 files: bcms-public.ts
and `bcms-private.ts
// src/bcms-public.ts import { Client } from "@thebcms/client"; export const bcmsPublic = new Client( import.meta.env.PUBLIC_BCMS_ORG_ID!, import.meta.env.PUBLIC_BCMS_INSTANCE_ID!, { id: import.meta.env.PUBLIC_BCMS_API_KEY_ID!, secret: import.meta.env.PUBLIC_BCMS_API_KEY_SECRET!, }, { injectSvg: true, }, );
// src/bcms-private.ts import { Client } from "@thebcms/client"; export const bcmsPrivate = new Client( import.meta.env.PUBLIC_BCMS_ORG_ID!, import.meta.env.PUBLIC_BCMS_INSTANCE_ID!, { id: import.meta.env.BCMS_API_KEY_ID!, secret: import.meta.env.BCMS_API_KEY_SECRET!, }, { injectSvg: true, }, );
With setup done and the BCMS client initialized you can move to create a page on which we will list all blog posts.
First, you will create a simple layout like shown in the snippet below:
<!--src/layouts/Layout.astro--> <!doctype html> <html lang="en" class="overflow-hidden"> <head> <meta charset="UTF-8" /> <meta name="description" content="Jumpstart your Astro project with this Blog. Easily manage your content and scale your application without the backend hassle. Get started now!" /> <meta name="viewport" content="width=device-width" /> <title>Astro Blog Tutorial</title> </head> <body class="font-inter overflow-x-hidden bg-appBody w-screen h-screen overflow-auto"> <div class="overflow-hidden flex flex-col min-h-screen"> <header class="sticky"> <ul class="flex gap-2 items-center"> <li><a href="/">Home</a></li> <li><a href="/blog">Blogs</a></li> </ul> </header> <main class="flex flex-col flex-1 mt-10"> <slot /> </main> </div> </body> </html>
As you can see you aren't doing anything fancy because your focus is on listing blogs and displaying a single blog content. Because of this, on the home page, you will just add a link to go to the list of blog posts:
--- // src/pages/index.astro import Layout from "../layouts/Layout.astro"; --- <Layout> <h1 class="text-2xl font-bold">My Astro Blog</h1> <div> <a href="/blog">Go to blogs page</a> </div> </Layout>
Now you can move to the page src/pages/blog/index.astro
where to list all blogs and you can do it like this:
--- // src/pages/blog/index.astro import Layout from "../../layouts/Layout.astro"; import { bcmsPrivate } from "../../bcms-private"; import { BlogEntry, BlogEntryMetaItem } from "../../../bcms/types/ts"; import { BCMSImage } from "@thebcms/components-react"; import { PropMediaDataParsed } from "@thebcms/types"; import { bcmsPublic } from "../../bcms-public"; interface BlogCardData { title: string; slug: string; image: PropMediaDataParsed; } const blogEntries = (await bcmsPrivate.entry.getAll("blog")) as BlogEntry[]; const blogCards: BlogCardData[] = blogEntries.map((blogEntry) => { const blogMeta = blogEntry.meta.en as BlogEntryMetaItem; return { slug: blogMeta.slug, title: blogMeta.title, image: blogMeta.cover_image, }; }); --- <Layout> <h1 class="text-2xl font-bold mb-5">All Blogs</h1> <div> { blogCards.map((card) => { return ( <a class="flex items-center gap-4 hover:bg-gray-100 transition-colors duration-300" href={`/blog/${card.slug}`} key={card.slug} > <div class="size-32"> <BCMSImage client:load className="size-full object-cover" media={card.image} clientConfig={bcmsPublic.getConfig()} /> </div> <div> <h3 class="text-xl font-bold">{card.title}</h3> </div> </a> ); }) } </div> </Layout>
As you can see, first you are fetching all blog entries for the BCMS and then mapping them to a simpler model: BlogCardData
. Here you are hardcoding .en
locale since you are not using localization. With data fetched, you have everything to render the page.
You can also notice, that you are using 1 utility component BCMSImage
that handles auth for file fetching and will also render the most optimal image for the provided container.
With this done, you can move to the single blog page. You will follow similar steps:
fetch data for specified blogs
display data on the page
--- // src/pages/blog/[slug].astro import Layout from "../../layouts/Layout.astro"; import { bcmsPrivate } from "../../bcms-private"; import { BlogEntry, BlogEntryMetaItem } from "../../../bcms/types/ts"; import { tryCatch } from "@thebcms/utils/try-catch"; import { EntryContentParsedItem } from "@thebcms/types"; import { BCMSImage, BCMSContentManager } from "@thebcms/components-react"; import { bcmsPublic } from "../../bcms-public"; export async function getStaticPaths() { const blogEntries = (await bcmsPrivate.entry.getAll("blog")) as BlogEntry[]; return blogEntries.map((e) => ({ params: { slug: e.meta.en?.slug || "", }, })); } const { slug } = Astro.params; const blogEntry = (await bcmsPrivate.entry.getBySlug( slug, "blog", )) as BlogEntry; const meta = blogEntry.meta.en as BlogEntryMetaItem; const content = blogEntry.content.en as EntryContentParsedItem[]; const date = new Date(meta.date.timestamp); const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; --- <Layout> <div class="flex flex-col items-center max-w-[1400px] w-full mx-auto"> <div class="text-gray-500 text-xl"> {date.getDate()} {months[date.getMonth()]}, {date.getFullYear()} </div> <h1 class="text-6xl mt-10">{meta.title}</h1> <div class="size-full mt-10 rounded-2xl overflow-hidden"> <BCMSImage client:load className="flex-shrink-0 flex-1 size-full object-cover" media={meta.cover_image} clientConfig={bcmsPublic.getConfig()} /> </div> <div class="mt-10"> <BCMSContentManager client:load items={content} /> </div> </div> </Layout> <style is:global> .bcms-content { h2 { @apply text-2xl mb-4 mt-6 font-bold; } } </style>
Here you are using 2 utility components: BCMSImage
and BCMSContentManager
. As mentioned above BCMSImage
is used to render the most optimal image for the provided container automatically while BCMSContentManager
is used to render content responses from the BCMS.
If you go to a single blog page you will be able to see that there is a problem, you are not able to see Text and image
and Image
Widgets. This is because you didn't tell BCMSContantManager
how to handle them.
If you inspect the DOM we will be able to see that there are 2 nodes with the message "Widget ... is not handled". You can see this in Figure 3.
Since you need to provide custom components for Widget rendering, you will need to handle content yourself, but it is pretty straightforward.
First, create a component for Text and image
Widget.
--- // src/components/widgets/TextAndImage.astro import { TextAndImageWidget } from "../../../bcms/types/ts"; import { BCMSImage, BCMSContentManager } from "@thebcms/components-react"; import { bcmsPublic } from "../../bcms-public"; const { data, }: { data: TextAndImageWidget; } = Astro.props; --- <div class={`flex gap-4 ${data.image_position === "RIGHT" ? "flex-row-reverse" : ""}`} > <div class="size-64 rounded-xl overflow-hidden flex-shrink-0"> <BCMSImage client:load media={data.image} className="size-full object-cover" clientConfig={bcmsPublic.getConfig()} /> </div> <BCMSContentManager items={data.text.nodes} /> </div>
Here you are checking if the image should be on the left or right and after that, you are using BCMSContentManager
utility component to render rich text property.
You have one more Widget called Image
and you will create it:
--- // src/components/widgets/Image.astro import { ImageWidget } from "../../../bcms/types/ts"; import { BCMSImage } from "@thebcms/components-react"; import { bcmsPublic } from "../../bcms-public"; const { data, }: { data: ImageWidget; } = Astro.props; --- <div class="w-full max-h-[450px] h-full rounded-xl overflow-hidden flex-shrink-0 my-5"> <BCMSImage client:load media={data.src} className="size-full object-cover" clientConfig={bcmsPublic.getConfig()} /> </div>
In this Widget, you are only displaying provided images using BCMSImage
utility component.
With Widget components created, you can use them to render content on the blog page.
--- // src/pages/blog/[slug].astro import Layout from "../../layouts/Layout.astro"; import { bcmsPrivate } from "../../bcms-private"; import { BlogEntry, BlogEntryMetaItem } from "../../../bcms/types/ts"; import { tryCatch } from "@thebcms/utils/try-catch"; import { EntryContentParsedItem } from "@thebcms/types"; import { BCMSImage, BCMSContentManager, BCMSWidgetComponents, } from "@thebcms/components-react"; import { bcmsPublic } from "../../bcms-public"; import TextAndImage from "../../components/widgets/TextAndImage.astro"; import Image from "../../components/widgets/Image.astro"; export async function getStaticPaths() { const blogEntries = (await bcmsPrivate.entry.getAll("blog")) as BlogEntry[]; return blogEntries.map((e) => ({ params: { slug: e.meta.en?.slug || "", }, })); } const { slug } = Astro.params; const blogEntry = (await bcmsPrivate.entry.getBySlug( slug, "blog", )) as BlogEntry; const meta = blogEntry.meta.en as BlogEntryMetaItem; const content = blogEntry.content.en as EntryContentParsedItem[]; const date = new Date(meta.date.timestamp); const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; --- <Layout> <div class="flex flex-col items-center max-w-[1400px] w-full mx-auto"> <div class="text-gray-500 text-xl"> {date.getDate()} {months[date.getMonth()]}, {date.getFullYear()} </div> <h1 class="text-6xl mt-10">{meta.title}</h1> <div class="size-full mt-10 rounded-2xl overflow-hidden"> <BCMSImage client:load className="flex-shrink-0 flex-1 size-full object-cover" media={meta.cover_image} clientConfig={bcmsPublic.getConfig()} /> </div> <div class="mt-10 blogContent max-w-[784px]"> <!--Handle BCMS entry content with Widgets--> { content.map((item) => { if (!item.widgetName) { return <Fragment set:html={item.value} />; } if (item.widgetName === "text-and-image") { return <TextAndImage data={item.value} />; } else if (item.widgetName === "image") { return <Image data={item.value} />; } return ( <div class="hidden">Widget "{item.widgetName}" is not handled</div> ); }) } </div> </div> </Layout> <style is:global> .blogContent { h2 { @apply text-2xl mb-4 mt-6 font-bold first:mt-0; } } </style>
As you can see you are looping through content items and mounting them depending on the Widget name. If widgetName
does not exist, that means that the content item is a primitive node, which are: paragraph, heading, ordered list, and so on.
Learn more about BCMS widgets: Reusable structured content guide
I hope this tutorial helped you to understand how to use Astro with a headless BCMS. Of course, you can use headless CMS for even more complicated projects in Astro development. If you found this blog-building straightforward, maybe you are ready to use BCMS as an Astro CMS for more complex tasks.
Get all the latest BCMS updates, news and events.
By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.
There are many actionable insights in this blog post. Learn more: