Keeping Jekyll up to date with Stackbit V2
10 Sep 2021
“The only Wix-like experience I’ve found so far that supports Jekyll is Stackbit … but who knows if they will forever? I can’t imagine that adding a live-edit drag-and-drop skin to Jekyll’s
localhost
engine has been easy for them to code, as amazing of a gift to the world it’s been that they figured it out.”
Their second major release of the Stackbit Studio no longer supports my theme.
How do I know?
Using Stackbit’s built-in code editor, I took the stackbit.yaml file from my Jekyll WYSIWYG site builder theme and changed stackbitVersion: ~0.3.0
to say 0.4.0
instead.
Immediately, I went from being able to edit my page inline…
…To having no Stackbit editability at all.
The model editor looked fine and recognized index.md
and two.md
as conforming to my “Advanced Page” page model, so it was time to experiment with their new “attributes & highlights” system.
According to the documentation, if I wanted to keep using Jekyll with the latest versions of Stackbit, I was going to have to pollute my code a little bit:
“Projects using a stackbit.yaml version of 0.3.x or lower will use automatic annotation inference. This is more magical, but less accurate and much slower. To enable annotations, bump the version of stackbit.yaml to 0.4.0 and then annotate all your components.”
Adding data-object IDs to every page
In “Choosing a headless CMS without losing your head,” I said:
“I need a CMS that can manage not only ‘lists’ and ‘objects’ of simple data types as page elements, but ‘lists of lists,’ ‘objects containing objects,’ ‘objects containing lists,’ and ‘lists of objects’ (allowing restrictions on object type, but also allowing multiple valid object types) – preferably with no limitation on the nesting depth.”
And then I complained about Contentful:
“I don’t see as deep of object/list nesting in Contentful as in Sanity. That means you need to store objects at the ‘top level’ before using them rather than them being non-reusable ‘details’ that form part of a page. It’s probably not the end of the world to make authors store things like ‘CTA Homepage 01’ in a top-level Call-To-Actions document, but Sanity is kind of nifty letting it only ‘exist’ within the homepage.”
Soooo … in Stackbit v2, you’d have pass your front-end HTML the record ID of 1ab23c4d5ef67
or whatever as a data-sb-obj-id
for every “call to action” in your entire website if your backend data source were Contentful, but only for the page records themselves if you were using Sanity.
My site is architected closer to Sanity, in that the only “top-level” identifiable records are my Markdown files under /pages/advanced
, only instead of having record IDs, I distinguish them from each other using filepaths.
Therefore, I’m going to try editing <div class="xyzzy">
in /_layouts/advanced_page.html
to instead read as <div class="xyzzy" data-sb-object-id="{{ page.path }}">
:
---
layout: standard_header_footer
---
<div class="xyzzy" data-sb-object-id="{{ page.path }}">
...
</div>
That was the right guess – immediately, when I hover over my main body of content in the live preview, it gets a blue box around it that reads “Advanced Page,” and the left sidebar fills up with a data structure that lets me change the content of my pink section from I did it!
to I did it! It's editable at left!
Keeping unnecessary code out of production
FWIW, what I actually ended up doing was creating a new /_includes/stackbit_data_object_id.html
file that reads:
{%- capture sb_obj_id -%} data-sb-object-id="{{ page.path }}"{%- endcapture -%}
{%- comment -%}At this moment, jekyll.env is reading backwards for production vs development, so this code will look weird{%- endcomment -%}
{%- unless jekyll.environment == "development" -%}
{{ sb_obj_id }}
{%- endunless -%}
And then /_layouts/advanced_page.html
reads:
---
layout: standard_header_footer
---
<div class="xyzzy" {% include stackbit_data_object_id.html %}>
...
</div>
This is my own homemade Jekyll equivalent of Stackbit’s utility methods for React, keeping these data attributes out of my production front-end HTML where they’re not needed.
Baby bug
(lol, the only problem being that Stackbit seems to be flipping JEKYLL_ENV
to say production
when in jekyll serve
mode and development
when in jekyll build mode
… I’ll have to report that bug to them.)
Adding data-field names to every page
I still can’t double-click on any text in my live preview and edit it, or add/remove sections from the main preview panel. I’ll try to fix that next.
Outer section list
The div around each section was the only HTML element in /_layouts/advanced_page.html
, so for now, while I develop, I’ll add an extra DIV
, just to be safe.
---
layout: standard_header_footer
---
<div class="xyzzy" {% include stackbit_data_object_id.html %}>
<article data-sb-field-path="sections">
{% for section in page.sections %}
...
{% endfor %}
</article>
</div>
That didn’t do anything useful, but I’m not terribly surprised, since sections
is a list. Let’s try identifying its child objects.
Once again, I need to add a spare DIV
that wasn’t there before.
I had noble intentions of keeping all this Stackbit annotation stuff out of my frontend, but I think that idea is shot. There’s no graceful way, in Liquid, to conditionally wrap HTML elements in parent HTML elements. (This isn’t Nunjucks with its macros.)
However, I do have excellent news to report: I can once again add and remove top-level sections from within Stackbit Studio’s preview panel, by wrapping my {% include {{ sectionTemplateFile }} section=section %}
call in a <div data-sb-field-path=".[-1]">
. (Jekyll starts counting the loop at 1; Stackbit at 0.)
Note that I label the loop-item index .[...]
with a leading dot. This helps Stackbit Studio understand that it’s the list whose field key is sections
, which I had just specified to Stackbit in the next HTML element up.
Also, Stackbit didn’t mind at all me putting a data-sb-object-id
and a data-sb-field-path
in the same DIV
, so I was able to get rid of the redundant ARTICLE
element.
---
layout: standard_header_footer
---
<div class="xyzzy" {% include stackbit_data_object_id.html %} data-sb-field-path="sections">
{% for section in page.sections %}
{% assign sectionType = section.type %}
{% if section and sectionType %}
{% capture sectionTemplateFile %}{{ sectionType }}.html{% endcapture %}
{% capture stackbitWhichSection %}{{ forloop.index | minus: 1 }}{% endcapture %}
<section data-sb-field-path=".[{{ stackbitWhichSection }}]">
{% include {{ sectionTemplateFile }} section=section %}
</section>
{% endif %}
{% endfor %}
</div>
Of course, I can’t yet edit any text from the preview panel, because I haven’t annotated that deeply, nor can I add/remove individual tasks, but we’ll try that next.
Inner task list
Here’s the new codebase for /_includes/section_task_list.html
:
...
<div class="task-list" data-sb-field-path=".accomplishments">
{% for task in task_list_section.accomplishments %}
{% capture alternatingClassName %}{% cycle 'task-odd', 'task-even' %}{% endcapture %}
{% capture stackbitWhichTask %}{{ forloop.index | minus: 1 }}{% endcapture %}
{% include item_task.html taskDetail=task alternatingClassName=alternatingClassName stackbitWhichTask=stackbitWhichTask %}
{% endfor %}
</div>
...
(By the way, the reason I explicitly re-pass paramaters like alternatingClassName
and stackbitWhichTask
to Jekyll includes is to keep my Liquid include syntax ready for a possible migration to 11ty.)
The code for /_includes/item_task.html
is now:
...
{% capture classes %}task {{ include.alternatingClassName }}{% endcapture %}
<div class="{{ classes }}" data-sb-field-path=".[{{ include.stackbitWhichTask }}]">
<b>{{ checkboxSymbol}}</b> <b>{{ single_task.task }}</b>
{% unless single_task.how == blank %}
<i> <span>{{ single_task.how }}</span></i>
{% endunless %}
</div>
...
Note that I label the DIV
outside the for-loop .accomplishments
with a leading dot, not accomplishments
. This helps Stackbit Studio understand that accomplishments
is a field key it should look for within the context of the section through which it arrived, rather than trying to find it as a top-level property of the page’s data.
Also, I label the inner loop-item index .[...]
with a leading dot. This helps Stackbit Studio understand that it’s the list whose field key is accomplishments
, which I had just specified to Stackbit in the next HTML element up.
UI-wise, I noticed that the moment I annotated this level of detail, it became impossible to visually click the “Tasks Section” as a whole in the preview panel. That’s a side effect of the DIV
I labeled .accomplishments
taking up 100% of the screen-space of its parent SECTION
that I labeled .[]
. A real-world theme would probably have a bit of padding and margin that would give me some actual clickable space between the parent and child HTML elements. With my theme’s CSS as it is, I have to use the left sidebar to delete the whole Tasks section at once.
Detail fields
Now I just need to go through and label .say
in the “pink sections,” .mention
in the “blue sections,” and .task
+ .how
in the “task items.”
(done
was never editable in the preview panel, even in the “magical” older version of Stackbit.)
The final code for /_includes/item_task.html
now annotates .task
and .how
:
...
<b>{{ checkboxSymbol}}</b> <b><span data-sb-field-path=".task">{{ single_task.task }}</span></b>
{% unless single_task.how == blank %}
<i> <span data-sb-field-path=".how">{{ single_task.how }}</span></i>
{% endunless %}
...
Note that text editing didn’t work unless I added actual SPAN
tags around the text I wanted to make editable.
- Putting it into the
B
tag didn’t work - Nor did hoping that Studio would “magically” still be able to distinguish the leading space from
.how
by putting it in theI
tag.
The final code for /_includes/section_blue.html
now annotates .task
and .how
:
...
<div class="blue-div" data-sb-field-path=".mention">
{{ blue_section.mention }}
</div>
...
The final code for /_includes/section_pink.html
now annotates .task
and .how
:
...
<div class="pink-div" data-sb-field-path=".say">
{{ pink_section.say }}
</div>
...
Conclusions
That wasn’t so bad.
It feels a little cluttered, yes.
But CloudCannon also makes you mark up your HTML for their engine’s benefit, that’s the only other WYSIWYG editor for older CMSes I know of.
I suspect that from my perspective, this will be a downgrade on the Jekyll experience (because the “magic” used to work quite nicely) and perhaps I’ll keep using 0.3.0
as long as it’s supported.
However, I imagine it might be an upgrade on the experience with 11ty projects using really weird data sources and complex pagination / routing. With the “magic” older version of Stackbit, it didn’t work so well. (I can’t blame it – there are just way too many unpredictable ways that Eleventy is more than happy to pass data from a source to being rendered.)
I look forward to trying an old 11ty site that “didn’t work” in Stackbit and seeing if I can now get it working.
(Update: okay, first I have to remember what it was I was doing in 11ty that breaks Stackbit V1. Because my standard “page builder” using file-by-file .md works fine in Stackbit V1. Maybe it was a pagination thing against single data files that contained lots of records, each of which needed a URL?)