Although I do most of my writing in Notion, I have to put posts into Statamic to publish them on my website. For more CSS-based demonstration posts, I needed a better way to display the code and the result. Sometimes code snippets aren't enough to get the point across, or at least not as much as a good ol' live demo you can interact with. Markdown works wonderfully, well, until you need to inject some code demos or more dynamic stuff right in the middle.
Putting a bunch of HTML and CSS directly in markdown is nasty! There had to be a better way. The ideal "demo component" would:
A single component with the ability to toggle between two views — demo and source code
Only have to write the code (snippet) one time to accomplish both
This had to be something I could put within the main page's content
So how do we accomplish this in the context of a Statamic site? These approaches seemed like they could get the job done, albeit with varying levels of feel-good:
I did this for my post titled Tailwind Tidbit - Vertically Align Icons and Text in Buttons, but this time I have a lot more code samples I want to publish. This isn't ideal, and to get both code and demo views, I'd have to have two copies of each code snippet. No thanks!
Since Statamic v3 switched to commonmark, it's much easier to extend Statamic's markdown parser bits. This could work combined with a fancy blade component to get it done. The more I thought about this approach, the more I decided it was like using an excavator to dig a hole in your garden. I didn't need to dig that deep.
Darn it, wouldn't you know this was the last one I considered? Sometimes the best solutions take a little thinking before they come to you. I was so hellbent on using markdown that a Bard field seemed out of the question. This approach, however, is the ticket! It is simple to implement and gives you the most control. Unfortunately, it's not markdown. Even experienced programmers are allowed to use WYSIWYGs sometimes. 🤫
First, I needed to create a new Blueprint. I already had one called Post
, so the new one is called Code Post
. The structure is the same as the normal Post
, but the content is a bard field instead of a markdown field. Inside the bard field, I've defined an extra set, called html_code
. This is just a single field with the code
type. It looks like this:
1html_code:2 display: 'HTML Code'3 fields:4 -5 handle: code6 field:7 theme: material8 mode: htmlmixed9 indent_type: spaces10 indent_size: 411 key_map: default12 line_numbers: true13 line_wrapping: true14 type: code15 listable: hidden16 display: Code
1html_code:2 display: 'HTML Code'3 fields:4 -5 handle: code6 field:7 theme: material8 mode: htmlmixed9 indent_type: spaces10 indent_size: 411 key_map: default12 line_numbers: true13 line_wrapping: true14 type: code15 listable: hidden16 display: Code
This works well and allows you to put nice code blocks right inside the bard content. In my opinion, this is actually superior to markdown. Not only do you get a better preview of the post, but you have more control over the display in your Antlers template. Add a little AlpineJS to the mix, and we start to have some magical things happening in that template.
Here's an example of what that template (Statamic convention is to call it a set
— short for fieldset) looks like. Note that I've simplified it a little bit from the real one which has a bunch of utility classes for styling and whatnot.
1<div x-data="{ tab: 'result' }">2 <div class="tabs">3 <button @click="tab = 'result'">Result</button>4 <button @click="tab = 'code'">Code</button>5 <button @click="$refs.copyTarget.select(); document.execCommand('copy')">Copy to Clipboard</button>6 </div>7 <div class="demo">8 <div x-show="tab === 'result'">9 {{ code }}10 </div>11 <div x-show="tab === 'code'" x-cloak>12 <pre class="language-html"><code class="language-html">{{ code | entities }}</code></pre>13 </div>14 <textarea x-ref="copyTarget" x-cloak class="absolute opacity-0">{{ code }}</textarea>15 </div>16</div>
1<div x-data="{ tab: 'result' }">2 <div class="tabs">3 <button @click="tab = 'result'">Result</button>4 <button @click="tab = 'code'">Code</button>5 <button @click="$refs.copyTarget.select(); document.execCommand('copy')">Copy to Clipboard</button>6 </div>7 <div class="demo">8 <div x-show="tab === 'result'">9 {{ code }}10 </div>11 <div x-show="tab === 'code'" x-cloak>12 <pre class="language-html"><code class="language-html">{{ code | entities }}</code></pre>13 </div>14 <textarea x-ref="copyTarget" x-cloak class="absolute opacity-0">{{ code }}</textarea>15 </div>16</div>
Let's break this down. At the top, we declare an alpine component with a tab
variable. I wanted my demo to have two tabs, so we'll mimic that in the data. Next, we have the .tabs
div with our three buttons — Result
, Code
, and Copy to Clipboard
. The first two buttons have an Alpine @click
action that sets the current tab.
The third button is a little more complex, but it's not bad once you understand how copying something to the clipboard works in javascript. The gist is: you need to have a hidden textarea (but not too hidden — you can't use display: none
) to select. Then you can use javascript to select the text and run document.execCommand('copy')
to put the current selection into the clipboard. See that textarea at the bottom with the attribute x-ref="copyTarget"
? This is the Alpine way of adding a handle to a DOM element which we can access with the magic $refs
property. A little nicer than using document.querySelector()
all over the place!
The next section is straightforward, we echo out the code variable {{ code }}
to display the results. This is where I might encounter a problem with CSS specificity. It's possible that my main website styles could leak into this "Result" tab and screw up the demo display. Thankfully, most of my styling on this site are inline utility classes, so no problems yet. The source code tab has one interesting part — the use of the entities modifier. Since we want to display HTML source code, we have to encode the special characters so that the browser doesn't parse it as part of the document. It turns this <div>
into this <div>
. This is the trick to getting the source code to display. Putting this inside of the pre
and code
elements allows PrismJS to pick it up for syntax highlighting.
Now we've got a great way to convey code posts in more demo-friendly way! Here's an example of what the demo component looks like in action:
1<div class="w-full h-24 flex items-center justify-center">2 <div class="animate-bounce text-7xl">3 🤠4 </div>5</div>
1<div class="w-full h-24 flex items-center justify-center">2 <div class="animate-bounce text-7xl">3 🤠4 </div>5</div>