Build a headless blog with Astro and Optimizely SaaS CMS Part 3
It is finally time to explore my basic blog example powered by Astro and Opti SaaS CMS. For those new to the series, you may want to read parts one and two first.
Project Structure
I've mostly kept to the standard project structure as initiated by the Astro blog template so it should be familiar to most people who have worked with the popular front-end frameworks.
First, on the right, we can see the two main configuration files: `astro.config.mjs` is quite barebones and imports some additional functionality (automatic sitemap & Vercel platform support) and we also enable view transitions for browsers that support it. `.env` contains some SaaS CMS environment variables.
Looking at the left of the image, we can follow the process of an incoming web request to a specific page URL:
- First, load the relevant logic for the specific URL path from the `pages` folder and execute the logic found. Similar to what we can think of as a 'controller' in MVC terms. In this example, I've set a catch-all route based on Astro naming conventions with `[...slug].astro`. It then loads the as-yet-unknown 'model for this URL by calling apollo-client.ts which talks to Optimizely Graph via GraphQL (more on this later).
- The route then uses the 'model' to populate common shared `components` like the header and footer, and most importantly the `TemplateResolver.astro` (also commonly referred to as TemplateCoordinator) to figure out what page type this 'model' is
- Once we have the page type figured out, we load and populate its exact layout from `layouts` so we can process the rendering of the 'view', here of the StartPage
It's all very familiar to those of us coming from the back-end world of doing this with PaaS in .NET. I'm sure there are other (better?) ways to structure a solution like this, but given the audience here I think this is easiest to follow.
Did Someone Say Graph?
In `apollo-client.ts` I have all the GraphQL queries needed to render everything for this site from the Optimizely Graph.
We're also caching the results using the built-in `InMemoryCache`, not that it matters too much for a statically generated site, but we might as well ensure our build times are quick when it is so easy to enable 😇
Admittedly, I've gone for the "quick demo" approach and built one big query to rule them all™ to resolve content by URL rather than what you'd actually want which is more finely scoped queries for each unique page/content type, but the approach is largely the same you just end up with multiple chained requests to the graph.
This could be further improved by using a package to automatically create TypeScript models based on what is indexed in the graph, as in the Optimizely Nextjs Demo, but I've not looked into that for this demo. It's standard GraphQL / Astro functionality here so shouldn't be too difficult.
CMS Editing
I didn't want to publish this post before I had time to wrap this up, and thanks to the holiday season I finally could! Optimizely provide documentation on the full process.
For this, I created an additional catch-all route in the `pages` directory, placing it under a new child `preview` folder. This means we now have a route registered for any request under `/preview` so that we can execute edit-only preview logic differently.
As on-page editing / visual builder is an interactive experience, this won't quite work for our statically generated website. So we tell Astro to not prerender this page route - thereby making it dynamic. Then we follow the guidance from Optimizely for what we can expect as query parameters of the /preview URL when editors view the page in on-page editing. Most importantly we need to use the provided preview token for our existing graph queries so that we can load unpublished/draft content based on content version rather than URL (as the page may not have a URL yet).
For the rendering side, we need to load the provided communication script as instructed by Opti, and setup a client-side event listener to detect the content saved event and refresh the on page editing experience for the editor.
Finally, we need to wrap our editable layout elements with `data-epi-edit` HTML tags matching the property name as indexed in Graph. Example here for a page `MainBody`
<span data-epi-edit='MainBody'>
<div set:html={pageContent.MainBody.html} />
</span>
And just like that, we have on-page editing and preview support 😁
Conclusion
We've finally reached what I had in mind for this project when I originally thought of it. We have a simple, yet solid, base template for a SaaS CMS site with Astro that mostly focuses on content that doesn't change all that often. I could 100% use this to power my personal blog by swopping out the markdown files I currently use and instead pull content from Graph as we just saw for this demo.
I have two more ideas in mind to build on:
- Render some purely Visual Builder content
- Render the whole thing dynamically rather than statically.
See you next time 😉
As always, code on Github: https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo
Comments