Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Keeping Jekyll up to date with Stackbit V2

10 Sep 2021 🔖 web development jamstack
💬 EN

Table of Contents

I called it:

“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…

Screenshot of editing the site in Stackbit

…To having no Stackbit editability at all.

Screenshot with editability disappeared from Stackbit

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.

Stackbit model editor

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!

Screenshot:  pages are once again editable in Stackbit Studio, although only from the sidebar

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>

Screenshot:  outer sections are once again addable and deleteable in Stackbit Studio's preview panel

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.

Screenshot:  inner tasks are once again addable and deleteable in Stackbit Studio's preview panel

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 the I 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>
...

Screenshot:  text is again editable in Stackbit Studio's preview panel

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?)

--- ---