Adding Components to Eleventy with WebC
Originally: https://11tymeetup.dev/events/ep-11-webc-with-zach/
Components have been an often requested feature in Eleventy. While I do consider it an unanswered question whether or not components are the best starting point for new developers, it’s hard to argue with the efficiency and delivery gains to be had for folks with a bit more experience.
Now, we could wait around for the heavier bundler-based frameworks to circle back and pretend like React server components are a revolutionary new pattern for HTML without JavaScript (can you imagine). But if I might be allowed to offer a well-argued and intellectually-bulletproof counterpoint: naw.
A History of Component-like Things
Historically, Eleventy has offered a few component-like features. Before we move on, I think it’s worthwhile to go through the prior art here (for Eleventy specifically).
- Liquid Render
{% render %}
tag, the successor to the deprecated Liquid Include{% include %}
tag. - Nunjucks Include
{% include %}
tag - Nunjucks Macro (and Import)
- Eleventy Shortcodes (both single and paired)
- Eleventy Render Plugin
should we rebrand eleventy shortcodes to be eleventy server components or no
— Eleventy 🎈 (@eleven_ty) December 23, 2020
Shortcodes (and the Render plugin) were close to the developer experience folks expect from a component system, but they have their own drawbacks.
- Some template languages were good at handling asynchronous behavior by default (Liquid) and others required awkward delineation between asynchronous and synchronous behavior (Nunjucks).
- You can’t invoke other shortcodes from inside of a shortcode definition (the JS function in your Eleventy configuration file). You can nest shortcodes in paired shortcode content but that isn’t always enough.
Shortcodes are okay. We can do better.
The Age of Vue + Eleventy
Shipped in 2020 to support a netlify.com tech stack move to Eleventy, the Eleventy Vue SFC plugin was the first big foray into full first-party components for Eleventy. This used Vue single file components and rendered them to plain-’ol HTML (without shipping any client JavaScript).
Vue components are great! Here are a few things I loved:
- Single file component authoring nicely co-locates HTML, CSS, and (for this plugin at least) server-only JavaScript.
- Vue components felt more like authoring HTML (not JavaScript for everything).
- Vue merges
class
andstyle
attributes smartly between host components and component definitions. I know it’s a little thing but this felt like a huge authoring win when your component is HTML-aware (which shortcodes are not). - Using the
is
attribute to redefine components inline. - Using attribute bindings for scripting attribute values.
- Vue SFCs also offer CSS scoping built-in, although in practice I didn’t use it much! I liked to have more descriptive classes for components I build.
- The Eleventy Vue plugin also generated per page CSS bundles and brought first-class incremental builds to Eleventy Vue projects (not all template languages in Eleventy support incremental builds).
Maintenance Woes
The biggest drawback of this approach was that the Eleventy Vue plugin uses rollup-plugin-vue
which—perhaps obviously—is tightly coupled to the Rollup bundler! I’ve talked a bit about the long term risk of coupling to an official bundler in Eleventy, and that concern certainly played out with some prescient accuracy here.
Vue 3 was released (beta in April 2020, stable in September 2020) after the Eleventy Vue plugin and an official Vue 3 rollup-plugin-vue@6.0.0
was released in November 2020. Notably, this was happening at the same time as a rapid rise in popularity for Vite. Accordingly, the rollup-plugin-vue
repo is now archived and not maintained and folks are recommended to use Vite instead of the rollup plugin for Vue compilation.
Unfortunately, due to Vue’s upstream moves here, we’ll likely end up archiving eleventy-plugin-vue
too.
Web Components
Building netlify.com with server-rendered zero-bundle Vue components was a great experience but interestingly we didn’t ship any client-side Vue components on the site (during my tenure). For interactivity we were leaning on Web Components (mostly Custom Elements), which offered a very similar zero-overhead mentality in the client-side world.
When you talk about web components publicly, it’s almost certain that you’ll get a helpful link to a 2019 blog post from Rich Harris (the creator of Svelte) titled Why I don’t use web components. The post has some valid criticism (though it leans kinda hard into complaints about Shadow DOM, that I quickly realized was an optional feature and conveniently ignored it 😅 for technical reasons that are outside the scope of this post).
But Rich’s top issue in the post is Progressive Enhancement. Yes, lots to agree on there. Rich starts his argument with the point that he can write a Svelte component that spits out server-rendered HTML like this:
<a target="_blank" noreferrer href="…" class="svelte-1jnfxx">
Tweet this
</a>
…and in this Rich compares Svelte’s server-rendered HTML to a client-rendered Web Component:
<twitter-share text="…" url="…" via="…"/>
Apples to oranges aside, this is a good distillation of the expectations mismatch for a lot of folks. If you’re coming over from a framework component background, you likely expect first-class server rendering for Web Components to be solved by the platform.
To put it succinctly, developers want to write <twitter-share text="…" url="…" via="…">
and have browsers serve <a target="_blank" noreferrer href="…">Tweet this</a>
without a build or compile step.
Well, you can’t. But JavaScript component frameworks can’t do this either.
Going back to Rich’s example, I would say that the ideal client-rendered web component markup would look something like this:
<twitter-share>
<a href="…">Tweet this</a>
</twitter-share>
…and the component might transform the markup to add the component attributes as needed, maybe to render something like this:
<twitter-share>
<a href="…" target="_blank" noreferrer>Tweet this</a>
</twitter-share>
Notably, any time you modify the DOM with JavaScript it creates a tension point for progressive enhancement. Is it okay that before-JS or broken-JS users won’t have those attributes added to the link? Maybe for this use case. But for more complex components, these tradeoff decisions cannot be made universally. They must be made by the web authors with the full context of the use case for the project they are working on.
(Side note that this is why component-based design systems have a hard time—components don’t have the individual use-case specific context necessary to make judgement calls and can only advise on how they might be used in more broader, generic use cases)
It isn’t a level playing field to compare server-rendered framework components to client-rendered web components but it does highlight the need for additional Web Component tooling to deliver the maximum amount of code re-use with the minimum amount of tradeoff. Let’s add a build/compile/server render step for Web Components—my biased suggestion is to use WebC!
The Age of WebC + Eleventy
WebC is a framework-independent standalone HTML compiler for generating markup for web components.
When creating WebC, I wanted to bring all of the good things from Vue’s single file components to Web Components: HTML-first single file component authoring, class
and style
attribute merging, webc:is
(instead of is
to avoid attribute collisions), dynamic attributes using the :
prefix, scoped CSS, per-page CSS and JS bundles, and fully incremental builds.
On top of those, WebC offers full access to the Data Cascade inside of your components, is 100% async friendly, designed as a server-first tool (rather than a client-first retrofit), offers zero-overhead client interactivity (requires no library code), is streaming friendly, is not coupled to a bundler and includes some bundler functionality built-in, and is extensible with other template language syntax (Markdown, Nunjucks, Liquid, Sass, etc).
Here are a few links to get started with WebC:
- Eleventy’s WebC documentation (this is the best place to start for folks using Eleventy)
- Crash Course in Eleventy’s new WebC Plugin (5m14s video)
- Interactive Progressively-enhanced Web Components with WebC (9m23s video)
- As this blog post was originally a talk given at the Eleventy Meetup, when the video is published I will include a deep link to the demo portion of WebC here which built on the above Interactive Progressively-enhanced… video and featured some larger points around tradeoffs between server-side rendering and progressive enhancement (mostly surrounding content layout shift). I imagine that we’ll likely continue to have those discussions moving forward!
What’s Next for WebC?
- More integrations! I loved to see an Express plugin for WebC from Nick Colley and a Vite plugin for WebC from @mayank99
- HTML bucketing! We already have CSS/JS asset aggregation. I’d like to see this with arbitrary HTML too. Think of a single WebC component file for a web font that includes both the
@font-face
CSS and the preload HTML together! Or a WebC icon that only includes a single<g>
that aggregates up to a reusable de-duplicated SVG icon set. - Writing asset bundles to files (now we’re really getting into bundler-functionality)
- Aliases for
node_modules
so you can import easily from npm! Folks can publish a webc file for re-use in any other project (that also includes the HTML, JS, and CSS) to use directly. - More sugar for loops/conditional rendering. Though this is possible using template syntax or JavaScript render functions now, it could be less verbose!
- Tighter integration with
<is-land>
: I’d like to see us get to the point where WebC components will be able to declare assets to load conditionally based on<is-land>
loading conditions. Super granular control and power!
The best way to keep up to date here is to subscribe to our YouTube channel or follow us on Twitter @eleven_ty and/or @webc_omponents!
Thanks, y’all!
17 Comments
@raymondcamden
Am I right in seeing that if I want to use a WC in a Liquid/MD/etc file, I have to wrap with the render plugin? That's the only part that seems kinda annoying - a lot of extra typing. I can use a .webc file as the top doc, but I want the power of Liquid too :)
@zachleat
A few options: 1. You can use Liquid inside of WebC 11ty.dev/docs/languages… 2. You can use postprocess using the transform 11ty.dev/docs/languages… 3. You can rename the tags in the render plugin (view full options): 11ty.dev/docs/plugins/r…
@raymondcamden
Kinda makes sense. It feels a bit weird that I may make a .webc file just so *that* file can use N other .webc files that really my components, where the first is just... the page. Does that make sense? Do I just need to get over it?
@zachleat
Not sure I follow, no 😅 To use a Liquid include you need to use Liquid. To use a Nunjucks macro you need to use Nunjucks. Same for WebC?
@raymondcamden
Yeah I get that - but it's like, I have page /foo. It isn't a web component. But it's going to use some. So I need to make /foo.webc. Meh, it will feel natural once I try once or twice (got an idea for a simple demo).
@zachleat
Just to be super clear, it isn’t a hard requirement. I converted zachleat.com to use WebC using the transform method so that I could just keep all of my existing liquid templates and sprinkle WebC components in there 11ty.dev/docs/languages…
@Dominus_Kelvin
Awesome. Hey Zach I’d love to have you on a TKYT session to learn @eleven_ty Here is a previous session. Let me know what you think youtu.be/j9rn2180mNI
@zachleat
Let’s do itttttt how do I schedule
@Dominus_Kelvin
Yay. I’ll DM you a link to a casual call 📞
@Dominus_Kelvin
DM sent
@chrisshank23
``` <a href="…" is=“twitter-share”>Tweet this</a> ``` As a side note, here is another way to progressively enhance with a custom element! Since the custom element extends the anchor element it’s arguable a cleaner way to implement this.
@zachleat
Yeah, I don’t predict that one will survive the trial by standards fire that is Safari.
@zachleat
I think the most acute pain I’ve felt directly from this, I documented a bit here: zachleat.com/web/webc-in-el…
@eleven_ty
why the 🙄 sounds fine to me though I would note zachleat.com/web/webc-in-el…
Matt Wilcox
@zachleat I actually find that slower pace a feature not a bug, is the thing. I *trust* PHP much more than I do Node. I *trust* Composer more than I do npm.Is that rational? No and yes. I'm sure there are shoddy packages and all the same vectors for bad actors in Composer pac… Truncated
Matt Wilcox
@zachleat (Which is why I'm happy playing with 11ty locally, but do not use node on live servers) @khalidabuhakmeh @adactio.com
Khalid ⚡
@mattwilcox @zachleat ???? Yeah, I wouldn't dream of it. Any viable use of a Web Component framework or SSR with an SSR stack (Laravel and ASP.NET Core) would have to be a build step.