How Craft, GraphQL, and a TypeScript frontend forces me to develop defensively

02/02/2024

The current iteration of this site still uses Craft CMS as a backend (I think that's the first time I've ever re-developed this site but kept the backend the same), and it's using GraphQL as it did previously, but it's now using Astro with TypeScript for the frontend, rather than Next.js. I like working with React for lots of things, but it annoys me so much in many ways that I didn't really want to go through that again. We've started using Astro for some headless stuff, owing to its simplicity and the fact that you can run it directly in Cloudflare, so it's totally abstracted from your backend. I still remain surprised by how quickly we can go Browser -> Cloudflare Pages -> Craft -> Cloudflare Pages -> Browser. It doesn't seem like it should be physically possible, but it seems to be happening!

I wanted to write about this because every time I learn something new like this, I feel like I have to un-learn everything that I had previously thought was bad, and change my opinions before I can really get to grips with something. I've never considered myself to be so mercurial before, but apparently you have to be to keep up with modern software trends. By the time I'm done with this post, I expect it to be outdated.

Something that really helped me to get behind this approach is the fact that this blog has three different post types: a block-based structure where I choose from text/images/YouTube videos/Github Gists, and a couple of other legacy block types to construct a post; a status-style post which is super stripped down; and a legacy style which is just a WYSIWYG everything gets dumped into. The reason this is so fundamental to everything is that these are all different types in GraphQL, so they're different types in Astro, and if you're in the context of a blog post, you have to be super defensive about which type of blog post when accessing fields in case they don't exist. In Craft's implementation of GraphQL, it seems like every field is optional, so you always have to check if things are undefined, but you also need to check the __typename of everything before even accessing those variables otherwise linters and LSPs start to complain (and rightly so - no point using TS if you're just going to ignore it).

Astro's markup structure is HTML-like with JS. This is something that I call JSX sometimes but I don't think that's technically correct. JS devs are always quick to point out when you're not being technically correct. It's really useful to be able to use JS in your templates for simple flow control and loops, but anything beyond that begins to get really horrible to read and maintain. I wanted to avoid, as much as possible {entry.__typename == 'blog_Blog_Entry' && (<div... throughout everything, and instead determine what the typename is once, then call a component specifically build to handle that one post type. I thought it'd make things easier to read. And to make matters slightly more complicated, my content block approach above is used both in posts and pages, so I wanted to adhere to Don't Repeat Yourself here. The way I've done this is with a Factory-style approach that just acts like a wrapper that decides what to do when each type is encountered.

The factory component is called from my main page - the only thing I need to do in the page is make sure that I pull in the fragment defined in the factory into my query document so that the data is there when I include the component, then pass that data to the component. This way of working seems totally logical to me, but also goes against everything I learned for a significant part of my career, which is that your view logic should be decoupled from everything else. Now my way of thinking is very much that my view logic is heavily dependent on everything else, so why wouldn't I define it together? I'm so tired of having to hop from file to file to find out how some data got from where it was called to where it was used to try and figure out why it didn't arrive. This approach, whilst obviously not infallible, reduces those kinds of issues significantly. It also means that once everything's set up, if I wanted to add a new block type, I can just do it in one file, and not have to add calls to it in multiple locations just to make sure it ends up in the implementation itself.

Rather than waffle on about this any longer, it's probably best to just show an example. This is excerpted and I've removed some stuff that isn't relevant. Typically, Github has decided that the order I defined the files isn't the order that I actually wanted to show them in, so here they all are here.

You can see how messy ContentBlocks.astro looks in comparison because it has the typename checks in there as well as the block implementation. Whilst there's an argument to be made for going a level deeper and making each of those their own components, I can live with this for maintenance's sake, otherwise adding another block type does become quite a lot more labour intensive.

Because Astro only lets you define one component per file, I have ended up with a lot of files defining each of my entry types. Whilst this was a drag to create (and learn - I felt like I was losing my mind when it wasn't working!), it's a lot easier to maintain. Once my [slug] file is calling my Factory fragment, which calls my post type fragments, which call the data they rely on, maintenance and debugging become so much easier. To add a content block, I only need to add it in ContentBlocks.astro, and it will appear wherever I have previously set up to use ContentBlocks because I changed the fragment they all reference. Writing it like that seems like stating the obvious, but I have been pretty opposed to this way of working for a very long time and I'm now wondering very loudly why that is!
 

Like a Sunday Morning I've added a new Media Diet area on this site. I have been bouncing the idea of doing this for a...