How I set up my Blogsite quickly with AstroJS & Storyblok

calendar-emoji

15 May 2024

JavascriptAstro

Been a while since I wrote my last blog, well I was mostly procrastinating thinking what to work on next and finally decided to build my own portfolio website. The only problem you ask... is that we are in 2024 and there are a hell lot of tech stacks available to achieve this, my first thought was to use my new Swiss Army Knife (Next JS) and quickly build a portfolio using a template but then, I read about Astro JS and it completely won me over.

Don't get me wrong though I still think Next JS is an insanely good framework to work with it's just sometimes you are better off wielding a fucking lightsaber (Astro JS) than a Swiss Knife. And in this case using Astro JS is more preferable because Astro JS is specifically made for quickly prototyping static sites.

Why Astro JS?

Before getting into development let us try to understand what is Astro and when exactly should you choose Astro.

From the official docs:

Astro is the web framework for building content-driven websites like blogs, marketing, and e-commerce. Astro is best-known for pioneering a new frontend architecture to reduce JavaScript overhead and complexity compared to other frameworks. If you need a website that loads fast and has great SEO, then Astro is for you.

The tradeoff between Performance & Complexity

In general most modern web frameworks excel at building applications with complex functionalities such as admin dashboards, role management, HR portals etc. However, this complexity comes at a great cost of performance and requires a whole lot of optimization to deliver content.

But, not every site needs to be complex in order to fulfill your needs, sites which serve to deliver static content such as Blogs, Portfolios, Attractive Landing pages require little to no interactivity with the user to showcase content and this is where Astro JS shines.

Astro JS delivers fast websites by default as mentioned on their docs:

It should be nearly impossible to build a slow website with Astro.

This is purely because Astro ships websites with ZERO Javascript by default to the browser! You heard me right ZERO Javascript meaning everything is pre-rendered on the server-side so it just aims to ship plain HTML to improve performance. As stated in the docs:

JavaScript is often the culprit, since many phones and lower-powered devices rarely match the speed of a developer’s laptop.

But if everything is HTML what about my Interactive components as they rely on Javascript?

This is where Astro's magic really kicks in because even though Astro components are rendered on the sever by default, you can really opt in and decide which components are to be rendered on the client side. This helps to ship minimal JavaScript to the browser while maintaining performance and interactivity on your website. The ability to enable different components to use static and dynamic rendering on demand is Astro's biggest strength and this architecture is known as Islands.

Astro Islands

The general idea of an “Islands” architecture is deceptively simple: render HTML pages on the server, and inject placeholders or slots around highly dynamic regions […] that can then be “hydrated” on the client into small self-contained widgets, reusing their server-rendered initial HTML.



Jason Miller, Creator of Preact

Simply put, think of your website as an archipelago (a group of islands) where the islands are your set of different interactive Components floating around in a sea of lightweight, pre-rendered static HTML. This design pattern allows a developer to use different UI frameworks with Astro, which brings me to my next point...

Astro does NOT compete with other Frontend-Frameworks instead it works in tandem with them

The Island architecture allows Astro to support multiple UI frameworks like React, Preact, Svelte, Vue, and SolidJS. Even though developers mostly stick to a particular framework this flexibility allows developers to use all of these at once in the same project!

---
// Example: Mixing multiple framework components on the same page.
import MyReactComponent from '../components/MyReactComponent.jsx';
import MySvelteComponent from '../components/MySvelteComponent.svelte';
import MyVueComponent from '../components/MyVueComponent.vue';
---
<div>
  <MySvelteComponent />
  <MyReactComponent />
  <MyVueComponent />
</div>

Setting up our Website

Portfolio/Landing Page

Now that we know what Astro Js is:

Right off the bat, we'll head on to Astro Themes and pick a template to kickstart our project so that we don't have to build from scratch. Create a new repo for this project and clone it in your local machine then run npm install to install all the required dependencies from package.json. Run npm run dev to start and you're good to go!

You can simply edit content on the Portfolio/Landing Page to quickly display your personal information and that is all, or you can ditch all or some of the components and add your own custom components to style your website.

Setting up our Blog Page

Now this is the more interesting part, before we start designing our Blog Page we first need to choose and set up a CMS. What is a CMS you might ask... don't worry I gotchu 🙃

CMS (Content Management System)

Let's take a small example to simplify why we need a CMS in the first place, imagine the company you're working at (with the ongoing recession, we can only imagine 😅) wants to start a blog page, so your boss assembles a team with 3-4 people including you as the sole tech guy. Unfortunately, the rest of your team does not know how to code and have no idea how to write a blog along with HTML for a website, but they are there as creative writers and UI designers.

Now you’re in a state of deadlock where your writers can’t write content on a webpage since they don’t know how to code and even if they did it is hard to write content in plain HTML.

Example Scenario

This is where a CMS comes in, A content management system (CMS) is an application that is used to manage content, allowing multiple contributors to create, edit and publish. It is divided into two main components:

  • CMA (Content Management Application): This is basically is a media storage cum editor where the author can write content in a user-friendly interface, think of it as Microsoft Word on Steroids.
  • CDA (Content Delivery Application): This on the other hand deals with all the behind the scenes stuff that processes all the static content and displays as a webpage.

Segregating work using a CMS

Choosing the Right CMS

As for picking a CMS, there are a plethora of options available in the market which you can choose from, but for the sake of this blog I'll be talking about Storyblok which is an excellent Headless CMS option I used for my website. Storyblok is a component-based headless CMS that allows you to manage your content using reusable components called Bloks. Though Astro provides guides for a lot CMS options, it announced Storyblok as its official CMS integration and trust me when I say this, it really is worth it.

Integrating Astro with Storyblok

  • First up, we need to sign up for a Storyblok account and set up our own space, I just went with the free plan and it works well. Copy the Preview token from the settings and paste it into your .env file for further use.

Now we need to install the official Storyblok Integration package using npm

npm install @storyblok/astro vite

Connecting Astro to your Storyblok Space

To connect our Astro project to our Storyblok space we just need to modify our astro.config.mjs file as shown below and add the preview token from our .env file.

import { defineConfig } from 'astro/config';
import storyblok from '@storyblok/astro';
import { loadEnv } from 'vite';

const env = loadEnv("", process.cwd(), 'STORYBLOK');

export default defineConfig({
  integrations: [
    storyblok({
      accessToken: env.STORYBLOK_TOKEN,
      components: {
        // Add your components here
      },
      apiOptions: {
        // Choose your Storyblok space region
        region: 'us', // optional,  or 'eu' (default)
      },
    })
  ],
});

Making Bloks in Storyblok

Bloks are literally the "building blocks" of our webpage when we incorporate Storyblok and are stored in the Block Library in your Space. Think of your content on your webpage split into different chunks (Blocks) which you can move around, modify at will and even inject in other blocks.

Right now, we just need three blocks to write content namely BlogPost, BlogPostList and the other one being Page. For every block that we create we need to create its equivalent Astro component, so we create a directory named storyblok inside pages to store all these components.

  • BlogPost: This is a Content Type block which basically acts as a layout for our blogs where we can define some fixed fields and even add other nestable blocks such as Banner Images, Tables etc. The fixed fields being title, description, image and content.
    src/pages/storyblok/BlogPost.astro
    
    ---
    import { storyblokEditable, renderRichText } from '@storyblok/astro'
    const { blok } = Astro.props
    const content = renderRichText(blok.content)
    ---
    
    <article {...storyblokEditable(blok)}>
      <h1>{blok.title}</h1>
      <p>{blok.description}</p>
      <img
          class="w-full h-[360px] lg:h- [450px] object-cover"
          src={`${blok.image.filename}/m/1600x0`}
       />
      <Fragment set:html={content} /> 
    </article>
    Note: Since our content field is of type Richtext, we need to convert it into HTML first therefore we use <Fragment set:html={content} />
  • BlogPostList: This will be a nestable block which will contain all the blocks of the type BlogPost and will be displayed as cards. It uses the useStoryblokApi hook to fetch all the stories with the content type of blogPost and then filter as draft/published as required.
    src/pages/storyblok/BlogPostList.astro
    
    ---
    import { storyblokEditable } from '@storyblok/astro'
    import { useStoryblokApi } from '@storyblok/astro'
    
    const storyblokApi = useStoryblokApi();
    
    const { data } = await storyblokApi.get('cdn/stories', {
      version: import.meta.env.DEV ? "draft" : "published",
      content_type: 'blogPost',
    })
    
    const posts = data.stories.map(story => {
      return {
        title: story.content.title,
        date: new Date(story.published_at).toLocaleDateString("en-US", {dateStyle: "full"}),
        description: story.content.description,
        slug: story.full_slug,
      }
    })
    
    const { blok } = Astro.props
    ---
    
    <ul {...storyblokEditable(blok)}>
      {posts.map(post => (
        <li>
          <time>{post.date}</time>
          <a href={post.slug}>{post.title}</a>
          <p>{post.description}</p>
        </li>
      ))}
    </ul>
  • Page: This is also a nestable type block which will render all the components/blocks inside its body field. It also adds the storyblokEditable attributes to the parent element which will allow us to edit the page in Storyblok.
    src/pages/storyblok/Page.astro
    
    ---
    import { storyblokEditable } from '@storyblok/astro'
    import StoryblokComponent from "@storyblok/astro/StoryblokComponent.astro";
    const { blok } = Astro.props
    ---
    
    <main {...storyblokEditable(blok)}>
      {
        blok.body?.map((blok) => {
          return <StoryblokComponent blok={blok} />
        })
      }
    </main>

Now, that we have created our bloks, we just need to handle dynamic routes for each webpage that we create for our blogs. Creating dyamic routes with Astro is fairly simple, we just need to create a new directory named blog inside our pages directory and inside it create a new file called [...slug].astro with the code below:

src/pages/blog/[...slug].astro

---
import { useStoryblokApi } from '@storyblok/astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'

export async function getStaticPaths() {
  const sbApi = useStoryblokApi();

  const { data } = await sbApi.get("cdn/stories", {
    content_type: "blogPost",
    version: import.meta.env.DEV ? "draft" : "published",
  });

  const stories = Object.values(data.stories);

  return stories.map((story) => {
    return {
      params: { slug: story.slug },
    };
  });
}

const sbApi = useStoryblokApi();
const { slug } = Astro.params;
const { data } = await sbApi.get(`cdn/stories/blog/${slug}`, {
  version: import.meta.env.DEV ? "draft" : "published",
});

const story = data.story;

---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Storyblok & Astro</title>
  </head>
  <body>
    <StoryblokComponent blok={story.content} tags={story.tag_list}/>    {/* Pass on Story taglist along with its content for each Story */}
  </body>
</html>

This way every request starting with blog/ will be handled by this file, as it maps the request URL to the slug generated by each Story that we have in our Library. We can test this out by making a Test Blog in our Storyblok Content Tab, head over to New Story and select Content type as BlogPost. You’ll be greeted with a split screen where on the left side you’ll see what is called the Visual Editor and on the right side a normal editor to write and manage content/media.

Getting the hang of Storyblok’s Visual Editor 

I feel Visual Editor from Storyblok is their game changing feature, which makes writing content and visualizing it on the fly an enjoyable experience for any writer/developer. 

But to set it up you need to change the preview URL from the default one to your localhost with port number where your dev server is running, in my case it is https://localhost:4321/ 

Note: By default the dev server runs on HTTP, however Storyblok requires apps to be served via HTTPS so to bypass this install basicSsl and run your app in HTTPS. For more details refer this link: https://www.storyblok.com/faq/setting-up-https-on-localhost-in-astro

And just like that we’re done, now we have our own Content Editor platform that we can use to write content, visualize changes in real-time, manage media using Storyblok’s Asset Library and customize every component using custom Bloks.

Source Code:

https://github.com/NikhilC2209/portfolio

References: