Skip to content

Proposal: First Class Shortcode Support #29

@nickreese

Description

@nickreese

Hey Elder.js users.

I want to float my current thinking on shortcodes and the plan to add them to the Elder.js core to get buy-in from early users.

Benefit: Shortcodes make static content dynamic and future proof. Elder.js should support them natively.

The Context

Whether your content lives in .md files, on Prismic, Contentful, WordPress, Strapi, your own CMS, or elsehwere, content is generally pretty static.

That said, anytime content lives in a CMS there is always a demand to add 'functionality' to this content.

In my experience thisfunctionalities come in a few flavors.

  • Adding custom HTML to style/wrap content or achieve design goals. (most common)
  • Other times on data driven sites you want to make just a part of the static content dynamic replacing a datapoint with a real statistic. (see below)
  • Embedding an arbitrary Svelte component.

Shortcodes to solve all of these problems.

Shortcodes

If you aren't familiar with shortcodes are strings that can wrap content or have their own attributes:

  • Self Closing: {{shortcode attribute="" /}}
  • Wrapping: {{shortcode}}wraps{{/shortcode}}

NOTE: The {{ and }} brackets vary from system to system.

1. Custom HTML Output

I'm in the process of porting my own website to Elder.js. During this process there are many times where I want a <div class="box">content here</div> to add design flair.

To achieve these I've built a simple shortcode: {{box}}content here{{/box}} allowing me to change the markup as needed should my needs change in the future.

2. Data With Shortcodes

A common use case for shortcodes that we've seen on ElderGuide.com and other properties that we are developing with Elder.js is the need to have an "article" with otherwise static content but that needs a datapoint from a database. (outlined above)

For example we often need to have content that would have this functionality: The US has [numberOfNursingHomes] nursing homes nationwide.

As it currently stands we do this type of replacement within our data functions and have a shortcode like so: {{stat key="numberOfNursingHomes" /}}.

The Problem:

As I've extracting out functionality from our sites, I'm finding more and more cases where plugins could offer shortcodes as well.

I've also found a major need for a shortcode to embed Svelte components, but the ideal implementation doesn't exist within the current Elder.js framework.

A. Minimizing Plugin Interdependence

My initial plan was to release a shortcode plugin using hooks. The shortcode plugin would register and manage the shortcodes from other plugins.

Then if a sister plugin (say the "image plugin") wanted to offer a shortcode it would require the shortcode plugin to also be installed.

The problem with this approach is plugin interdependence.

It just doesn't feel right for a plugin to only offer 1/3 of it's functionality without another plugin installed. There is also an issue of making sure each plugin doesn't overwrite the other's functionality.

B. data.key replacement

Another hurdle is that in a perfect world we'd be able to do {{svelteComponent component="Clock" props="data.key" options="{loading: "eager"}"/}} and the clock component would be hydrated with the values found on data.key.

In order to achieve this, the shortcode plugin would need the context of the data object returned from the route and would need to run between the generation of the route html and the template html.

Currently there is no hook that can support this... and hooks aren't really the right solution because we only want 1 instance of the shortcode parser to run.

The Plan

Our current implementations use a fork of the https://github.com/metaplatform/meta-shortcodes library. I've used this shortcode library in one form or another since 2016. It is battle tested across my sites, allows for open and close bracket customization, but could use a modernization effort. (not the focus of this proposal)

To implement shortcodes the plan is as follows:

Page.ts:

  1. In Page.ts after the page.route.templateComponent function has built the route's HTML, shortcodes would be executed on the returned HTML.
  2. After the shortcodes have executed, the returned html would then be passed into page.route.layout and page HTML generation would continue as it currently does.

The benefits of this location are 3 fold:

  1. Whether content comes from a CMS, Svelte component, or a data store, we can assume that the shortcodes would be in the HTML returned by the route.
  2. Because we still have to process the page.route.layout we can use Elder.js' internal system for inlineSvelteComponent to embed arbitrary svelte components offering a solution to embed Svelte components {{svelteComponent component="Clock" /}}.
  3. Because all data related hooks and functions will have executed, we know that the data object is stable and could offer the ability to pass the data context into shortcodes as well allowing for {{svelteComponent component="Clock" props="data.key"/}} where data.key would be the key taken from the data object.

Defining Shortcodes:

Shortcodes could be defined in two places:

  1. elder.config.js for user defined shortcodes.
  2. plugins could define shortcodes by returning them during plugin initialization.

Proposed Definition (simple)

  • props are the attributes defined on the shortcode.
  • content is the content wrapped in the shortcode.
  • data is the data object.
// elder.config.js

shortcodes: [
        {
          shortcode: 'box',
          run: (props, content, data) => {
            return `<div class="box">${content}</div>`;
          },
        },
]

Proposed Definition (robust)

The biggest limitation to the 'simple' design is there is no access to add css, js, or elements to the <head>.

I've hit this limitation in the past with WordPress and it was limiting, so a more complex design that would be more robust would be to support an API signature as below where css, js, and head are all added to the stacks needed to support them.

NOTE: this requires moving where stacks are processed.

// elder.config.js

shortcodes: [
        {
          shortcode: 'box',
          run: (props, content, data) => {
            return { html: `<div class="box">${content}</div>`, css:'', js: '', head:'' }
          },
        },
]

Shortcodes by Default

By default the only shortcode I'd imagine shipping is the {{svelteComponent component="Clock" props="" options="" /}}.

This does add a small regex call that appears to add about 1.2ms per page generation time in local testing.


This is the first major feature that I'm looking at adding to the core and I'd like community feedback. I'm not sure how OSS projects usually handle this but my goal is to get buy-in from early users. If you have feedback I'd love to hear it.

Having used several static site generators, I do believe this functionality belongs in the core. One of my biggest gripes with Gatsby is that it pushes all of the customization to plugins and solves the plugin interdependence problem with "Themes." While this works for one-off projects, it doesn't allow easy copy and pasting between projects because each Gatsby project is it's own special snowflake due to plugin interdependence. I think Elder.js offering shortcodes from the core is a wise move but would love feedback and some devil's advocates on why it shouldn't be in the core.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions