For recent journal entries about SVG clipping masks and animating clipping masks I wanted to render actual code examples in the article to show the reader what the end result of the tutorial would look like. Since I use the Tailwind CSS JIT compiler my website ships with a CSS file that contains only the classes that are actually being used in my template files. That seems very restricting as I would probably always use some unavailable class in my examples. So I came up with a solution that uses the Tailwind Play CDN to live parse the code snippet and return the CSS needed. This is an example of what we're building.
Since the Tailwind CDN comes from a non EU server, personal information (IP-address and maybe more) will transfer outside of the EU. To respect the GDPR we need the users permission for this. On this website I've put this information in my privacy statement and decided to ask for permission on a per-session basis. Let's get started.
The Bard set
My Statamic Starter Kit Peak uses Bard as the editor for long form content like this very article. Within it I have various sets like a figure, a code block and a table. If you don't use Peak you have to do the following manually. To quickly scaffold a new set in Peak however, simply run php please peak:add-set
in your terminal and type in a set_name
. This does three things:
It generates
resources/fieldsets/set_name.yaml
where we can define our fields for this set. By default it also contains a sizing utility so in the Control Panel we can size the block normal, LG or XL.It generates
resources/views/components/_set_name.antlers.html
as a template for this set. The set will respect the sizing set in the control panel.It updates
resources/fieldsets/article.yaml
to include the generated set.
After running this command the Article Page Builder Block will contain the set. I named it HTML
.
By default the generated fieldset looks like this.
1title: HTML2fields:3 -4 handle: size5 field: common.size6 config:7 instructions: 'The size in which the HTML should be displayed.'
The generated fieldset YAML file.
We need to add a code
field to be able to input any code that we want rendered on the frontend. The fieldset now looks like this.
1title: HTML 2fields: 3 - 4 handle: size 5 field: common.size 6 config: 7 instructions: 'The size in which the HTML should be displayed.' 8 - 9 handle: html10 field:11 theme: material12 mode: htmlmixed13 mode_selectable: false14 indent_type: spaces15 indent_size: 216 key_map: default17 line_numbers: true18 line_wrapping: true19 display: Code20 type: code21 icon: code22 localizable: true23 listable: hidden24 instructions: 'Uses the Play CDN and JIT.'25 instructions_position: below26 read_only: false27 validate:28 - required
The complete HTML fieldset YAML.
That fieldset will end up looking like this when you're working with the Bard field in the Statamic Control Panel.
The template logic
To wire this all up we need to edit the generated template file. The following example is what Peak generates by default. It includes IDE hinting for the VS Code Antlers Toolbox, HTML comments to make it easier to debug your templates and the Peak sizing utilities that give the editor a choice how big the component should be rendered. Don't worry about this though. It's a specific thing to Peak and not important to what we're building in this article.
1{{# 2 @name HTML 3 @desc The HTML component. 4 @set page.article.html 5#}} 6 7<!-- /components/_html.antlers.html --> 8<div 9 class="10 not-prose11 {{ switch(12 (size == 'md') => 'size-md',13 (size == 'lg') => 'size-lg',14 (size == 'xl') => 'size-xl',15 () => 'size-md'16 )}}17 "18>19 <h2>🔧<br>HTML</h2>20</div>21<!-- End: /components/_html.antlers.html -->
The bare bones HTML component template file.
The first thing we need to add is a bit Alpine logic. Since the user should give permission for using the Tailwind CDN I create a persisted store containing the permission. I'm using an Alpine store called twCDNConsent
so a user only has to give permission once should a page contain multiple components. This is how it looks.
1{{ once }} 2 <script> 3 document.addEventListener('alpine:init', () => { 4 Alpine.store('twCDNConsent', { 5 value: Alpine.$persist(false).as('twCDNConsent').using(sessionStorage), 6 set(value) { 7 this.value = value 8 } 9 })10 })11 </script>12{{ /once }}
The final template to render a code example.
Using a store makes all components reactive. This means a user only has to accept once per session and then all components will render. I use Alpine Persist to store this consent in the browsers sessionStorage
. This storage value is also called twCDNConsent
. The store gets a set()
method to update the store value. If you omit .using(sessionStorage)
Alpine will save it in the localStorage
and the preference would persist across browser sessions. You might want to provide a way for users to reset their settings if you choose to got that route.
This store logic is wrapped in an Antlers tag as it only has to be defined once
should your page contain multiple of those HTML components.
Now let's setup the HTML for this component. I'm keeping the styling in this example bare bones so it's easier to grasp.
1<div 2 x-data="html('{{ html | add_slashes | entities | collapse_whitespace }}')" 3 x-bind="root" 4 x-cloak 5> 6 <div x-bind="result"> 7 </div> 8 9 <div x-bind="consent" class="w-full h-44 p-6 flex flex-col justify-center items-center space-y-4">10 <button x-bind="button" class="uppercase tracking-wider">11 Render code12 </button>13 14 <span>15 I agree that my data is being collected and stored.16 </span>17 </div>18</div>
The basic HTML structure of the component.
As you can see I extract the Alpine logic out of the component to get some syntax highlighting. For our x-data
we call the html()
function and pass in the escaped HTML from the code
field for this very set. This way we only have to define our Javascript once and still use multiple of the same components on one page by passing in the unique HTML for each set.
In addition to that I've also setup four elements we can bind to:
root
: the root element.result
: the div that should get the actual rendered HTML and should be hidden when no consent is given.consent
: the consent wrapper that should be hidden when consent is given.button
: the actual button a user has to click to give consent.
Let's setup the the Alpine logic now.
1document.addEventListener('alpine:initializing', () => { 2 Alpine.data('html', (html) => { 3 return { 4 html: '' , 5 root: { 6 ['x-effect']() { 7 this.$store.twCDNConsent.value 8 && (this.html = html) 9 && this.loadTailwind()10 }11 },12 consent: {13 ['x-show']() {14 return !this.$store.twCDNConsent.value15 }16 },17 button: {18 ['@click']() {19 this.$store.twCDNConsent.set(true)20 }21 },22 result: {23 ['x-html']() {24 return this.html25 }26 },27 loadTailwind() {28 let tailwind = document.createElement('script')29 tailwind.src = 'https://cdn.tailwindcss.com'30 this.$root.appendChild(tailwind)31 let config = document.createElement('script')32 config.textContent = 'tailwind.config={corePlugins:{preflight:false}}'33 tailwind.onload = () => { this.$root.appendChild(config) }34 },35 }36 })37})
The actual Alpine logic.
Lets go through this code step by step:
First we set an
html
property that will later contain the HTML that you entered in thecode
field in your fieldset.We're binding the
x-effect
property to theroot
element. In this we use short circuit operators that do the following. If consent has been given, set thehtml
property with thehtml
that we passed in this function. If that is successful run theloadTailwind()
function.Our
consent
element should only show when consent has not yet been given. We reach into our previously created$store.twCDNConsent
and get its value with.value
.When the
button
is clicked consent should be set to true. For this we use theset()
method we added to the store.The
result
part should add thehtml
property as its inner HTML. Since the HTML on this site always comes from me I trust the contents.Inside the function we create a
script
tag and set the source to the Tailwind CDN. This tag can be appended to theroot
element.We then create another
script
tag containing the configuration for the Tailwind CDN. We setpreflight: false
. This makes sure the returned CSS doesn't contain any reset CSS that could conflict with the styles for the rest of the site. It could overwrite your default font family for example. This config will be appended as soon as the Tailwind CDN script is loaded.
The final component should look something like this:
1{{# 2 @name HTML 3 @desc The HTML component. 4 @set page.article.html 5#}} 6 7<!-- /components/_html.antlers.html --> 8<div 9 class="10 not-prose11 my-412 {{ switch(13 (size == 'md') => 'size-md',14 (size == 'lg') => 'size-lg',15 (size == 'xl') => 'size-xl',16 () => 'size-md'17 )}}18 "19>20 <div21 x-data="html('{{ html | add_slashes | entities | collapse_whitespace }}')"22 x-bind="root"23 x-cloak24 >25 <div x-bind="result">26 </div>27 28 <div x-bind="consent" class="w-full h-44 p-6 flex flex-col justify-center items-center space-y-4">29 <button x-bind="button" class="uppercase tracking-wider">30 Render code31 </button>32 33 <span>34 I agree that my data is being collected and stored.35 </span>36 </div>37 </div>38 39 {{ once }}40 <script>41 document.addEventListener('alpine:init', () => {42 Alpine.store('twCDNConsent', {43 value: Alpine.$persist(false).as('twCDNConsent').using(sessionStorage),44 set(value) {45 this.value = value46 }47 })48 })49 50 document.addEventListener('alpine:initializing', () => {51 Alpine.data('html', (html) => {52 return {53 html: '' ,54 root: {55 ['x-effect']() {56 this.$store.twCDNConsent.value57 && (this.html = html)58 && this.loadTailwind()59 }60 },61 consent: {62 ['x-show']() {63 return !this.$store.twCDNConsent.value64 }65 },66 button: {67 ['@click']() {68 this.$store.twCDNConsent.set(true)69 }70 },71 result: {72 ['x-html']() {73 return this.html74 }75 },76 loadTailwind() {77 let tailwind = document.createElement('script')78 tailwind.src = 'https://cdn.tailwindcss.com'79 this.$root.appendChild(tailwind)80 let config = document.createElement('script')81 config.textContent = 'tailwind.config={corePlugins:{preflight:false}}'82 tailwind.onload = () => { this.$root.appendChild(config) }83 }84 }85 })86 })87 </script>88 {{ /once }}89</div>90<!-- End: /components/_html.antlers.html -->
The final component.
And that will give us a component we can use in our tutorials. Add some code and have it rendered on the frontend using the Tailwind CDN. In order for the styles to load the user has to give consent and the Tailwind CDN will only return styles actually used so the request stays as small as possible.
Did you like this article? Let me know.