Friday 25 October 2024
Astro + storyblok (how we built this site)
Learn how we upgraded our web presence using Astro and Server Side Rendering (SSR), integrating Storyblok CMS, and employing Tailwind CSS for styling. It highlights the process of choosing the right tech stack, the nuances of deployment with Serverless Stack (SST), and the notable improvements in site performance.
A Brief History
Despite a long career in software development, when I set up as a freelancer in 2017 I was mainly building Wordpress sites with a bit of legacy C# development. I thought my main focus of business was going to be in and around the WordPress ecosystem. As such I built a basic placeholder website in WordPress and left it at that. At this point I didn't really have a niche, or was in a position to have a preferred tech-stack. As I'm sure many freelancers will agree with, I was taking on any work that I felt like I could do. Fast forward a few years and I found that I had embraced serverless and was doing more and more digital transformation work. My slow, clunky, non-SEO optimised Wordpress site was in need of a refresh to better reflect the services that I offered.
Most of my client projects are some aspect of a web-app; front-end, back-end or full-stack. Or even little utilities/scripts as part of a larger data exercise. Either way, the work I was doing was generally not public facing. Therefore SEO has not been a consideration, nor has the fact that I couldn't have the app completely rendered on the client. So here was an opportunity. I needed a new site and I wanted to familiarise myself with some new (to me) concepts around either Static Site Generation (SSG) or Server Side Rendering (SSR).
Choosing the Tech Stack
Through my earlier WordPress work (I still manage a live WordPress e-commerce site) I was aware of the pitfalls, especially around performance (and the impact this has on SEO). I have been looking at various ways to go lightweight on the frontend. So I had dabbled with a few SSG tools already and how I could link these tools to a CMS for publishing content. Anyone working in the web space will understand that this world moves pretty fast, there seems to be a new framework or latest version announced every week. I had at this point settled on the Serverless Stack (SST) for all my cloud/serverless deployments. Around this time I heard about Astro and how it linked nicely with SST. The team there had built a new Astro construct. I was really keen on the fact that I could also use my existing React knowledge to incorporate components and the performance stats that were being published were seriously impressive.
So I had now decided on Astro and SST, what was next was a CMS. Yes, I could have used Astro without and simply used markdown files for my content. This was also an excercise in trying out some different services for potential use in other projects. As of the time of writing, Astro lists 27 CMS's in the guide docs. The official CMS is Storyblok and it does come with quite a generous free-tier (and I do like a free-tier), so what better place to start.
One final thing, Astro supports both SSG and SSR (and a hybrid mode). For this site, I wouldn't expect the content to change very often so it would be a perfect case for SSG with the site being rebuilt everytime that the content changed. However, I wanted to explore SSR as this is something I have in mind for some other projects.
Building the Site Content
Before getting stuck into the code, I needed to do a bit of design and think about the content for the site. My main objectives for this site were to keep it lean but also convey all the necessary information to reflect what I, as Eastgate Digital did. I decided that I would have a main Home
page with some overviews, an About
page with more in-depth detail and then a Blog
.
I would divide my home page up into sections,
A hero section with a headline overview of what we are about
A features section that stated a bit more detail of what you can expect with working with us
A team section with a brief description and contact details for each team member (just me at the moment)
A get started section for how to contact us or book an initial meeting
A testimonials section from previous clients that I have worked with
For the About
page I would need a PageHeader
and a PageSection
component that could then be used to build content for any pages dynamically (I would later add the legal pages for privacy and cookie policies using these components).
The Blog
would consist of a BlogList
and a BlogPost
component.
For both the blog posts and page sections, I would use a rich text field. This would enable a great deal of flexibility for what I could put into the field. How this is then rendered on the site is discussed later.
Storyblok, as the clever name suggests, works on the basis of Stories and Bloks. Your content is a collection of Stories. Each Story is made up of reusable bloks with different content depending on the story. For example, my PageHeader blok would have fields like heading and sub-heading. I could use the PageHeader on many different pages (as a Story), with a different heading and sub-heading.
In the interests of brevity, I then created all of my bloks and added some content to them within Storyblok to create my main pages and sections. To link these up with my Astro project, I added these bloks as components to my Astro config as follows.
// astro.config.mjs
storyblok({
accessToken: env.PUBLIC_STORYBLOK_TOKEN,
components: {
blogPost: 'storyblok/BlogPost',
blogPostList: 'storyblok/BlogPostList',
feature: 'storyblok/Feature',
getStarted: 'storyblok/GetStarted',
grid: 'storyblok/Grid',
hero: 'storyblok/Hero',
image: 'storyblok/Image',
page: 'storyblok/Page',
pageHeader: 'storyblok/PageHeader',
pageSection: 'storyblok/PageSection',
rows: 'storyblok/Rows',
teamList: 'storyblok/TeamList',
teamMember: 'storyblok/TeamMember',
testimonial: 'storyblok/Testimonial',
testimonialsList: 'storyblok/TestimonialList'
},
Astro Project Layout
As my content is coming from the Storyblok API, I can use the dynamic routing feature of Astro. This means that I can build the site with relatively few files in the Astro project. At the heart of Astro is basically a templating engine this allows me to create a layout and components to be reused throughout the site. For the template I need a BaseLayout
, Header
and Footer
. I also created a Navigation
component to keep this separate from the Header.
This is how the body of my BaseLaout component looks (before I do any styling, obviously).
<body>
<Header />
<slot />
<Footer />
</body>
On each of my pages I then put my Storyblok content inside the BaseLayout component and it will get rendered as part of the <slot />
element.
<BaseLayout>
<StoryblokComponent blok={story.content} />
</BaseLayout>
When I say each of my pages, this is a little misleading as the fact I'm using dynamic routing and all my content is coming from Storyblok, I only really have one page. Dynamic routing in Astro works by having a file [...slug.astro]
in the pages
folder. This single file takes the slug
parameter and uses that to fetch the associated Storyblok content. This is all done within the frontmatter of the Astro file.
const storyblokApi = useStoryblokApi();
const { data } = await storyblokApi.get(`cdn/stories/${slug === undefined ? 'home' : slug}`, {
version: import.meta.env.DEV ? 'draft' : 'published',
});
const story = data.story;
Adding Some Interactivity
What I have up to this point is a basic site that would render all of my Storyblok content, although not very pretty to look at yet. I wanted to add a bit of client side interactivity. For the blog, each post had a list of tags. I wanted to display these on the blog page and allow the user to click on a tag to filter the list of blog posts based upon the selected tag. One of the great features of Astro in my opinion is the ability to add in components made with any of a number of javascript frameworks. In my case React. I created a TagFilter
component that would display the list of tags and also the filtered list of posts for that tag. This would also have the initial list of unfiltered posts.
In my BlogList
component I would then call the React component.
---
import { storyblokEditable, useStoryblokApi } from '@storyblok/astro';
import TagFilter from '../components/react/TagFilter';
const storyblokApi = useStoryblokApi();
const { data } = await storyblokApi.get('cdn/stories', {
version: import.meta.env.DEV ? 'draft' : 'published',
content_type: 'blogPost',
is_startpage: false,
});
const posts = data.stories.map((story: any) => {
return {
...story,
date: new Date(story.published_at).toLocaleDateString('en-GB', { dateStyle: 'full' }),
};
});
const response = await storyblokApi.get('cdn/tags/', {
version: import.meta.env.DEV ? 'draft' : 'published',
starts_with: 'blog/',
});
const fetchedTags = response.data ? response.data.tags : [];
const { blok } = Astro.props;
---
<section {...storyblokEditable(blok)}>
<TagFilter tags={fetchedTags} initialPosts={posts} client:load />
</section>
The important bit to note here is the use of client:load
. This tells Astro that this component needs to run on the client. Astro will then include all the necessary javascript and send it through with the request.
Styling and Theme
Up to now, nothing is looking pretty. Tailwind is my go-to CSS framework so I'll be using that again. I wanted to pick a colour scheme that would complement the green Eastgate logo so I used this colour wheel picker. Once I had my base colours (in my case four), I then entered them into this tool. This then allowed me to to export the palette for adding to my tailwind.config.js
file. Using the Tailwind utility classes, I built each of the Storyblok components, making sure to use the colour scheme that I had created.
Originally I had planned to create the larger content blocks to use markdown and then render these accordingly. Storyblok has one more trick up its sleeve thouhgh when used with Astro, renderRichText
. This takes the rich text content and generates the correct HTML. As Tailwind overrides all the formatting of HTML elements, we can use the Tailwind Typography plugin. This gives us the prose class that takes all of the HTML and styles it in a more readable manner.
Deploying
With the site now looking good and all the initial content written, it was time to deploy. With my previous SST projects, I had created the SST project first and then added the other constituent parts (frontend, API, etc...). Since I had already created my site, I could use what SST call "drop in mode". This added a lightweight SST configuration to an existing project. A word of warning, when this happens it overwrites the astro.config.js
file! There is a lesson in here (that I am old enough to know better) to always backup files before running any scripts. I'm ashamed to say that I hadn't even got it source control yet as I was waiting until I had added SST. Lesson learned, I had to rebuild the config file from scratch, remembering what integrations I needed and how they were configured.
After that, I was able to deploy to production using the SST CLI and the site was live. I absolutely love the fact that I can do this without having to touch a single piece of infrastructure. It just works (aside from my config mistake), will scale effortlessly and costs next to nothing to host.
The Results Are In...
Before any tweaking, I wanted to see the performance for myself. A quick run of Lighthouse and I am getting four green rings. I'm happy with that.
All of the source code for this site can be found on GitHub.