How to: Interactive blog posts with no build step

Published

June 26, 2021

demo web components js lit

What tools should you use to build a website?

Unless you love hand writing HTML, you may have glanced over this Site Generator list. These are free tools that take your non-HTML code and return you a website.

So what non-HTML coding variant then?

Markdown is simple and allows quick content creation. It does feel a bit static. Those entire frontend frameworks seem like they might be a bit much for a blog though.

I’ll share how I

Now with the click bait out of the way, here’s what I want you to get out of this post.

  • You can mix markdown and dynamic content. I’m doing it now.
  • Modern JavaScript is clean, can remove build tools, and I want to make you aware of some great features.
  • To stay honest, you can copy paste this page and interact with it as you read it… or read it directly in some online code editor while modifying it.
  • Finally I’ll share what optimizations have been left out that tooling solves.

These opinions are my own. I love all things web so you should use what’s fun when working on your own projects! Lets dive in!

For learning you can copy this post

Included is the markdown, image, and the interactive bits. I’ve kept out global visual styles, and the copy/paste logic stuff.

  1. Press the “Toggle Raw” button.
  2. Copy the HTML with the button, or manually.
  3. Paste it into your favorite index.html location. Here are two options I am familiar with:
    1. CodePen
    2. Lit Playground

or, skip all that and play with the code here on repl.it: https://replit.com/@AndrewJakubowic/making-of-a-blog-post#index.html

Great!!! Now you have a slightly less styled version of this post.

How I like to write content

I find markdown incredibly easy and quick to write. It’s a very flexible format, allowing you to drop in html tags when required. Like this details tag:

What's in here!?

We’re inside a details tag!

And markdown works in here as well!

This site is built with Hugo, but many Static Site Generators will turn the markdown into html for you.

To make the copy/paste work, I can’t rely on a build step, so I used a <wc-markdown> web component for rendering the markdown on the client.

But markdown is the easy part. For me getting behavior and interactive content is where I’ve spent most of my time.

Defining Behavior

Adding behavior to markdown needs to be no more complicated than adding an html tag. The snag in that plan is that we need custom html tags that don’t exist yet.

You can invent them yourself with Web Components! Now you can define your own custom HTML tags! This page uses Lit, which literally extends web components with some quality of life helpers (like reactivity).

Local interactivity

Here’s an exciting web component:

To save you looking at the raw markdown, it looks like this: <local-counter-btn label="☝"></local-counter-btn>. And it’s sitting right here in the markdown!

Web components also compose! Inspired by this post, I’ve written my own take on the happy-thing component named <flip-me>.

This component flips whatever it surrounds. Including this markdown (on hover or touch).

And You can compose components just like you would html tags:

Neat!

Shared Communcation

We’ve seen some web components with local state. A truly immersive post has shared communication.

Custom vents work really well on small scale posts. You can send an event with additional details defined, and any components that care will be listening to it.

I’ve written two small components:

  1. <number-view> component that stores a local counter and listens for custom increment events.
  2. <state-counter-btn> that fires custom increment events.

Now we can put increment buttons everywhere.

βž• βž•

If you wanted to keep all the numbers in sync, we could store the state centrally. This seemed more fun!

Libraries and no build step??? 😲

Thanks to JavaScript Modules working in all major browsers we can now write code that looks like this without any build step:

import {LitElement, html, css} from 'https://cdn.skypack.dev/lit';

Modern tools use this and the benefits are outrageous. For example snowpack can use these modules to build only the file you changed, leading to quicker incremental builds!

Importing a library from Skypack directly without any npm feels magic.

Missing optimizations

If you are viewing the copy pasted version of this post, you’re seeing everything client side rendered. That’s frankly a lot of work! Server side rendering can provide a blazing fast experience.

Tooling can also do more than handle your imports, they can minify and tree shake your code. This site uses esbuild for minifying and preparing the code on every page.

Conclusion

JavaScript modules and Web Components provide simpler development by delegating the hard work to the browser!

We’re quickly moving into a future where build tooling isn’t always required. I couldn’t be more delighted and thankful.

<!DOCTYPE html>
<body>
<wc-markdown>
<script type="wc-content">

What tools should you use to build a website?

Unless you love hand writing HTML, you may have glanced over this [Site Generator list](https://jamstack.org/generators/).
These are free tools that take your non-HTML code and return you a website.

So what non-HTML coding variant then?

Markdown is simple and allows quick content creation. It does feel a bit static.
Those entire frontend frameworks seem like they might be a bit much for a blog though.

I'll share how I
<text-changer word-list='["make experiences.","design mind-🀯 demos.", "express myself. 😺", "✨dazzle✨ the audience.", "build content easily.", "stay vendor unlocked.", "use the platform.", "built this page.", "leverage standards."]'></text-changer>

Now with the click bait out of the way, here's what I want you to get out of this post.

- You can mix markdown and dynamic content. I'm doing it now. <text-changer word-list='["πŸ™‚", "πŸ˜‰", "😊", "πŸ™ƒ", "πŸ˜‡"]'></text-changer>
- Modern JavaScript is clean, can remove build tools, and I want to make you aware of some great features.
- To stay honest, you can copy paste this page and interact with it as you read it... or read it directly in some online code editor while modifying it.
- Finally I'll share what optimizations have been left out that tooling solves.

These opinions are my own.
I love all things web so you should use what's fun when working on your own projects!
Lets dive in!

<always-has-been-img></always-has-been-img>

## For learning you can copy this post

Included is the markdown, image, and the interactive bits.
I've kept out global visual styles, and the copy/paste logic stuff.

1. Press the "Toggle Raw" button.
2. Copy the HTML with the button, or manually.
3. Paste it into your favorite index.html location. Here are two options I am familiar with:
   1. [CodePen](https://codepen.io/pen/?editors=1000)
   2. [Lit Playground](https://lit.dev/playground/#project=W3sibmFtZSI6ImluZGV4Lmh0bWwiLCJjb250ZW50IjoiIn1d)

**or**, I've also set up an environment with the code here: https://replit.com/@AndrewJakubowic/making-of-a-blog-post#index.html

Great!!!
Now you have a slightly less styled version of this post.

## How I like to write content

I find markdown incredibly easy and quick to write.
It's a very flexible format, allowing you to drop in html tags when required.
Like this `details` tag:

<details style="border: 2px solid #6085ff; padding: 0.2rem;">
  <summary style="cursor: pointer;">What's in here!?</summary>

We're inside a [details](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)
tag!

And markdown _works_ in here as well!

</details>

This site is built with [Hugo](https://gohugo.io/),
but many [Static Site Generators](https://jamstack.org/generators/)
will turn the markdown into html for you.

To make the copy/paste work, I can't rely on a build step, so I used a [`<wc-markdown>`](https://github.com/vanillawc/wc-markdown)
web component for rendering the markdown on the client.

But markdown is the easy part.
For me getting behavior and interactive content is where I've spent most of my time.

## Defining Behavior

Adding behavior to markdown needs to be no more complicated
than adding an html tag.
The snag in that plan is that we need custom html tags _that don't exist yet_.

You can invent them yourself with [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components)!
Now you can define your own custom HTML tags!
This page uses [Lit](https://lit.dev/), which [literally extends web components](https://github.com/lit/lit/blob/2ba8a4deab411c6c8f69f3bd75c9b62c2942c769/packages/reactive-element/src/reactive-element.ts#L284)
with some quality of life helpers (like reactivity).

### Local interactivity

Here's an exciting web component: <local-counter-btn label="☝"></local-counter-btn>

To save you looking at the raw markdown, it looks like this: `<local-counter-btn label="☝"></local-counter-btn>`.
And it's sitting right here in the markdown!

Web components also compose!
Inspired by [this post](https://meowni.ca/posts/web-components-with-otters/), I've written my own take on the [happy-thing](https://glitch.com/edit/#!/polymer-custom-element?path=public%2Fhappy-thing.html%3A1%3A0)
component named `<flip-me>`.

<flip-me>

This component flips whatever it surrounds.
Including **this** markdown (on hover or touch).

</flip-me>

And You can compose components just like you would html tags:

<flip-me>
  <local-counter-btn label="☝"></local-counter-btn>
</flip-me>

Neat!

### Shared Commun<number-view counter="1"></number-view>cation

We've seen some web components with local state.
A truly immersive post has shared communication.

[Custom <number-view counter="3"></number-view>vents](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) work really well on small scale posts.
You can send an event with additional `details` defined, and any components that care will be listening to it.

<aside class="post-aside keep">πŸ”₯<number-view counter="-100"></number-view>!!!πŸ”₯</aside>

I've written two small components:

1.  `<number-view>` component that stores a local counter and listens for custom increment events.
2.  `<state-counter-btn>` that fires custom increment events.

Now we can put <state-counter-btn>increment buttons</state-counter-btn> everywhere.

<state-counter-btn>βž• <number-view counter="0"></number-view> βž•</state-counter-btn>

If you wanted to keep all the numbers in sync, we could store the state centrally.
This seemed more fun! <text-changer word-list='["😈", "πŸ€ͺ", "πŸ₯³", "πŸ™ƒ", "πŸ‘€"]'></text-changer>

## Libraries and no build step??? 😲

Thanks to [JavaScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) working
in all major browsers we can now write code that looks like this without any build step:

`import {LitElement, html, css} from 'https://cdn.skypack.dev/lit';`

Modern tools use this and the benefits are outrageous.
For example [snowpack](https://www.snowpack.dev/) can use these modules to build only the file you changed,
leading to quicker incremental builds!

Importing a library from [Skypack](https://www.skypack.dev/) directly without any `npm` feels magic.

## Missing optimizations

If you are viewing the copy pasted version of this post, you're seeing everything client side rendered.
That's frankly a lot of work! Server side rendering can provide a blazing fast experience.

Tooling can also do more than handle your imports, they can minify and tree shake your code.
This site uses [esbuild](https://esbuild.github.io/) for minifying and preparing the code on every page.

## Conclusion

[JavaScript modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) and Web Components
provide simpler development by delegating the hard work to the browser!

We're quickly moving into a future where build tooling isn't always required.
I couldn't be more delighted and thankful.

</script>
</wc-markdown>

<script defer type="module" src="https://cdn.jsdelivr.net/gh/vanillawc/wc-markdown@1/index.js"></script>

<script type="module">
/**
 * We are gonna use JavaScript Module imports!
 * reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
 */
import {LitElement, html, css, svg} from 'https://cdn.skypack.dev/lit';
import {classMap} from 'https://cdn.skypack.dev/lit/directives/class-map.js';

class SVGImageText extends LitElement {

  static get styles () {
    return css`
      svg .svg-text {
        fill: white;
        font: 10px sans-serif;
      }
    `
  }
  render () {
    return svg`<svg height="100%" width="100%" viewBox="0 0 189 100" xmlns="http://www.w3.org/2000/svg">
    <image xlink:href="data:image/jpg;base64,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" width="100%" height="100%" x="0" y="0" />

    <text x="60" y="42" class="svg-text">its all html</text>
    <text x="100" y="9" class="svg-text">always has been</text>
    </svg>`
  }
}

customElements.define('always-has-been-img', SVGImageText);

class TextChanger extends LitElement {
  static get properties() {
    return {
      words: {attribute: 'word-list', type: Array},
      targetWord: {attribute: false, state: true},
    }
  }

  counter = 0;

  constructor () {
    super();
    this.words = [];
    this.targetWord = ''
    this.timeoutId = null;
  }

  firstUpdated () {
    this.changeTargetWord()
  }

  changeTargetWord = async () => {
    const sleep = () =>  new Promise((resolve) => setTimeout(() => resolve(), (Math.random() < 0.08 ? Math.random() * 100 + 200 : Math.random() * 35) + 20));
    if (this.targetWord === "") {
      this.targetWord = this.words[Math.floor(Math.random() * this.words.length)]
    }

    const nextWordIdx = (this.counter + 1) % this.words.length;
    this.counter = nextWordIdx;

    const nextWord = [...this.words[nextWordIdx]];
    const targetWord = [...this.targetWord];

    const length = Math.max(nextWord.length, targetWord.length)


    const marker = ["β–ˆ", "@", "β–“", "β–‘", "β– "]

    for (let i = 0; i < length; i++) {
      targetWord[i] = nextWord[i] || ' '
      this.targetWord = targetWord.slice(0, i).join('') + marker[Math.floor(Math.random() * marker.length)]+ targetWord.slice(Math.max(i-1, 0)).join('');
      await sleep();
    }
    this.targetWord = targetWord.join('');
    
    this.timeoutId = setTimeout(() => this.changeTargetWord(), 4000)
  }

  render () {
    return html`<span>${this.targetWord}</span>`
  }
}
customElements.define('text-changer', TextChanger);

class ExampleLocalCounterButton extends LitElement {
  static get styles () {
    return css`
    button {
      padding: 5px 10px;
    }
    button:hover {
      background-color: #ffd8fb;
    }
    `
  }

  static get properties() {
    return {
      counter: {type: Number, state: true},
      label: {type: String}
    }
  }

  constructor () {
    super()
    this.counter = 0;
    this.label = ""
  }

  autoBoundHandler () {
    this.counter++;
  }

  render () {
    return html`
      <button
        @click="${this.autoBoundHandler}"
      >
        ${this.label || "Increment me with local state"}: ${this.counter}
      </button>`
  }
}

customElements.define('local-counter-btn', ExampleLocalCounterButton);


// Inspired by https://glitch.com/edit/#!/polymer-custom-element?path=public%2Fhappy-thing.html%3A1%3A0
// Flips contents!
// Usage: <flip-me>⭐</flip-me>
class FlipMe extends LitElement {
  static get styles () {
    return css`
    :host {
      display: inline-block;
      cursor: pointer;
    }

    :host(:hover) {
      -webkit-animation: rotate-vert-center 4s both;
      animation: rotate-vert-center 4s both;
    }

    :host(:active) {
      -webkit-animation: rotate-vert-center 4s both;
      animation: rotate-vert-center 4s both;
    }

    /* ----------------------------------------------
    * Generated by Animista on 2021-5-8 22:43:42
    * Licensed under FreeBSD License.
    * See http://animista.net/license for more info. 
    * w: http://animista.net, t: @cssanimista
    * ---------------------------------------------- */

    /**
     * ----------------------------------------
     * animation rotate-vert-center
     * ----------------------------------------
     */
    @-webkit-keyframes rotate-vert-center {
      0% {
        -webkit-transform: rotateY(0);
                transform: rotateY(0);
      }
      100% {
        -webkit-transform: rotateY(360deg);
                transform: rotateY(360deg);
      }
    }
    @keyframes rotate-vert-center {
      0% {
        -webkit-transform: rotateY(0);
                transform: rotateY(0);
      }
      100% {
        -webkit-transform: rotateY(360deg);
                transform: rotateY(360deg);
      }
    }
    `
  }

  render () {
    return html`<slot></slot>`
  }
}
customElements.define('flip-me', FlipMe);

export class SingletonNumber {
  static _number = 0;
  
  get number () {
    return SingletonNumber._number
  }

  constructor (host) {
    this.host = host
  }

  increment () {
    SingletonNumber._number += 1;
    this.host.requestUpdate()
  }
}


class NumberView extends LitElement {
  static get properties() {
    return {
      counter: {type: Number},
    }
  }

  handleInc = () => { this.counter++; }

  connectedCallback () {
    super.connectedCallback()
    document.addEventListener('inc-counters', this.handleInc)
  }

  disconnectedCallback () {
    super.disconnectedCallback()
    document.removeEventListener('inc-counters', this.handleInc)
  }

  render () {
    return html`${this.counter || 0}`
  }
}

customElements.define('number-view', NumberView);



class ExampleStateCounterButton extends LitElement {
  static get styles () {
    return css`
    button {
      padding: 5px 10px;
    }
    button:hover {
      background-color: #ffd8fb;
    }
    `
  }

  autoBoundHandler () {
    const event = new CustomEvent('inc-counters');
    // We are using document as an event bus.
    // Feels like a bit of a hack.
    document.dispatchEvent(event);
    console.log('sent event!')
  }

  render () {
    return html`
      <button
        @click="${this.autoBoundHandler}"
      ><slot></slot></button>`
  }
}

customElements.define('state-counter-btn', ExampleStateCounterButton);

</script>
</body>