Eleventy and Craft
I love Eleventy, and I love it even more when I can split code from content.
Earlier this summer, I switched this site from Jekyll to Eleventy. Jekyll was fine, but I'm not a Ruby developer, so when I ran into issues or wanted to extend functionality a little bit, I was stuck. Not that big of a deal, because I didn't need that much extra functionality, but then I ran into build errors. Suddenly Ruby, or Jekyll, or Bundler, or something was behind or ahead by a couple versions, and I couldn't get anything to work anymore. Not only could I not add functionality, now I couldn't manage my site at all.
"BUT VINCE," you might be saying, "THIS IS A GREAT OPPORTUNITY TO LEARN RUBY. EMBRACE IT."
Learn Ruby? In THIS economy?!
Instead, I looked into this Eleventy SSG (static site generator) I'd heard everyone talking about. A few hours later, I was up and running. Surely, this would help encourage me to write more, like I'd promised myself so many times before.
A few months pass, and nothing happens.
It took me that long to pinpoint my issues:
- I wanted deployment to be automagical, but I didn't want to deal with deploying from a repo. Hell, the thought alone of dealing with webhooks for this little ole site gives me hives.
- I enjoy using Markdown, but writing blog posts in a dev environment, then pushing them up to a repo, then logging into my server and doing a build there was such a hassle, I avoided publishing anything I'd managed to finish writing.
- I wanted to keep my site repo open, but writing blog posts over time in an open repo makes me really anxious.
Everything I considered kept telling me I need to separate code from content—a radical idea, I know. So now, this site is generated by Eleventy using Craft CMS, and it's (mostly) found on GitHub. (And this is the first blog post I've published using this method. NEAT.)
I did this by separating the build process into three pieces:
- The JSON API
- Eleventy's configuration and templates
- A "smarter" build process
The JSON API
Craft's Element API plugin makes it easy to define some endpoints, tell it what content to include, and even do a little finessing of things before it is sent back to my Eleventy build script.
Since every site is special and unique, I won't get into my endpoints' specifics, but assuming you installed Craft and the Elements API plugin, and created a channel-type section named "Blog" with some fields named summary
and blogPost
, your elements-api.php
config file would look something like this:
If you were to visit example.com/entries.json
, you'd receive something like this:
In addition to the Elements API plugin, Craft released version 3.3, with support for a headless mode and built-in GraphQL API, no plugin needed. I was nearly finished with this process when they released v3.3, and since any GraphQL API was a bit overpowered for my purposes, I decided not to backtrack and redo things using these new built-in features.
You do you, though. I believe in you and all of your wildest dreams.
Eleventy's configuration and templates
If you're interested in Eleventy's general configuration, I refer you to its wonderful documentation. There's a lot in there, but it's well organized, and pretty accessible to all JavaScript skill levels.
I need Eleventy to consume and build pages from this JSON API, so I decided to use fetch
(because it's the tool I'm most familiar with, but Node has several options for making HTTP requests; follow your heart on this one):
You may have noticed that there is a second, seemingly unnecessary .then()
method in the chain. I did that as a shortcut because, by default, the Elements API returns a JSON object with two root properties, data
and meta
, the latter containing pagination data, which is of no use to me here.
What to do now that Eleventy is retrieving the JSON? Make it usable. Building on the above code:
At this point, I iterate over every entry returned by the API, converting each entry's published_at
value to a proper JavaScript Date()
object, and also converting the post's Markdown here, rather than in a template.
Hooray, now Eleventy has a collection
named "blogPosts" that it can work with as if it were a set of Markdown files in its src
folder. In my setup, I have a template named blog-posts.html
with the following front-matter:
Again, I'll defer to Eleventy's documentaion on pagination for specifics, but this front-matter is telling Eleventy to use the collection we added from the API (collections.blogPosts
) and treat it as if the data
were coming from files in src
, regardless of what templating engine you choose.
A "smarter" build process
Here is where I got hung up for a while. My goal was automating the Eleventy build, but I also didn't want to run Eleventy if I didn't need to. Out of the box, Eleventy doesn't provide a way to conditionally build—and really, why would it? I knew I could detect changes in content simply by hashing the API response, and running the build step if there had been a change, so I set out to write a cron job to handle this.
Because that makes sense, right?
Sure... and then it didn't work. Things were executed, but the errors piled up, mostly the "THIS VARIABLE IS UNDEFINED" variety. So I wrote some more code to account for how cron works with Node2.
But you know what's fun about researching "Node" and "cron?" You don't find a ton of information about running Node scripts via cron, instead you get a lot of information about node-cron, a Node package that allows you to schedule tasks in a Node app using the same syntax as cron (more or less). I tossed this information to the side because I didn't want something like cron, I wanted to run a Node script via Cron, dammit. Give me THAT knowledge, plz, google.
Dear reader, I should've taken the hint.
So here's my new and improved, node-cron-powered scheduled-build.js
:
This works and after scratching my head for a while, it's a relief. But scheduling tasks via node-cron only works if the script itself is running.
Hello, PM2. PM2 keeps things running without my involvement, AND it solves the issue of logging build results. HOORAY.
That's it. That's the post.
I'm pretty happy with how things turned out, but there are a couple of gotchas:
- The scheduled build process runs both Eleventy and Gulp, but only if Eleventy's content has changed. If I make some CSS or JavaScript changes, I still need to run the build script locally and upload the new CSS files manually. It would be nice to have the script check for changes relevant only to Gulp, too.
- The process does not have any support for deleting posts. If I take down a post, Eleventy sees that something has changed, but there isn't anything in place to tell it "hey, build the site, but also delete these things, too." Not a deal-breaker, but also not critical to me right now.
- Closely related to (2), I noticed that Craft hid a couple of entries from the API response (they weren't visible in the control panel, either!) that only returned after I ran an update on the CMS. Don't know what happened, but if I fix gotcha #2 and this happens again, it means content disappears from the site without me knowing it. This is part of the reason I added as much logging as I did.
Footnotes
I've still not found "official" documentation about this (which I would love to read, if anyone out there has it!), but from what I've gathered, the gist is this: you can run Node scripts via cron, but even if you set the working directory, and pass that down to any
exec
commands you run, Node will eventually not be able to find the contents ofnode_modules
and things will break.