<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Martin Hicks</title>
    <description>The latest articles on DEV Community by Martin Hicks (@martinhicks).</description>
    <link>https://dev.to/martinhicks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg</url>
      <title>DEV Community: Martin Hicks</title>
      <link>https://dev.to/martinhicks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martinhicks"/>
    <language>en</language>
    <item>
      <title>Introducing Dynoxide: a fast, embeddable DynamoDB engine</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Thu, 02 Apr 2026 21:02:51 +0000</pubDate>
      <link>https://dev.to/martinhicks/introducing-dynoxide-a-fast-embeddable-dynamodb-engine-15ni</link>
      <guid>https://dev.to/martinhicks/introducing-dynoxide-a-fast-embeddable-dynamodb-engine-15ni</guid>
      <description>&lt;p&gt;I've been working with DynamoDB for the best part of a decade. It's my default database for most things I build at &lt;a href="https://sinovi.uk" rel="noopener noreferrer"&gt;Si Novi&lt;/a&gt;, and I genuinely like it. The data modelling is satisfying once it clicks, the operational overhead is basically zero, and it scales without you having to think about it.&lt;/p&gt;

&lt;p&gt;Last year, Si Novi started building &lt;a href="https://nubo.sinovi.uk" rel="noopener noreferrer"&gt;Nubo&lt;/a&gt;, a native DynamoDB client for macOS and iPadOS, with Windows and Linux on the way. One of the features we wanted from the start was a built-in sandbox. A local DynamoDB instance running on-device that you could experiment with, no AWS credentials needed. Create tables, insert data, test access patterns. All offline. All on your machine. Maybe even on an iPad.&lt;/p&gt;

&lt;p&gt;The problem was: how do you ship that?&lt;/p&gt;

&lt;p&gt;DynamoDB Local's &lt;a href="https://aws.amazon.com/dynamodb/dynamodblocallicense/" rel="noopener noreferrer"&gt;licence&lt;/a&gt; explicitly prohibits redistribution. You can't bundle it inside another application or sublicence it. Even setting the licence aside, the developer experience would have been awful. Asking users to separately download and install a Java-based emulator, with Docker or a JVM as a prerequisite, just to use one feature in your app? That's not a sandbox. That's homework.&lt;/p&gt;

&lt;p&gt;And on iPadOS there's no path at all. No JVM. No Docker. Nothing.&lt;/p&gt;

&lt;p&gt;So I started building something new. A DynamoDB-compatible engine in Rust, backed by SQLite, that compiles to a single native binary. Something I could embed directly into Nubo as a library.&lt;/p&gt;

&lt;p&gt;That's how Dynoxide started.&lt;/p&gt;

&lt;h2&gt;
  
  
  From library to tool
&lt;/h2&gt;

&lt;p&gt;The first version did exactly what I needed: a local DynamoDB that Nubo could bundle as its sandbox. Job done, in theory. But the thing that surprised me was just how small and fast it turned out to be.&lt;/p&gt;

&lt;p&gt;A ~3 MB download (~6 MB on disk). Under 5 MB of RAM at idle. Cold startup in about 15 milliseconds, compared to over two seconds for DynamoDB Local. Numbers like that change what's practical. Instead of running one shared DynamoDB instance in your CI pipeline, you could spin up a fresh one per test. Isolated state. No cleanup step. No flaky tests from leftover data. Tear it down when you're done and the overhead is essentially nothing.&lt;/p&gt;

&lt;p&gt;That realisation shifted what Dynoxide was becoming. It wasn't just infrastructure for Nubo any more. It was a tool in its own right.&lt;/p&gt;

&lt;p&gt;The same properties that made it good for CI made it a natural fit for agentic development. A coding agent that needs to create tables, write data, or test queries can do all of that through Dynoxide's built-in MCP server. 34 tools, no setup required. Run &lt;code&gt;dynoxide mcp&lt;/code&gt; and your agent has a full DynamoDB environment to work with.&lt;/p&gt;

&lt;p&gt;And then I looked at how many Node.js projects still depend on &lt;a href="https://github.com/mhart/dynalite" rel="noopener noreferrer"&gt;dynalite&lt;/a&gt; for local DynamoDB. I owe a lot to dynalite. Using it through &lt;a href="https://arc.codes" rel="noopener noreferrer"&gt;arc.codes&lt;/a&gt; and Architect is what turned DynamoDB from a database I'd reach for occasionally into the one I'd reach for first. Having a fast local emulator that just worked changed how I thought about building with DynamoDB. It made the feedback loop tight enough that I actually wanted to experiment. But dynalite hasn't seen updates in a while and newer DynamoDB features like transactions and streams aren't covered. Dynoxide supports both, along with the rest of the API surface. With an npm package shipping platform-specific binaries (the same approach esbuild and Biome use), it works as a drop-in replacement. Same workflow, one line change in your &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Dynoxide implements the DynamoDB API as a translation layer in Rust, storing data in SQLite and compiling to a native binary with no runtime dependencies. DynamoDB Local takes the same general approach (SQLite as the storage engine is &lt;a href="https://dev.to/aws-heroes/dynamodb-local-in-docker-25i"&gt;well documented&lt;/a&gt;) but ships as a Java application with a JVM dependency and a restrictive licence.&lt;/p&gt;

&lt;p&gt;Because SQLite is an embedded database, Dynoxide doesn't have to run as a standalone server. It can be linked directly into another application, which is exactly how Nubo uses it. Encrypt the database file with SQLCipher and you've got a portable, encrypted, DynamoDB-compatible data store. Nubo uses this for its local cache: data you've previously accessed from AWS is kept in an encrypted database on-device, so it loads instantly and works offline.&lt;/p&gt;

&lt;p&gt;To make sure Dynoxide actually behaves like the real thing, I built a &lt;a href="https://github.com/nubo-db/dynamodb-conformance" rel="noopener noreferrer"&gt;conformance test suite&lt;/a&gt;. 526 tests, all validated against real DynamoDB on AWS. Every emulator gets the same suite. Dynoxide passes all 526. DynamoDB Local manages 92%. LocalStack gets 93%. Dynalite hits 81%.&lt;/p&gt;

&lt;p&gt;The suite is public. Anyone can run it and verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;nubo-db/tap/dynoxide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm (drop-in dynalite replacement):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; dynoxide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cargo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;dynoxide-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dynoxide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation is at &lt;a href="https://dynoxide.dev" rel="noopener noreferrer"&gt;dynoxide.dev&lt;/a&gt;, and the source is on &lt;a href="https://github.com/nubo-db/dynoxide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Dynoxide is the foundation. But the thing I'm building on top of it is what I really want to talk about next. &lt;a href="https://nubo.sinovi.uk" rel="noopener noreferrer"&gt;Nubo&lt;/a&gt; is a native DynamoDB client for macOS and iPadOS, and I think people who work with DynamoDB day to day will love it. If that sounds interesting, there's a mailing list at &lt;a href="https://nubo.sinovi.uk" rel="noopener noreferrer"&gt;nubo.sinovi.uk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Dynoxide stands on its own though. If you're running DynamoDB Local in CI and wondering why your pipeline is slow, or you're still using dynalite and need coverage for newer API features like transactions and streams, give it a try. I'd love to know what you think.&lt;/p&gt;

&lt;p&gt;Si Novi will be at AWS Summit London on April 22nd. If you're there and want to talk DynamoDB, come and find me.&lt;/p&gt;

</description>
      <category>dynamodb</category>
      <category>rust</category>
      <category>aws</category>
    </item>
    <item>
      <title>Using Whisper and FFmpeg to build a fully automated YouTube Shorts pipeline</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Sat, 21 Mar 2026 22:13:53 +0000</pubDate>
      <link>https://dev.to/martinhicks/automating-my-sons-youtube-with-python-and-ffmpeg-3a48</link>
      <guid>https://dev.to/martinhicks/automating-my-sons-youtube-with-python-and-ffmpeg-3a48</guid>
      <description>&lt;p&gt;My son Ben is 12. Back in August he decided he wanted to start a YouTube channel. We talked about what it could be and he landed on Premier League score predictions. Every gameweek he'd predict the scores for all 10 matches, record himself talking through his picks, then after the weekend we'd see how he did and record a review.&lt;/p&gt;

&lt;p&gt;Simple concept. He was into it. I was into it. Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The manual era
&lt;/h2&gt;

&lt;p&gt;The first video was recorded in the car on the drive home from France. I'd knocked together a Keynote presentation on my iPad with the fixtures for Gameweek 1. Team names, badges, blank spaces for Ben to write his predictions with Apple Pencil while screen recording. It was rough, but it worked. He recorded his picks, we uploaded it, and &lt;a href="https://www.youtube.com/@BensPLPredictions" rel="noopener noreferrer"&gt;Ben's PL Predictions&lt;/a&gt; was born.&lt;/p&gt;

&lt;p&gt;The workflow went like this: I'd create the Keynote slides manually, Ben would screen record on the iPad, I'd trim the video in iMovie, adjust the audio levels and brightness, then upload to YouTube Studio and fill in the title, description, and tags by hand.&lt;/p&gt;

&lt;p&gt;For the review video I'd do the same thing again, but this time with actual results filled in and a little thumbnail showing what he'd predicted, so viewers could see both side by side as he talked through each result.&lt;/p&gt;

&lt;p&gt;It was taking me about an hour on a Friday evening or Saturday morning. Not terrible, but I could feel the fragility of it. Ben was being consistent, recording every week without fail, and I didn't want &lt;em&gt;me&lt;/em&gt; to be the reason he couldn't get a video out. If I was busy, or forgot, or just didn't fancy spending an hour creating slides and editing video, his streak would break. That didn't sit right.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON and Python: the first automation
&lt;/h2&gt;

&lt;p&gt;After a couple of weeks I rebuilt the system. Instead of manually creating Keynote slides, I set up a JSON data model for each gameweek (fixtures, predictions, results, points) and wrote a Python CLI using Click that generates PowerPoint presentations from the data. The slides get copied to iCloud so they appear on the iPad automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"league"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"season"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-26"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gameweek"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ars"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"away"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"prediction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"away"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"away"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scoring is simple: three points for an exact score, one point for the correct result (home win, away win, or draw), zero for wrong. The CLI generates prediction slides with blank scores for Ben to write on, and review slides with actual results, points, and a thumbnail of his predictions.&lt;/p&gt;

&lt;p&gt;This got my weekly time down from an hour to about thirty minutes. Create the JSON with fixtures, run the command, copy to iCloud. After matches, add results, run the review command. Better, but I was still manually typing fixtures, manually watching his video to work out what he'd predicted, manually looking up results, manually editing in iMovie, and manually uploading to YouTube. Five of those steps are things a computer should be doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating everything else
&lt;/h2&gt;

&lt;p&gt;I've just finished building the next phase, removing every remaining manual step. It's ready to go from the Gameweek 31 review and Gameweek 32 onwards. Here's what the pipeline looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixtures from the FPL API
&lt;/h3&gt;

&lt;p&gt;No more manual data entry. The Fantasy Premier League website exposes an undocumented API that &lt;a href="https://github.com/topics/fantasy-premier-league" rel="noopener noreferrer"&gt;the community&lt;/a&gt; has been using for years. It provides fixture data per gameweek, free, no authentication required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python cli.py fetch-fixtures &lt;span class="nt"&gt;-w&lt;/span&gt; 31
Fetched 8 fixtures &lt;span class="k"&gt;for &lt;/span&gt;GW31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI maps FPL's numeric team IDs to our registry, handles postponed matches and writes the gameweek JSON automatically. It's worth noting this is an unofficial API with no formal terms of use for developers. For a personal project making a handful of calls per week, the risk is negligible, but it's not something I'd build a commercial product on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Video transcription with Whisper
&lt;/h3&gt;

&lt;p&gt;This is the one that surprised me most. After Ben records his prediction video, OpenAI's &lt;a href="https://github.com/openai/whisper" rel="noopener noreferrer"&gt;Whisper&lt;/a&gt; runs locally to transcribe the audio, then a regex parser extracts his predicted scores by matching them to fixtures by team name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python cli.py transcribe &lt;span class="nt"&gt;-w&lt;/span&gt; 31
 1 | Bournemouth  | Man United  | 1 - 1  | matched
 2 | Brighton     | Liverpool   | 2 - 1  | matched
 ...
Matched 8/8 fixtures
Save these predictions to the gameweek JSON? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It handles colloquial names like "Spurs," "Palace," and "Forest", and works out which score belongs to home and away based on context. I run the Whisper &lt;code&gt;medium&lt;/code&gt; model with an initial prompt containing all 20 Premier League team names, which dramatically improves recognition accuracy. I tested it against Ben's existing recordings and it matched all predictions correctly: 10/10 for GW30 and 8/8 for GW31.&lt;/p&gt;

&lt;p&gt;It asks for confirmation before writing to the JSON, so there's always a human check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results from the API
&lt;/h3&gt;

&lt;p&gt;Same FPL endpoint, different data. After the weekend's matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python cli.py fetch-results &lt;span class="nt"&gt;-w&lt;/span&gt; 31
Updated 8 matches with results
Total points: 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merges results into the existing JSON, preserving Ben's predictions, and calculates points automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encoding publish-ready Shorts
&lt;/h3&gt;

&lt;p&gt;This replaces iMovie entirely. A single command takes the raw iPad screen recording and produces a YouTube-optimised Short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python cli.py encode-short &lt;span class="nt"&gt;-w&lt;/span&gt; 31 &lt;span class="nt"&gt;-t&lt;/span&gt; predict &lt;span class="nt"&gt;-i&lt;/span&gt; RPReplay_Final.MP4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood it runs a single Whisper pass (reused for both trim detection and SRT caption generation), detects the speech start via FFmpeg's &lt;code&gt;silencedetect&lt;/code&gt; filter, finds the closing phrase ("see you next time") in the transcript, then encodes with normalised audio, proportional scaling and YouTube-recommended H.264 settings.&lt;/p&gt;

&lt;p&gt;The trim is tight, half a second after the closing phrase, so it cuts before Ben swipes up to stop the screen recording and you don't see the iOS Control Centre.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload to YouTube
&lt;/h3&gt;

&lt;p&gt;The final piece. One command uploads the encoded Short as a private video with auto-generated metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python cli.py upload-short &lt;span class="nt"&gt;-w&lt;/span&gt; 31 &lt;span class="nt"&gt;-t&lt;/span&gt; predict
Title: GW31 Premier League Predictions &lt;span class="c"&gt;#PremierLeague #matchweek31&lt;/span&gt;
Tags: Premier League, EPL, GW31, Football...
Thumbnail &lt;span class="nb"&gt;set
&lt;/span&gt;Captions uploaded
Privacy: PRIVATE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It generates the title, description (including season accuracy stats and an engagement question), tags with relevant team names, extracts and sets a thumbnail from the title frame, and uploads the SRT captions for search indexing.&lt;/p&gt;

&lt;p&gt;Everything uploads as &lt;em&gt;private&lt;/em&gt;. Ben and I review it in YouTube Studio and he publishes when he's happy. This was a deliberate decision. It's a child's channel and I wanted a human gate before anything goes public.&lt;/p&gt;

&lt;p&gt;The YouTube API OAuth credentials are pulled from 1Password at runtime via the &lt;code&gt;op&lt;/code&gt; CLI, so nothing sensitive sits on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weekly routine from Gameweek 32
&lt;/h2&gt;

&lt;p&gt;What used to be nine manual steps should now be a handful of CLI commands plus Ben's two recordings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;fetch-fixtures&lt;/code&gt; then &lt;code&gt;pptx-predict-table --icloud&lt;/code&gt; - pull fixtures, generate slides, send to iPad&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Ben records prediction video&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transcribe&lt;/code&gt; then &lt;code&gt;encode-short&lt;/code&gt; then &lt;code&gt;upload-short&lt;/code&gt; - process and publish&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch-results&lt;/code&gt; then &lt;code&gt;pptx-review-table --icloud&lt;/code&gt; - pull results, generate review slides&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Ben records review video&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encode-short&lt;/code&gt; then &lt;code&gt;upload-short&lt;/code&gt; - process and publish&lt;/li&gt;
&lt;li&gt;Review and publish manually in YouTube Studio&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm looking forward to running it for the review once GW31's fixtures are all played, and both predictions and review for real in Gameweek 32 after the international break. If the numbers hold, my active time should go from an hour to under two minutes. And critically, none of those two minutes are things that block Ben. If I'm not around, someone else could run four commands, or I could do it from my phone over SSH. The next step is getting this set up on Ben's Chromebook so he can run the pipeline himself — once I work out why the option to enable Linux is disabled on his machine, he won't need me involved at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm proud of
&lt;/h2&gt;

&lt;p&gt;It's not the code. It's that Ben has stuck with it. He's at &lt;a href="https://www.youtube.com/@BensPLPredictions" rel="noopener noreferrer"&gt;over a hundred subscribers&lt;/a&gt;, he's recorded every single gameweek this season, and he genuinely looks forward to it. He's learning that consistency matters more than virality, that showing up every week builds something, and that over a hundred people choosing to follow a 12-year-old's football predictions is pretty cool.&lt;/p&gt;

&lt;p&gt;The automation exists to protect that consistency. Not to make the videos for him. He still records every one himself, writes his predictions by hand with Apple Pencil, and talks through his reasoning. The personality is all him. I just make sure the boring infrastructure never gets in the way.&lt;/p&gt;

&lt;p&gt;If you're into Premier League predictions, or you want to support a kid who's been showing up every week, you can find Ben at &lt;a href="https://www.youtube.com/@BensPLPredictions" rel="noopener noreferrer"&gt;youtube.com/@BensPLPredictions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>python</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Recently Played: bringing back my Last.fm component</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Sun, 08 Mar 2026 23:21:01 +0000</pubDate>
      <link>https://dev.to/martinhicks/recently-played-bringing-back-my-lastfm-component-2ank</link>
      <guid>https://dev.to/martinhicks/recently-played-bringing-back-my-lastfm-component-2ank</guid>
      <description>&lt;p&gt;Around 2010, my personal website had a Last.fm widget showing what I'd been listening to. It was a small thing - just a few album covers and track names in the sidebar - but it was very &lt;em&gt;me&lt;/em&gt;. Back then your personal site was an extension of yourself, and having your music taste ticking away in the corner felt right.&lt;/p&gt;

&lt;p&gt;Fast forward fifteen years and I've rebuilt &lt;a href="https://martinhicks.dev" rel="noopener noreferrer"&gt;this site&lt;/a&gt; from scratch several times over. The Last.fm widget never made the cut again. Not for any good reason - it just fell off the list each time. When I was recently working on the sidebar I remembered it, and thought &lt;em&gt;why not bring it back?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's something I like about the full circle. The web has gone through its various phases since 2010 - the single page app years, the JavaScript-for-everything years - and come out the other side valuing progressive enhancement and server-rendered HTML again. My site is built with &lt;a href="https://www.11ty.dev" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt; and deployed from GitHub Actions to S3 with CloudFront. It felt fitting to bring back a feature from an earlier era, built with the principles I care about now.&lt;/p&gt;

&lt;p&gt;So here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The component is a two-layer system: build-time SSR for the initial HTML, and client-side polling for live updates. It's dropped into the sidebar as a &lt;code&gt;&amp;lt;now-listening&amp;gt;&lt;/code&gt; WebC element alongside &lt;code&gt;&amp;lt;my-details&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build time:  listening.js → Last.fm API → tracks data → SSR into HTML
Runtime:     Client JS → polls Last.fm API every 60s → diffs → updates DOM
             ├── localStorage cache (2min TTL) for instant loads
             └── pauses when tab hidden, resumes on focus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data layer: &lt;code&gt;src/_data/listening.js&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is an Eleventy global data file that runs at build time. It calls the Last.fm API - specifically &lt;code&gt;user.getrecenttracks&lt;/code&gt; - requesting the five most recent tracks for my account.&lt;/p&gt;

&lt;p&gt;It has a three-second timeout so that if Last.fm is having a bad day, builds aren't left hanging. On any failure it returns &lt;code&gt;{ tracks: [] }&lt;/code&gt;, which means builds never break regardless of what the API does.&lt;/p&gt;

&lt;p&gt;The raw API response gets mapped into clean objects - &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;artist&lt;/code&gt;, &lt;code&gt;album&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;art&lt;/code&gt;, and a &lt;code&gt;nowPlaying&lt;/code&gt; boolean. All text fields are HTML-escaped and URLs are validated &lt;em&gt;(only &lt;code&gt;http:&lt;/code&gt; and &lt;code&gt;https:&lt;/code&gt; schemes allowed)&lt;/em&gt; - this is the server-side XSS protection layer.&lt;/p&gt;

&lt;p&gt;The data is then available to templates as &lt;code&gt;listening.tracks&lt;/code&gt; via Eleventy's data cascade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component: &lt;code&gt;now-listening.webc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The WebC component has three distinct parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-rendered HTML
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;webc:type="render"&lt;/code&gt; script runs at build time, reading &lt;code&gt;this.$data.listening.tracks&lt;/code&gt;. It generates the initial HTML so the page ships with real track data baked in. This means content is visible immediately on load - and for search engines or anyone browsing without JavaScript, this &lt;em&gt;is&lt;/em&gt; the component. It's done. No spinner, no empty state.&lt;/p&gt;

&lt;h3&gt;
  
  
  A &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; element
&lt;/h3&gt;

&lt;p&gt;An inert HTML template used by the client-side code for DOM cloning. It defines the track row structure with &lt;code&gt;data-*&lt;/code&gt; attribute hooks - &lt;code&gt;data-track&lt;/code&gt;, &lt;code&gt;data-name&lt;/code&gt;, &lt;code&gt;data-detail&lt;/code&gt;, &lt;code&gt;data-art-placeholder&lt;/code&gt;, and &lt;code&gt;data-now-playing&lt;/code&gt;. Nothing renders from this until JavaScript picks it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-side polling script
&lt;/h3&gt;

&lt;p&gt;A self-executing IIFE &lt;em&gt;(kept alive with &lt;code&gt;webc:keep&lt;/code&gt; to prevent Eleventy from stripping it)&lt;/em&gt; that handles the live behaviour:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt; - hits the Last.fm API every 60 seconds to keep the track list current.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;localStorage cache&lt;/strong&gt; - on page load, if a fresh cache exists &lt;em&gt;(two-minute TTL)&lt;/em&gt;, it renders from cache immediately and defers the first API call. This avoids the flash-of-stale-content problem on repeat visits or navigating between pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diffing&lt;/strong&gt; - compares &lt;code&gt;JSON.stringify(tracks)&lt;/code&gt; against the last known state. If nothing has changed, the DOM stays untouched. No unnecessary reflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visibility awareness&lt;/strong&gt; - listens to &lt;code&gt;visibilitychange&lt;/code&gt; to stop polling when the tab is hidden and restart when it becomes visible. If you leave the tab in the background for an hour, it's not hammering the API the whole time. Be a good citizen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AbortController&lt;/strong&gt; - cancels in-flight fetch requests when polling stops, so there's no risk of stale responses landing after the component has moved on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side XSS protection&lt;/strong&gt; - uses the same &lt;code&gt;safeUrl&lt;/code&gt; and &lt;code&gt;esc&lt;/code&gt; helpers as the server layer. Content is inserted via &lt;code&gt;textContent&lt;/code&gt; &lt;em&gt;(inherently safe)&lt;/em&gt; and URLs are validated before being set as &lt;code&gt;href&lt;/code&gt; or &lt;code&gt;src&lt;/code&gt; attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;p&gt;The main tradeoff worth calling out is the duplicated logic. The same API call and data mapping exists in both &lt;code&gt;listening.js&lt;/code&gt; &lt;em&gt;(server)&lt;/em&gt; and the client-side script. I could abstract it into a shared module, but the server code runs in Node during the Eleventy build and the client code runs in the browser. Keeping them self-contained means each layer is independently understandable and testable. The duplication is small and the mapping is straightforward - it's not the kind of logic that's likely to drift in dangerous ways.&lt;/p&gt;

&lt;p&gt;The other thing I'm quietly pleased with is how little the component asks of the outside world. Between the visibility-aware polling, the localStorage cache, and the diff check before touching the DOM, it makes the minimum number of requests necessary. If you're pulling from someone else's API on every page load of your site, I think you owe it to them &lt;em&gt;(and your users)&lt;/em&gt; to be thoughtful about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;It took a couple of hours to build and it makes me smile every time I see it on the page. Sometimes the best features aren't the most technically ambitious - they're the ones that make a site feel like &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I might extract this into a standalone package at some point - the component is fairly self-contained and would slot into any Eleventy site with minimal config.&lt;/p&gt;

&lt;p&gt;If you want to see it in action, it's in the sidebar at &lt;a href="https://martinhicks.dev" rel="noopener noreferrer"&gt;martinhicks.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Remove trailing slash on Eleventy S3 hosted sites using Cloudfront function</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Tue, 16 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/remove-trailing-slash-on-11ty-s3-hosted-sites-using-cloudfront-function-1be7</link>
      <guid>https://dev.to/martinhicks/remove-trailing-slash-on-11ty-s3-hosted-sites-using-cloudfront-function-1be7</guid>
      <description>&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;p&gt;This article will walk through the steps required to create a Cloudfront function to handle redirecting trailing slash URIs to non-trailing slash equivalents on your S3 hosted 11ty website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;Several years ago we published our website at Si Novi using a hand-balled static site generator we built for ourselves, and deployed it to S3 with Cloudfront used for caching and routing our A record from Route 53.&lt;/p&gt;

&lt;p&gt;For this old website we wanted to strip trailing slashes on URLs, so &lt;code&gt;https://example.com/articles/some-article&lt;/code&gt; instead of &lt;code&gt;https://example.com/articles/some-article/&lt;/code&gt;, personal preference I guess.&lt;/p&gt;

&lt;p&gt;Anyway, to achieve this &lt;a href="https://sinovi.uk/articles/static-website-url-optimisation-with-aws-serverless" rel="noopener noreferrer"&gt;we used a Lamda@Edge function to handle the redirects&lt;/a&gt; for us - 301 redirecting from the trailing slash URI to the non-trailing slash URI - something we'd long achieved using &lt;code&gt;.htaccess&lt;/code&gt; on an Apache server.&lt;/p&gt;

&lt;p&gt;We published this function to the &lt;a href="https://serverlessrepo.aws.amazon.com/applications/us-east-1/951661612909/LambdaEdgeRemoveTrailingSlash" rel="noopener noreferrer"&gt;AWS Serverless Application Repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Fast forward a few years, and we now publish our website using &lt;a href="https://11ty.dev/" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt; - still hosted on S3, still fronted with the Cloudfront CDN, but our long-standing redirect function no longer worked.&lt;/p&gt;

&lt;p&gt;With 11ty we were hitting a redirect loop which we believe was due to it's use of subfolder index.html pages - our old hand-balled system created S3 objects like &lt;code&gt;articles/some-article.html&lt;/code&gt; whereas Eleventy creates S3 objects like &lt;code&gt;articles/some-article/index.html&lt;/code&gt;. The the old system resolved to the object correctly, whereas when using sub-directory within an &lt;code&gt;index.html&lt;/code&gt; as 11ty and others do, this caused an infinite redirect loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Create a new CloudFront function
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function handler(event) {
    var request = event.request;
    var uri = request.uri;

    var params = '';
    if(('querystring' in request) &amp;amp;&amp;amp; (request.querystring.length &amp;gt; 0)) {
        params = '?'+request.querystring;
    }

    if(uri.endsWith('/')) {
        if(uri !== '/') {
            var response = {
                statusCode: 301,
                statusDescription: 'Permanently moved',
                headers:
                { "location": { "value": `${uri.slice(0, -1) + params}` } } // remove trailing slash
            }

            return response;    
        }


    }
    //Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }



    return request;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code achieves the same trailing slash removal as we had in our old Lambda@Edge function, but also includes an additional check to ensure that &lt;code&gt;index.html&lt;/code&gt; is appended to any requests on their way to S3 (only if the request doesn't already include '.', so &lt;code&gt;image/some-image.png&lt;/code&gt; will pass-through just fine ).&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Publish the function
&lt;/h4&gt;

&lt;p&gt;Save and publish your newly created function, in this example I've named it &lt;code&gt;subfolder-index&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Configure your Cloudfront distribution to route requests through the function
&lt;/h4&gt;

&lt;p&gt;Modify your Cloudfront distribution's behaviour and set the published function to run on Viewer request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qoint0rvicbzkz0yn7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qoint0rvicbzkz0yn7a.png" alt="A screenshot demonstrating how to select the Cloudfront function as a Viewer request" width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrap up
&lt;/h3&gt;

&lt;p&gt;That's it, you should now be seeing URLs redirected to your non-trailing slash URI preference, while still successfully serving the subfolder index.html file (which wont appear in the URL)&lt;/p&gt;

&lt;p&gt;One benefit of using a Cloudfront function is it's cheaper to invoke than a Lambda@Edge function.&lt;/p&gt;

&lt;p&gt;You can see our old &lt;a href="https://github.com/sinovi/lambda-edge-remove-trailing-slash" rel="noopener noreferrer"&gt;Lambda@Edge function here&lt;/a&gt;, which still might be useful.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ps: If you're not hosting subfolder &lt;code&gt;index.html&lt;/code&gt; files you can remove the else if from the Cloudfront function.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>11ty</category>
      <category>cloudfront</category>
      <category>s3</category>
    </item>
    <item>
      <title>Enhance CSRF package - now supports multipart form data</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Mon, 15 May 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/enhance-csrf-package-now-supports-multipart-form-data-4nb7</link>
      <guid>https://dev.to/martinhicks/enhance-csrf-package-now-supports-multipart-form-data-4nb7</guid>
      <description>&lt;p&gt;I've just published a minor update to my &lt;a href="https://www.npmjs.com/package/@hicksy/enhance-csrf" rel="noopener noreferrer"&gt;CSRF plugin&lt;/a&gt; for &lt;a href="https://enhance.dev/" rel="noopener noreferrer"&gt;Enhance projects&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;v0.9.0 now supports &lt;code&gt;multipart/form&lt;/code&gt; data, meaning you can easily use the &lt;code&gt;&amp;lt;csrf-form&amp;gt;&amp;lt;/csrf-form&amp;gt;&lt;/code&gt; component for forms that contain file uploads.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;usage:&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;csrf-form method="post" action="/upload" enctype="multipart/form-data"&amp;gt;
  &amp;lt;input type="file" name="file" /&amp;gt;
&amp;lt;/csrf-form&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;outputs:&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form action="/si-novi/christopher-dee/media/upload" method="post" enctype="multipart/form-data"&amp;gt;
  &amp;lt;input type="hidden" name="csrf" value="540c460e-946e-4c78-8c8d-63c4cd091ee9"&amp;gt; &amp;lt;!-- auto-generated hidden input with the unique csrf token for this request (use with verifyCsrfToken on your post handler) --&amp;gt;
  &amp;lt;input type="file" name="file"&amp;gt;
&amp;lt;/form&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, I've also improved the html generation for the component so you can now include all the following optional attributes and they'll pass-through into your HTML &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enctype
target
acceptCharset
autocomplete
id
novalidate
rel

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course you can still use the standaone &lt;code&gt;&amp;lt;csrf-input&amp;gt;&amp;lt;/csrf-input&amp;gt;&lt;/code&gt; if you'd prefer to use a standard form tag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get in touch
&lt;/h3&gt;

&lt;p&gt;Any feedback on your usage of this plugin, bugs, or suggestions for improvements please get in touch on &lt;a href="https://github.com/hicksy/enhance-csrf" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I'd love to hear from you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/hicksy/enhance-csrf" rel="noopener noreferrer"&gt;https://github.com/hicksy/enhance-csrf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NPM: &lt;a href="https://www.npmjs.com/package/@hicksy/enhance-csrf" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@hicksy/enhance-csrf&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>DynamoDB Streams locally with arc.codes &amp; Enhance</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Mon, 24 Apr 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/dynamodb-streams-locally-with-arccodes-enhance-c33</link>
      <guid>https://dev.to/martinhicks/dynamodb-streams-locally-with-arccodes-enhance-c33</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;When working on a recent web app project using &lt;a href="https://enhance.dev/" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt;, I encountered a requirement for using both transactions and DynamoDB streams, two DynamoDB features that aren't supported by Dynalite - the fast in-memory DynamoDB engine that &lt;a href="https://arc.codes/" rel="noopener noreferrer"&gt;Architect&lt;/a&gt; uses to support local development.&lt;/p&gt;

&lt;p&gt;In this article, I'll introduce my new plugin (&lt;a href="https://www.npmjs.com/package/@hicksy/arc-plugin-sandbox-stream" rel="noopener noreferrer"&gt;arc-plugin-sandbox-stream&lt;/a&gt;) for working with DynamoDB streams locally within Arc and Enhance projects. This plugin enables developers to use DynamoDB streams locally in their arc or enhance sandbox environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;Architect got me hooked back in 2018 with its amazing local developer experience, it's a huge DX boon to be able to try ideas out on your own machine without being concerned about provisioning real infrastructure, or being tied to a location where network connectivity is guaranteed.&lt;/p&gt;

&lt;p&gt;One of the services Architect spins up locally is &lt;a href="https://github.com/mhart/dynalite" rel="noopener noreferrer"&gt;Dynalite&lt;/a&gt;. It provides a fast in-memory DynamoDB engine. For lots of projects this engine is absolutely ideal, fast to launch, lightweight and not a process hog. But for this particular project I really needed streams and I wasn't prepared to sacrifice the local developer experience. So I set about researching how this could be achieved, if at all. Helpfully it's a question that has cropped up on &lt;a href="https://discord.com/channels/880272256100601927/1078256032087805972/1079142054279524552" rel="noopener noreferrer"&gt;Arc's discord&lt;/a&gt; and the thread of replies helped point me in the right direction.&lt;/p&gt;

&lt;p&gt;I'd need to do a few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch to using DynamoDB Local&lt;/li&gt;
&lt;li&gt;Create a middleware to poll the stream for new data&lt;/li&gt;
&lt;li&gt;Invoke the corresponding function handler when new data is returned from the stream.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using the plugin
&lt;/h2&gt;

&lt;p&gt;The plugin kicks in when a &lt;code&gt;@tables-streams&lt;/code&gt; pragma is discovered within your arc config, and queries the table meta data / stream meta data to retrieve iterators for each shard of the table's stream.&lt;/p&gt;

&lt;p&gt;If data is found the plugin invokes the corresponding lambda function using arc's inbuilt invoke function.&lt;/p&gt;

&lt;p&gt;Your function code will receive one or more results off the stream like so (as you'd expect if you've used streams on AWS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "eventID": "7c1ac231-2c9d-4ba0-b3a2-1de2eb6602c0",
    "eventName": "MODIFY",
    "eventVersion": "1.1",
    "eventSource": "aws:dynamodb",
    "awsRegion": "ddblocal",
    "dynamodb": {
      "ApproximateCreationDateTime": "2023-04-24T12:19:00.000Z",
      "Keys": {
        "sk": {
          "S": "account:si-novi"
        },
        "pk": {
          "S": "account:si-novi"
        }
      },
      "NewImage": {
        "_type": {
          "S": "Organisation"
        },
        "name": {
          "S": "Si Novi"
        },
        "sk": {
          "S": "account:si-novi"
        },
        "created_at": {
          "S": "2023-04-24T12:19:07.409Z"
        },
        "id": {
          "S": "01GYSK850HWHGE36A2VTDG1CPK"
        },
        "pk": {
          "S": "account:si-novi"
        },
        "modified_at": {
          "S": "2023-04-24T12:19:07.409Z"
        },
        "slug": {
          "S": "si-novi"
        }
      },
      "SequenceNumber": "000000000000000000935",
      "SizeBytes": 310,
      "StreamViewType": "NEW_IMAGE"
    }
  }
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Add the dependency to your project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install @hicksy/arc-plugin-sandbox-stream&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Configure your project to use @tables-streams in &lt;code&gt;.arc&lt;/code&gt; file&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@tables
example
  pk *String
  sk **String

@tables-streams
example

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add the plugin to arc config&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@plugins
hicksy/arc-plugin-sandbox-stream

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Setup DynamoDB Local&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are various ways to use DyanmoDB Local. I opted for the bundled version that comes with &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html" rel="noopener noreferrer"&gt;AWS's NoSQL Workbench tool&lt;/a&gt;. But you can use which ever version you're comfortable with.&lt;/p&gt;

&lt;p&gt;Just note that if you use the NoSQL Workbench version there's the following caveats I came accross:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data storage is persistent in the NoSQL Workbench version - I can't find a way to set the &lt;code&gt;inMemory&lt;/code&gt; flag available to the standalone / docker version (you'll need to modify any seed scripts to conditionally insert or drop and re-create the table)&lt;/li&gt;
&lt;li&gt;You need to be running NoSQL Workbench while you develop and remember to toggle on DynamoDB Local each time (not a biggie really, but it'd be great to have the means to set it to autostart or run as a start-up background task)&lt;/li&gt;
&lt;li&gt;The NoSQL Workbench version runs on port 5500, I can't see a way to change this, but it's not a problem for me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Configure Arc to use DynamoDB Local&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Arc environment params
&lt;/h4&gt;

&lt;p&gt;Arc makes it really easy to switch out Dynalite for DynamoDB Local.&lt;/p&gt;

&lt;p&gt;There's a couple of env vars you need to set to get you going.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tell arc that you're using an external db&lt;/li&gt;
&lt;li&gt;Change the port used for tables within arc&lt;/li&gt;
&lt;li&gt;Set your region to &lt;code&gt;ddblocal&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can do this one of several ways.&lt;/p&gt;

&lt;p&gt;Either setting an environment variable ARC_DB_EXTERNAL=true / ARC_TABLES_PORT=5500 in an &lt;code&gt;.env&lt;/code&gt; file, or using the &lt;code&gt;prefs.arc&lt;/code&gt; file and setting the properties within the @sandbox section &lt;a href="https://arc.codes/docs/en/reference/configuration/local-preferences#local-preferences" rel="noopener noreferrer"&gt;see arc's docs for more info&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;e.g.&lt;/p&gt;

&lt;p&gt;.env file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_URL=http://localhost:3333
ARC_TABLES_PORT=5500

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;prefs.arc file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@sandbox
external-db true

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Arc data seeds
&lt;/h4&gt;

&lt;p&gt;As DynamoDB Local data is persistent when using the bundled NoSQL Workbench version, you'll need to modify any startup data seeds you have and update them to either conditionally insert data, or what I prefer, to delete the table and re-create it each time - this makes it more in keeping with all my other arc projects.&lt;/p&gt;

&lt;h4&gt;
  
  
  Arc table create or update
&lt;/h4&gt;

&lt;p&gt;Sadly, as Dynalite doesn't support streams, arc's sandbox doesn't create the table with the required &lt;code&gt;StreamSpecification&lt;/code&gt; param, so you'll need to supply that either when re-creating the table using &lt;code&gt;createTable&lt;/code&gt; or supplying it via an &lt;code&gt;updateTable&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;An example &lt;code&gt;StreamSpecifcation&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StreamSpecification: {
    StreamEnabled: true,
    StreamViewType: 'NEW_IMAGE' 
},

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project I'm working on currently uses Sensedeep's &lt;a href="https://github.com/sensedeep/dynamodb-onetable" rel="noopener noreferrer"&gt;OneTable&lt;/a&gt;. So I can achieve the above like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const client = new Dynamo({client: new DynamoDBClient(params)})
const table = new Table({
    name: DDB_NAME,
    client: client,
    logger: true,
    schema: AppSchema,
    partial: false
})

if(await table.exists()) {
    await table.deleteTable('DeleteTableForever');
}

await table.createTable({
    StreamSpecification: {
        StreamEnabled: true,
        StreamViewType: 'NEW_IMAGE' 
    },
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also do this using the standard JS dynamodb client for any projects that aren't using an additional tool like onetable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;tip: use &lt;a href="https://www.npmjs.com/package/@architect/functions" rel="noopener noreferrer"&gt;@architect/functions&lt;/a&gt; to infer the full name for your DynamoDB table's, which will be a combination of the name you've given and it's deployement environment etc&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Modify the polling interval [optional]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default the plugin will poll for new stream data every 10 seconds. You can override this by providing an alternate millisecond interval by updating your &lt;code&gt;.arc&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@sandbox-table-streams
polling_interval 1000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get in touch
&lt;/h3&gt;

&lt;p&gt;Any feedback on your usage of this plugin, bugs, or suggestions for improvements please get in touch on &lt;a href="https://github.com/hicksy/arc-plugin-sandbox-stream" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I'd love to hear from you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/hicksy/arc-plugin-sandbox-stream" rel="noopener noreferrer"&gt;https://github.com/hicksy/arc-plugin-sandbox-stream&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NPM: &lt;a href="https://www.npmjs.com/package/@hicksy/arc-plugin-sandbox-stream" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@hicksy/arc-plugin-sandbox-stream&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>webdev</category>
      <category>aws</category>
      <category>dynamodb</category>
      <category>javascript</category>
    </item>
    <item>
      <title>(re-)Introduction</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Tue, 20 Dec 2022 13:58:47 +0000</pubDate>
      <link>https://dev.to/martinhicks/re-introduction-3b1c</link>
      <guid>https://dev.to/martinhicks/re-introduction-3b1c</guid>
      <description>&lt;p&gt;Hello 👋🏻&lt;/p&gt;

&lt;p&gt;I'm Martin, &lt;a href="https://martinhicks.net" rel="noopener noreferrer"&gt;a UK based Web Developer and AWS consultant&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I've been a member of this community for 4 years now, but purely in read-only mode I'm afraid. Recent events have made me reconsider my online profile and I'm trying to take more ownership of my content. &lt;/p&gt;

&lt;p&gt;Still dipping-my-toe into &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt;, but hope to make it a more regular fixture in 2023. &lt;/p&gt;

&lt;p&gt;I've just published a handful of back-dated posts from my personal website - &lt;a href="https://martinhicks.net/articles" rel="noopener noreferrer"&gt;martinhicks.net&lt;/a&gt; - over here as a starter for ten. &lt;/p&gt;

&lt;p&gt;1. A look into a brilliant new Web Component based web framework - &lt;a href="https://enhance.dev" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt; &lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai" class="crayons-story__hidden-navigation-link"&gt;Sharing Enhance elements between projects&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/martinhicks" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" alt="martinhicks profile" class="crayons-avatar__image" width="800" height="1066"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/martinhicks" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Martin Hicks
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Martin Hicks
                
              
              &lt;div id="story-author-preview-content-1303567" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/martinhicks" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" class="crayons-avatar__image" alt="" width="800" height="1066"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Martin Hicks&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 15 '22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai" id="article-link-1303567"&gt;
          Sharing Enhance elements between projects
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/enhance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;enhance&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webcomponents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webcomponents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/progressiveenhancement"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;progressiveenhancement&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;2. Some brief thoughts on why I'm excited about a shift towards web-platform first principles &lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/martinhicks/back-to-the-future-2coj" class="crayons-story__hidden-navigation-link"&gt;Back to the Future&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/martinhicks" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" alt="martinhicks profile" class="crayons-avatar__image" width="800" height="1066"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/martinhicks" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Martin Hicks
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Martin Hicks
                
              
              &lt;div id="story-author-preview-content-1303546" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/martinhicks" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" class="crayons-avatar__image" alt="" width="800" height="1066"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Martin Hicks&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/martinhicks/back-to-the-future-2coj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 12 '22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/martinhicks/back-to-the-future-2coj" id="article-link-1303546"&gt;
          Back to the Future
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webplatform"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webplatform&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webframeworks"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webframeworks&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/martinhicks/back-to-the-future-2coj" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/martinhicks/back-to-the-future-2coj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;
 

&lt;p&gt;3. A guide to using 11ty's upcoming 2.0 version along with their new templating language, WebC&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/martinhicks/eleventy-20-webc-2a4o" class="crayons-story__hidden-navigation-link"&gt;Eleventy 2.0 &amp;amp; WebC&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/martinhicks" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" alt="martinhicks profile" class="crayons-avatar__image" width="800" height="1066"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/martinhicks" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Martin Hicks
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Martin Hicks
                
              
              &lt;div id="story-author-preview-content-1303569" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/martinhicks" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F94005%2F6aef5084-5a35-4e9d-b7fa-a1f1e53a2f87.jpeg" class="crayons-avatar__image" alt="" width="800" height="1066"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Martin Hicks&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/martinhicks/eleventy-20-webc-2a4o" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 17 '22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/martinhicks/eleventy-20-webc-2a4o" id="article-link-1303569"&gt;
          Eleventy 2.0 &amp;amp; WebC
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webc"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webc&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/11ty"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;11ty&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/eleventy"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;eleventy&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/staticsite"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;staticsite&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/martinhicks/eleventy-20-webc-2a4o#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            11 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Looking forward to becoming more active in the community. &lt;/p&gt;

&lt;p&gt;Thanks for having me :)&lt;/p&gt;

</description>
      <category>introduction</category>
      <category>webcomponents</category>
      <category>webframeworks</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A mistake the internet won't forget easily</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Mon, 19 Dec 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/a-mistake-the-internet-wont-forget-easily-5e8j</link>
      <guid>https://dev.to/martinhicks/a-mistake-the-internet-wont-forget-easily-5e8j</guid>
      <description>&lt;p&gt;_ &lt;strong&gt;tldr&lt;/strong&gt; : If you visited my site between 2022-12-01 and 2022-12-18 you might not be seeing updated content on previously accessed pages. I've added a &lt;code&gt;Clear-Site-Data&lt;/code&gt; header to this page to possibly rectify. If you don't see multiple blogs posts on the homepage after viewing this article, please manually refresh any page you previously visited - thank you_ 🙏&lt;/p&gt;

&lt;p&gt;_ &lt;strong&gt;edit: 2022-12-20&lt;/strong&gt; - Following a &lt;a href="https://indieweb.social/@martinhicks/109542056075959267" rel="noopener noreferrer"&gt;brief discussion with Jake Archibold&lt;/a&gt; he offered the suggestion to use a fetch request with the &lt;code&gt;cache: 'reload'&lt;/code&gt; property. &lt;strong&gt;The nice thing about this is that it's cross-browser, so even Safari on both Desktop and iOS should re-validate their local cache using this technique.&lt;/strong&gt; It's still not something you'd want to put on every request - &lt;a href="https://martinhicks.net/articles/how-a-misconfigured-header-caused-an-unforgettable-problem#update-20th-dec-2022" rel="noopener noreferrer"&gt;I've added an update below that describes this&lt;/a&gt;._&lt;/p&gt;




&lt;p&gt;A couple of weeks or so ago, enthused by my move to &lt;a href="https://indieweb.social/@martinhicks" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;, the growing desire of owning your own content post-Musk, and an upcoming New Year's resolution I'd made with myself to be "less mute" on the internet in 2023 - I decided to re-publish my website and start sharing my thoughts in longer form writing.&lt;/p&gt;

&lt;p&gt;Over the weekend &lt;em&gt;(hungover, after some pre-Christmas drinks the night before)&lt;/em&gt; I realised I'd made a BIG mistake with the website deployment mechanism - accidentally setting a long-term &lt;code&gt;cache-control&lt;/code&gt; header for all resources, not just the immutable ones 🤦🏻‍♂️.&lt;/p&gt;

&lt;p&gt;Read on for what what caused the problem, how I've resolved it, the steps I've taken to mitigate it, and what I wish was possible to limit damage following a situation like this in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic deployments
&lt;/h2&gt;

&lt;p&gt;I update this site using a &lt;a href="https://github.com/hicksy/martinhicks.net/blob/main/.github/workflows/deploy.yml" rel="noopener noreferrer"&gt;GitHub action&lt;/a&gt;; triggering a build of my static site and transferring the web assets over to AWS S3 where I statically host my website.&lt;/p&gt;

&lt;p&gt;It's within this S3 sync command that I made the galling mistake...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 sync ./_site s3://martinhicks.net --cache-control max-age=31536000 --delete --acl=public-read --follow-symlinks

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not familiar with the AWS S3 cli - this command transfers the directory &lt;code&gt;/_site&lt;/code&gt; to the S3 bucket hosting this website.&lt;/p&gt;

&lt;p&gt;As you can see I use a number of CLI params:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--delete&lt;/code&gt;: This tells the &lt;code&gt;s3 sync&lt;/code&gt; command to delete any items that exist in the destination bucket, but aren't in the local source directory &lt;em&gt;(perfect for clearing up old, no-longer needed objects)&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--acl=public-read&lt;/code&gt;: Every object uploaded to the bucket has public read enabled - this being a public website, you need every asset to be available. Omitting this would prevent you from accessing the page, despite the bucket itself being public&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--follow-symlink&lt;/code&gt;: I &lt;em&gt;think&lt;/em&gt; this is a default actually, but added it in just to make sure any content symlinked into that source directory was actually transferred&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--cache-control&lt;/code&gt;: The biggie, the doozy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here I inform S3 to set the Cache-Control header - a header S3 will include when returning a request for each individual web asset - a html page, a css file, an image, a sitemap.xml... i.e. everything I've just synced.&lt;/p&gt;

&lt;p&gt;In haste I inadvertently set the &lt;code&gt;max-age&lt;/code&gt; value to &lt;code&gt;31536000&lt;/code&gt; - that's ONE YEAR - and I told S3 to do so for all my assets. Therefore for every request, S3 will return the header &lt;code&gt;cache-control: max-age=31536000&lt;/code&gt;, which tells your web browser that it's allowed to cache the contents of the page you requested &lt;em&gt;for up to one year.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this a problem - caching is good right?
&lt;/h2&gt;

&lt;p&gt;So firstly, caching is great.&lt;/p&gt;

&lt;p&gt;It's definitely the right thing to do. You'll improve the speed in which return visitors access your page(s), and you'll receive a better Page Speed Rank from Google, which is a &lt;a href="https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking" rel="noopener noreferrer"&gt;ranking factor on Google's search results&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using a CDN is even better - I do that here too with &lt;a href="https://aws.amazon.com/cloudfront/" rel="noopener noreferrer"&gt;AWS CloudFront&lt;/a&gt; storing a cache on the edge, closer to where you're accessing the internet from.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;eg: you live in London, someone else from London has already accessed this page, you then visit the same page, and the CDN will serve you from the edge cache - win for you as the page will load much faster, bonus for a webmaster as the origin server won't have to handle the request&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Anyway that's an aside.&lt;/p&gt;

&lt;p&gt;Here I'm taking about the &lt;code&gt;cache-control: max-age=31536000&lt;/code&gt; header I was inadvertently serving.&lt;/p&gt;

&lt;p&gt;The issue with that header is the liberal use of it for all my assets. It's absolutely the correct header to return for anything immutable - images, css etc.&lt;/p&gt;

&lt;p&gt;If you visit &lt;a href="https://martinhicks.net/" rel="noopener noreferrer"&gt;my home page&lt;/a&gt;, you'll see that it has some changeable content - a list of latest blog posts that are updated when I publish a new article. So by setting this long-term cache on all web assets, I'd created a situation where the next time you visit my homepage you'd think I hadn't written any new content - you'd likely move on elsewhere - you most certainly wouldn't think;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I know - I'll hit refresh, just in case Martin has messed up his cache headers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had an inkling something wasn't right during development, but given I'm an impatient soul, while my site was being synced with S3, in another tab I would be furiously hitting refresh to see the changes&lt;/p&gt;

&lt;p&gt;&lt;em&gt;hitting refresh clears the cache, and hid the problem&lt;/em&gt;. It was only when I revisited a page on mobile (iOS Safari), that I realised older content was being served.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correct way to cache content for a website
&lt;/h2&gt;

&lt;p&gt;If you've read this far, you might be interested in seeing a more sensible mechanism for caching content using the S3 sync command.&lt;/p&gt;

&lt;p&gt;I updated my GitHub action to make two separate calls to s3 sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//long-lived cache for images and css
aws s3 sync ./_site s3://martinhicks.net --cache-control 'max-age=31536000' --delete --acl=public-read --follow-symlinks --exclude '*' --include 'images/*' --include 'css/*'

//no-cache on everything else
aws s3 sync ./_site s3://martinhicks.net --cache-control 'no-cache' --delete --acl=public-read --follow-symlinks --exclude 'images/*' --exclude 'css/*'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command ensures that my immutable content (images and css) are served with a year long &lt;code&gt;max-age&lt;/code&gt;. Telling the browser that once you've received an image or a css file for the first time to cache it, and keep it in cache for up-to a year. &lt;em&gt;(i.e. your browser won't keep requesting it on subsequent pages, or reloads, over the internet. It'll serve that image from your local browser cache instead.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The key props to that are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--exclude '*'&lt;/code&gt;: tell s3 sync to exclude every item in the source &lt;code&gt;_/site&lt;/code&gt; directory &lt;em&gt;(nothing that isn't explicitly included will be transferred to S3)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--include 'images/*'&lt;/code&gt; &amp;amp; &lt;code&gt;--include 'css/*&lt;/code&gt;: tell s3 sync to include anything within images and the css directory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the second pass of S3 sync, we invert that - excluding the &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;css&lt;/code&gt; directories. This run will upload every web asset that isn't in the two excluded directories while setting a cache-control header of &lt;code&gt;no-cache&lt;/code&gt; &lt;em&gt;(i.e. don't locally cache the HTML page in browser, re-request it from the CDN or origin as appropriate for every request)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the problem for returning visitors
&lt;/h2&gt;

&lt;p&gt;Sadly, this isn't a problem that's easily fixed.&lt;/p&gt;

&lt;p&gt;First off, you need to hope anyone who accessed page content with a long cache header, will revisit your site to read something new you've written - this is the only way to set a new header after all, as the old content, say the index page, will have already been stored long-term in the returning-visitor's browsers cache.&lt;/p&gt;

&lt;p&gt;After studying MDN I found the &lt;code&gt;Clear-Site-Data&lt;/code&gt; header, which is exactly what I need.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data" rel="noopener noreferrer"&gt;As MDN states&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Clear-Site-Data header clears browsing data (cookies, storage, cache) associated with the requesting website. It allows web developers to have more control over the data stored by a client browser for their origins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Great. So this article is being served with the following header.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Clear-Site-Data: "cache"&lt;/code&gt;. Which I've acheived by creating a response headers policy on CloudFront that includes this header in the response for this specific path.&lt;/p&gt;

&lt;p&gt;Which I sincerely hope means your browser has now been &lt;a href="https://meninblack.fandom.com/wiki/Neuralyzer" rel="noopener noreferrer"&gt;zapped by a Neuralyzer&lt;/a&gt; and forgotten any cached data it stored. It's good for all browsers except for Safari (desktop and iOS), and Firefox (although that's behind a feature flag, so hopefully wider roll-out will be in an iminent version).&lt;/p&gt;

&lt;p&gt;~&lt;em&gt;(If you're on iOS, maybe you could humour me by giving the &lt;a href="https://martinhicks.net/" rel="noopener noreferrer"&gt;homepage&lt;/a&gt; and &lt;a href="https://martinhicks.net/articles" rel="noopener noreferrer"&gt;journal&lt;/a&gt; page a refresh?)&lt;/em&gt;~&lt;/p&gt;

&lt;h2&gt;
  
  
  Is there a better solution?
&lt;/h2&gt;

&lt;p&gt;I'm currently only applying the &lt;code&gt;Clear-Site-Data&lt;/code&gt; header to this page and really hoping / relying on some of the people that read my earlier articles might stumble upon this one? Which is probably unrealistic...&lt;/p&gt;

&lt;p&gt;It feels like I shouldn't apply this header on every new page I publish from this point forward - wouldn't that mean the browser will never cache any data? I guess I could include the header for a limited period on all html pages, but it still doesn't feel quite right.&lt;/p&gt;

&lt;p&gt;What I really wish is that there was a more deterministic header I could apply, say something like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Clear-Site-Data: 'cache' '2022-12-01:2022-12-18'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Wouldn't it be handy to be able to say "clear this site cache if the item was stored between the following dates"?&lt;/p&gt;

&lt;p&gt;That way I'd be able to drop this header onto all future assets, and eventually anyone returning to my site will have the rogue cached pages cleared - without affecting the behaviour of anything cached before or after the incident date.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I have no idea how the internals of a browser work. Maybe this is nonsense as the item in cache might just store its expires-at date, rather than the date it was accessed and originally stored?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To be honest, I'm probably over analysing the problem. There's been very little inter-page navigation here - most people have followed a link to a specific article, read it and then moved on. But it's still bugging me...&lt;/p&gt;

&lt;p&gt;I'd be keen to hear from anyone who has any better ideas. Is there a more appropriate solution I could apply?&lt;/p&gt;

&lt;p&gt;You can contact me at &lt;a href="https://indieweb.social/@martinhicks" rel="noopener noreferrer"&gt;https://indieweb.social/@martinhicks&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Update: 20th Dec 2022:
&lt;/h2&gt;

&lt;p&gt;I outlined my issue on the &lt;a href="https://indieweb.social/@martinhicks/109542056075959267" rel="noopener noreferrer"&gt;Fediverse&lt;/a&gt; - and tagged in &lt;a href="https://indieweb.social/@jaffathecake@mastodon.social" rel="noopener noreferrer"&gt;Jake Archibald&lt;/a&gt; and &lt;a href="https://indieweb.social/@slightlyoff@toot.cafe" rel="noopener noreferrer"&gt;Alex Russell&lt;/a&gt; to see if they had any ideas - I wasn't expecting a response tbh, so was blown away that they took time out to offer their thoughts.&lt;/p&gt;

&lt;p&gt;Alex rightly pointed out that Apple lagging behind on this spec &lt;em&gt;(and many more, let's be clear)&lt;/em&gt;, and the general issues holding up improving the web, are largely down to the lack of browser competition on iOS - something the &lt;a href="https://open-web-advocacy.org/" rel="noopener noreferrer"&gt;Open Web Advocacy (OWA)&lt;/a&gt; group are looking to change. I completely agree and support this cause, and I'm going to reach out on &lt;a href="https://discord.gg/x53hkqrRKx" rel="noopener noreferrer"&gt;OWA's Discord&lt;/a&gt; to see how I go about helping. If you care about the web, then why don't you too?&lt;/p&gt;

&lt;p&gt;Jake offered the following suggestion - &lt;code&gt;fetch(brokenUrl, { cache: 'reload' })&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I tested this out on a random test URL and set the long &lt;code&gt;max-time&lt;/code&gt; on its &lt;code&gt;cache-control&lt;/code&gt; response header. I then hit a second page (after changing the initial test page's &lt;code&gt;cache-control&lt;/code&gt; to &lt;code&gt;no-cache&lt;/code&gt;), which included the suggested fetch call to the now stuck test page.&lt;/p&gt;

&lt;p&gt;After hitting the page with this fetch command, the rogue stuck resource was re-requested by Safari, and when I returned to the original long-cached page it had it's new &lt;code&gt;cache-control: no-cache&lt;/code&gt; applied.&lt;/p&gt;

&lt;p&gt;It's perfect for what I need - a cross-browser solution, albeit with an additional couple of network requests as you need to specify individual stuck resources - there's no site-wide mechanism like with &lt;code&gt;clear-site-data&lt;/code&gt;. But these quick tests proved that you can clear the cache of a previously visited page, from a second independent page (on the same domain).&lt;/p&gt;

&lt;p&gt;This technique uses the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/cache" rel="noopener noreferrer"&gt;Request.cache api&lt;/a&gt; to tell the browser to re-request the URL specified without using any existing local cache. And to re-populate its cache based on the rules of the new &lt;code&gt;cache-contol&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Like with &lt;code&gt;clear-site-data&lt;/code&gt; you wouldn't want to apply this to all your pages, but if you can get your returning-vistors to view some new content* it's a great solution.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;* perhaps a special article such as this, or if you've built a web-app you could email users who you know had accessed your service between a specific date period, and send them to a brand new landing page to peform the cleanse.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's also an experimental spec proposal &lt;code&gt;only-if-cached&lt;/code&gt; on this API too. Meaning in the future, we could get even more specific and only perform the cache reload if the problematic stuck resource is a) in cache for this specific visitor, and b) has a date earlier than the incident.&lt;/p&gt;

&lt;p&gt;Anyway, for now.. I've added the following script to this page, and this page only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script type="module"&amp;gt;

  async function clearRogueCache(url) {
      return await fetch(url, {
          "cache": "reload"
      });
  }

  await clearRogueCache("https://martinhicks.net");
  await clearRogueCache("https://martinhicks.net/articles");

&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There we go. Still hoping this article can catch any of the early readers of my site. But also hopeful that if you're reading this way in the future, and you've made a similar mistake, you might have more options at your disposal - lucky you.&lt;/p&gt;

&lt;p&gt;Thanks again Jake and Alex - really appreciated.&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>caching</category>
      <category>aws</category>
    </item>
    <item>
      <title>Eleventy 2.0 &amp; WebC</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Sat, 17 Dec 2022 17:34:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/eleventy-20-webc-2a4o</link>
      <guid>https://dev.to/martinhicks/eleventy-20-webc-2a4o</guid>
      <description>&lt;p&gt;I recently rebuilt this website and my company website - &lt;a href="//sinovi.uk"&gt;sinovi.uk&lt;/a&gt; - using &lt;a href="https://www.11ty.dev/" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt; 2.0 and their new &lt;a href="https://www.11ty.dev/docs/languages/webc/" rel="noopener noreferrer"&gt;WebC&lt;/a&gt; language for templating.&lt;/p&gt;

&lt;p&gt;It's really good. And well worth checking out. &lt;/p&gt;

&lt;p&gt;In this post I look at my experiences trying out Eleventy 2.0 and its new Web Component language, WebC.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the time of writing 11ty 2.0 is pre-release. &lt;/p&gt;

&lt;p&gt;It’s just hit version &lt;a href="https://www.npmjs.com/package/@11ty/eleventy/v/2.0.0-canary.20" rel="noopener noreferrer"&gt;canary-20&lt;/a&gt; and judging by 11ty's &lt;a href="https://fosstodon.org/@eleventy/109524124580037564" rel="noopener noreferrer"&gt;recent toot&lt;/a&gt;, it’s official release is very close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I’d heard of the Eleventy project in passing via newsletters and Twitter posts for several years, but up until a few months ago I hadn’t tried to build anything with it.&lt;/p&gt;

&lt;p&gt;I think I’d mentally stored it away as similar in vain to Gatsby  or some similar react tool - &lt;em&gt;...there's a lot of those, right?&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;How wrong was I?&lt;/p&gt;

&lt;h2&gt;
  
  
  Static site generation
&lt;/h2&gt;

&lt;p&gt;Back in 2018, at Si Novi, we rolled our own internal tool for static site generation, that I’ve now learnt felt pretty similar to 11ty in some respects, but lacked its finesse and feature set.&lt;/p&gt;

&lt;p&gt;Our templates were HTML and we used a companion JSON file &lt;em&gt;(ie index.html &amp;amp; index.json)&lt;/em&gt; to hook data into each page view using HTML attributes. &lt;/p&gt;

&lt;p&gt;We had collections in an array within a standalone json file &lt;em&gt;(eg articles.json)&lt;/em&gt;. We even hooked it up to PHP blade templates for one test project.&lt;/p&gt;

&lt;p&gt;There was a key thing wrong with it though &lt;strong&gt;- it was a bloody pain to use.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;At the time we didn’t know about the &lt;a href="https://www.11ty.dev/docs/data-frontmatter/" rel="noopener noreferrer"&gt;front matter&lt;/a&gt; syntax, which in hindsight might’ve helped. &lt;/p&gt;

&lt;p&gt;But the key issue was our node CLI processing engine was written using a jquery compatible dom lib - &lt;a href="https://www.npmjs.com/package/cheerio" rel="noopener noreferrer"&gt;Cheerio&lt;/a&gt;, and a load of &lt;a href="https://gruntjs.com/" rel="noopener noreferrer"&gt;Grunt&lt;/a&gt; hooks. &lt;/p&gt;

&lt;p&gt;It was fiddly; hard to maintain and lacked features, and we never quite found the time to make improvements alongside our client work.&lt;/p&gt;

&lt;p&gt;Furthermore, we were having to stuff lots of properties into the JSON files to handle conditional templating logic particularly for reusing partial views / re-usable template snippets.&lt;/p&gt;

&lt;p&gt;Maybe if we’d known about frontmatter, had chosen a templating engine like &lt;a href="https://mozilla.github.io/nunjucks/" rel="noopener noreferrer"&gt;nunjucks&lt;/a&gt;, and had known about &lt;a href="https://github.com/inikulin/parse5" rel="noopener noreferrer"&gt;parse5&lt;/a&gt;, I’d still be maintaining it now - who knows?&lt;/p&gt;

&lt;p&gt;What I do know &lt;em&gt;(now)&lt;/em&gt; is that 11ty absolutely nails static site generation. &lt;/p&gt;

&lt;p&gt;They have multiple &lt;a href="https://www.11ty.dev/docs/languages/" rel="noopener noreferrer"&gt;templating languages&lt;/a&gt;, &lt;a href="https://www.11ty.dev/docs/layout-chaining/" rel="noopener noreferrer"&gt;nested layouts&lt;/a&gt;, a &lt;a href="https://www.11ty.dev/docs/config/" rel="noopener noreferrer"&gt;sensible config&lt;/a&gt; and &lt;a href="https://www.11ty.dev/docs/plugins/" rel="noopener noreferrer"&gt;plugin system&lt;/a&gt;, and a really cool &lt;a href="https://www.11ty.dev/docs/data-cascade/" rel="noopener noreferrer"&gt;data cascade&lt;/a&gt; which provides lots of options for populating a page’s templating data or mutating a particular value prior to generating the page.&lt;/p&gt;

&lt;p&gt;The docs site is &lt;em&gt;veeeeerrrry&lt;/em&gt; comprehensive. To be honest… so big I found it overwhelming to begin with &lt;em&gt;(it really melted my head for a bit)&lt;/em&gt;, but it's a fantastic resource and a credit to the community of contributors. &lt;/p&gt;

&lt;h2&gt;
  
  
  My first look
&lt;/h2&gt;

&lt;p&gt;I took my first spin of Eleventy when I heard about &lt;a href="https://enhance.dev" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt; in September and noticed one of their &lt;a href="https://enhance.dev/docs/learn/deployment/11ty" rel="noopener noreferrer"&gt;deployment targets was 11ty&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Building out a few demo pages, I really liked what I saw in both projects but struggled with a few bits as I tried to understand how these worked together.&lt;/p&gt;

&lt;p&gt;Attempting to learn two new things at once meant I was doing a disservice to both tools; hampering my understanding of each project, and limiting my discovery of any overlap / boundaries of concern when using them together.&lt;/p&gt;

&lt;p&gt;I gave up. &lt;/p&gt;

&lt;p&gt;But promised myself I’d go deeper into each tool individually when time allowed. &lt;/p&gt;

&lt;h2&gt;
  
  
  Learning Eleventy
&lt;/h2&gt;

&lt;p&gt;A month or so later I started a fresh starter Eleventy project. &lt;/p&gt;

&lt;p&gt;Making some really simple pages and layouts, I learnt about 11ty’s special dirs (like &lt;code&gt;_includes&lt;/code&gt; &amp;amp; &lt;code&gt;_data&lt;/code&gt;), and generally started to feel more comfortable with how Eleventy worked and it’s limitations.&lt;/p&gt;

&lt;p&gt;Those limitations, for me, were the fact that sharing blocks of re-usable components felt difficult. &lt;/p&gt;

&lt;p&gt;You could use 11ty’s &lt;a href="https://www.11ty.dev/docs/shortcodes/" rel="noopener noreferrer"&gt;shortcodes&lt;/a&gt; to create snippets, or &lt;a href="https://www.trysmudford.com/blog/encapsulated-11ty-components/" rel="noopener noreferrer"&gt;nunjuck macros&lt;/a&gt;, but these felt to me like reusing strings of templates was just about ok, but making them configurable using properties or attributes wasn’t a great experience for me. &lt;/p&gt;

&lt;p&gt;Having first used 11ty with Enhance, I’d been drawn to Enhance’s use of Web Components as &lt;a href="https://enhance.dev/docs/learn/starter-project/elements" rel="noopener noreferrer"&gt;reusable elements&lt;/a&gt; - it fit my more recent mental model of using components in &lt;a href="https://github.com/developit/htm" rel="noopener noreferrer"&gt;htm&lt;/a&gt; or react.&lt;/p&gt;

&lt;p&gt;Maybe I gave up too soon in my earlier experiments? &lt;/p&gt;

&lt;p&gt;But then I heard about Webc - I decided to persevere with my original plan, and set about re-building the &lt;a href="https://sinovi.uk" rel="noopener noreferrer"&gt;Si Novi&lt;/a&gt; and &lt;a href="https://martinhicks.net" rel="noopener noreferrer"&gt;martinhicks.net&lt;/a&gt; sites .&lt;/p&gt;

&lt;h2&gt;
  
  
  Webc - now we’re rocking
&lt;/h2&gt;

&lt;p&gt;Webc is a brand new 11ty templating language. &lt;/p&gt;

&lt;p&gt;It uses Web Components and requires Eleventy 2.0 &lt;em&gt;(at time of writing this is pre-release and requires installing &lt;a href="https://www.npmjs.com/package/@11ty/eleventy/v/2.0.0-canary.20" rel="noopener noreferrer"&gt;their canary package&lt;/a&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Using Webc with 11ty is seamless; You can use WebC to build an individual component, for re-use and you can use it for layouts. Basically anywhere you’d expect an 11ty template to work, .webc files work. &lt;/p&gt;

&lt;p&gt;Meaning it works on its own, or can be used within their other templating languages too. And being 11ty you can mix and match templating languages throughout a project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I like this 11ty feature a lot&lt;/strong&gt; - there’s loads of useful 11ty code snippets on gist, on their website, and, well all over the internet written in &lt;a href="https://www.11ty.dev/docs/languages/nunjucks/" rel="noopener noreferrer"&gt;Nunjucks&lt;/a&gt; or &lt;a href="https://www.11ty.dev/docs/languages/liquid/" rel="noopener noreferrer"&gt;Liquid&lt;/a&gt; and choosing to use WebC doesn’t prohibit you from tapping into this rich seam.&lt;/p&gt;

&lt;p&gt;For example I’m using an njk page to create my &lt;code&gt;sitemap.xml&lt;/code&gt; and another to create my RSS &lt;code&gt;feed.xml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//sitemap.njk

---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
&amp;lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&amp;gt;
    {% for page in collections.all %}
        &amp;lt;url&amp;gt;
            &amp;lt;loc&amp;gt;{{ site.url }}{{ page.url | url | replace(r/\/$/, "") }}&amp;lt;/loc&amp;gt;
            &amp;lt;lastmod&amp;gt;{{ page.date.toISOString() }}&amp;lt;/lastmod&amp;gt;
        &amp;lt;/url&amp;gt;
    {% endfor %}
&amp;lt;/urlset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//rss.njk
---json
{
  "permalink": "feed.xml",
  "eleventyExcludeFromCollections": true,
  "metadata": {
    "title": "Martin Hicks - Journal",
    "subtitle": "Martin Hicks is a software developer from Manchester, UK",
    "language": "en",
    "url": "https://martinhicks.net/",
    "author": {
      "name": "Martin Hikcs",
      "email": "hello@martinhicks.net"
    }
  }
}
---
&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
&amp;lt;rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="{{ metadata.url }}" xmlns:atom="http://www.w3.org/2005/Atom"&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;title&amp;gt;{{ metadata.title }}&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;{{ metadata.url }}&amp;lt;/link&amp;gt;
    &amp;lt;atom:link href="{{ permalink | absoluteUrl(metadata.url) }}" rel="self" type="application/rss+xml" /&amp;gt;
    &amp;lt;description&amp;gt;{{ metadata.subtitle }}&amp;lt;/description&amp;gt;
    &amp;lt;language&amp;gt;{{ metadata.language }}&amp;lt;/language&amp;gt;
    {%- for post in collections.articles | reverse %}
    {%- set absolutePostUrl = post.url | absoluteUrl(metadata.url) %}
    &amp;lt;item&amp;gt;
      &amp;lt;title&amp;gt;{{ post.data.title }}&amp;lt;/title&amp;gt;
      &amp;lt;link&amp;gt;{{ absolutePostUrl }}&amp;lt;/link&amp;gt;
      &amp;lt;description&amp;gt;{{ post.data.description | htmlToAbsoluteUrls(absolutePostUrl) }}&amp;lt;/description&amp;gt;
      &amp;lt;pubDate&amp;gt;{{ post.date | dateToRfc822 }}&amp;lt;/pubDate&amp;gt;
      &amp;lt;dc:creator&amp;gt;{{ metadata.author.name }}&amp;lt;/dc:creator&amp;gt;
      &amp;lt;guid&amp;gt;{{ absolutePostUrl }}&amp;lt;/guid&amp;gt;
    &amp;lt;/item&amp;gt;
    {%- endfor %}
  &amp;lt;/channel&amp;gt;
&amp;lt;/rss&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also really like the fact I can &lt;em&gt;just&lt;/em&gt; use WebC as a &lt;a href="https://www.11ty.dev/docs/languages/webc/#html-only-components" rel="noopener noreferrer"&gt;templating language for HTML generation&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This website currently has zero need for any client-side progressive enhancement, so I don’t need any of the web component features in the HTML served to the browser. &lt;/p&gt;

&lt;p&gt;11ty allows you to build a component that just returns HTML, and if so, the generator treats that component output as just the inner HTML omitting the enclosing custom element tag.&lt;/p&gt;

&lt;p&gt;If you did want to keep the component wrapper for some reason you pass an attribute of &lt;code&gt;webc:keep&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//my-avatar.webc
//example html only web component
&amp;lt;picture&amp;gt;
    &amp;lt;source srcset="/images/5F8AB69C-FA08-4C25-B932-74D76EBB7721.webp" type="image/webp"&amp;gt;
    &amp;lt;img src="/images/5F8AB69C-FA08-4C25-B932-74D76EBB7721.jpg" alt="Me and my wife, Helen, lying on the grass in summer. " :width="this.width" :height="this.height" class="max-w-[100px] md:max-w-[200px] mx-auto aspect-square ring-2 ring-zinc-500/40 rotate-45 rounded-full bg-zinc-100 object-cover"/&amp;gt;
&amp;lt;/picture&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webc with 2.0 means I could now build proper re-usable components, configurable with attribute props if required. No more snippets or Nunjucks macros.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;my-avatar width="200" height="200"&amp;gt;&amp;lt;/my-avatar&amp;gt;
&amp;lt;my-avatar width="100" height="100"&amp;gt;&amp;lt;/my-avatar&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect.&lt;/p&gt;

&lt;p&gt;Other wins are;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using web components within the &lt;a href="https://www.11ty.dev/docs/languages/webc/#components" rel="noopener noreferrer"&gt;head element&lt;/a&gt;&lt;/strong&gt; - this isn’t allowed by the Web Component spec I don't think, but given you may have a Webc component that just provides HTML you can use the &lt;code&gt;webc:is&lt;/code&gt; attribute to upgrade a standard element to a WebC component just for templating purposes &lt;em&gt;(but only if it just returns html)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script webc:is="json-ld" &amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above &lt;code&gt;script&lt;/code&gt; tag in my head, will be ran using my component &lt;code&gt;json-ld&lt;/code&gt;, which basically adds some dynamic json-ld for articles on this site, and excludes if it's not an article page. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebC components can include render only Js functions to iterate collections&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
meta:
title: "Articles"
description: "Occasional thoughts"
pagination:
  data: collections.articles
  size: 10
  alias: articles
  reverse: true
layout: layouts/main.webc
---


&amp;lt;container&amp;gt;
  &amp;lt;div class="flex flex-col mx-auto justify-center "&amp;gt;
    &amp;lt;h1 class="text-4xl font-bold tracking-tight text-zinc-800  sm:text-5xl mt-4"&amp;gt;
      Journal
    &amp;lt;/h1&amp;gt;

    &amp;lt;div class="grid grid-cols-1 gap-8 md:grid-cols-2 auto-cols-auto md:auto-rows-[1fr] mb-8"&amp;gt;
      &amp;lt;script webc:type="render" webc:is="template"&amp;gt;
        function () {
          //console.log(this.pagination)
          let articles = this.pagination.items;

          return articles.map((article, idx) =&amp;gt; /*html*/`

            &amp;lt;div class="prose relative pb-8"&amp;gt;
                    &amp;lt;a href="${article.data.url}"&amp;gt;
                        &amp;lt;picture class="flex w-full " &amp;gt;
                            &amp;lt;source srcset="${article.data.image.webp}" type="image/webp"&amp;gt;
                            &amp;lt;img class=" full-width mb-2" src="${article.data.image.path}" width="345" height="236" alt="${article.data.image.alt}"&amp;gt;
                        &amp;lt;/picture&amp;gt; 
                    &amp;lt;/a&amp;gt;
                    &amp;lt;span class="!text-sm"&amp;gt;
                    ${article.data.date}
                    &amp;lt;/span&amp;gt;
                    &amp;lt;h1 class="!text-xl mb-2 font-semibold"&amp;gt;&amp;lt;a class=" no-underline" href="${article.data.url}"&amp;gt;${article.data.title}&amp;lt;/a&amp;gt;&amp;lt;/h1&amp;gt;
                    &amp;lt;p&amp;gt;${article.data.description}&amp;lt;/p&amp;gt;
                    &amp;lt;a class="absolute bottom-2 " href="${article.data.url}"&amp;gt;
                        Read the article
                    &amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;

                    `)
          .join("");
        }
      &amp;lt;/script&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;hr&amp;gt;

  &amp;lt;/div&amp;gt;

  &amp;lt;my-details mode="full"&amp;gt;&amp;lt;/my-details&amp;gt;
&amp;lt;/container&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slots&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve used any Web Component tool or manually created your own, you’ll know that web components use ‘slots’ to control where nested elements or strings are displayed within the component template.&lt;/p&gt;

&lt;p&gt;They’re super useful and help direct content to the correct placeholder without using attributes or similar.&lt;/p&gt;

&lt;p&gt;eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//social-link.webc

&amp;lt;a class="group -m-1 p-1" :href="href" target="_blank" :aria-label="this.arialabel" :role="this.role" :rel="this.rel"&amp;gt;
    &amp;lt;div class="flex items-center space-x-2"&amp;gt;
        &amp;lt;slot name="icon"&amp;gt;&amp;lt;/slot&amp;gt;
        &amp;lt;slot name="content"&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is usable like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;social-link rel="me" role="listitem" href="https://indieweb.social/@martinhicks"&amp;gt;
    &amp;lt;icon-mastodon slot="icon" class="w-8 h-8"&amp;gt;&amp;lt;/icon-mastodon&amp;gt;
    &amp;lt;span slot="content"&amp;gt;Follow on Mastodon&amp;lt;/span&amp;gt;
&amp;lt;/social-link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;nb icon-mastodon is another webc component - completely nest-able as you'd expect&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to look out for in 2.0 / webc
&lt;/h2&gt;

&lt;p&gt;Having not been a long time user of 11ty, I’ll leave any deep comparison between the two versions to seasoned experts. &lt;/p&gt;

&lt;p&gt;There’s loads of new features in 11ty 2.0, some of which are breaking changes. &lt;/p&gt;

&lt;p&gt;Their docs site does a good job of signposting these changes, and I’m sure when it’s released there will be loads written to guide users in migration.&lt;/p&gt;

&lt;p&gt;What I’ve found:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Webc: Script and link tags&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since, I think, &lt;code&gt;"@11ty/eleventy": "2.0.0-canary.18"&lt;/code&gt; or &lt;code&gt;"@11ty/eleventy-plugin-webc": "0.8.0"&lt;/code&gt;, you’ve been required to add &lt;code&gt;webc:keep&lt;/code&gt; to any script or link tag that has an external src. &lt;/p&gt;

&lt;p&gt;Thankfully the CLI warns you of these during build, throwing an error.&lt;/p&gt;

&lt;p&gt;However, I found that several non external script tags in my head (two json-ld and the local google analytics gtag script) weren’t included in my published site for a week.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;webc:keep&lt;/code&gt; brought them back. &lt;/p&gt;

&lt;p&gt;Maybe I misread the CLI warning, but I don’t think I did. And certainly the lack of them on an external src caused a build fail, whereas omitting them for locally src’d elements didn’t.&lt;/p&gt;

&lt;p&gt;Luckily I don’t care much about either of those on my personal site, so no biggie. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;__**Given that I’m using a canary build and I haven’t been studiously keeping up with the change notes between pre-release build versions I’ll take the blame on this one.&lt;/em&gt;&lt;em&gt;**&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;11ty 2.0 - The copy command doesn’t actually copy files locally during dev&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is new and intentional, for performance reasons, it sort of magically symlinks them internally during the local serve process. &lt;/p&gt;

&lt;p&gt;So as far as the browser is concerned the images folder you’ve set to copy to the output dir, for example, and therefore is served from &lt;code&gt;/images/myimage.jpg&lt;/code&gt;, hasn’t physically been copied to that location on your machine. That only happens during a production build.&lt;/p&gt;

&lt;p&gt;Fine when you know but it’s a little confusing at first.&lt;/p&gt;

&lt;p&gt;I’ve had to build a few production builds locally at times just to make sure my copy configs are set correctly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Plug-ins have a whole bunch of new hooks&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a big upgrade and I think will make integrating other tools way easier. &lt;/p&gt;

&lt;p&gt;I think it's backwards compatible. I’m looking forward to playing around with this more. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WebC components aren’t automatically discoverable within a project by default.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I found this confusing.&lt;/p&gt;

&lt;p&gt;Especially as most of my &lt;code&gt;.webc&lt;/code&gt; components were simply returning HTML, I didn’t want to have to add a load of &lt;code&gt;webc:import&lt;/code&gt; attributes per component.&lt;/p&gt;

&lt;p&gt;Thankfully 11ty’s config system has you covered. &lt;/p&gt;

&lt;p&gt;Adding the following to your &lt;code&gt;.eleventy.js&lt;/code&gt; file, and placing all your components within &lt;code&gt;/_includes/components&lt;/code&gt; makes them usable throughout the entire project without individually importing them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eleventyConfig.addPlugin(pluginWebc, {
    // Glob to find no-import global components
        components: "src/_includes/components/**/*.webc",
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Everything in project root by default&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I think this is right, and not just a mistake I made. But the default 11ty starter had everything configured to run from the project root.&lt;/p&gt;

&lt;p&gt;Eg, &lt;code&gt;index.webc&lt;/code&gt; was in the same root folder as &lt;code&gt;package.json&lt;/code&gt; and other non web assets.&lt;/p&gt;

&lt;p&gt;I didn’t like this. &lt;/p&gt;

&lt;p&gt;Again, 11ty config to the rescue, it’s super simple to tell 11ty where your input and output dirs should be. So it was quick get things how I wanted them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return {
  dir: {
      input: "src",
      output: "_site"
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’d be great if the starter cli command could ask you where you’d like your source and output directories to be, and auto configure this for newbies like me. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tailwind&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tailwind works really well with a component based system, so 11ty and webc is no different, outputting only the css for classes that you’ve actually used in your pages.&lt;/p&gt;

&lt;p&gt;To make that work better, I pointed tailwind config to the output folder &lt;em&gt;(&lt;code&gt;_site&lt;/code&gt; in my case)&lt;/em&gt; to look for content, rather than the src directory, and made it run after 11ty prod build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//tailwind.config.js
module.exports = {
  content: ['./_site/**/*.html'],
  plugins: [require('@tailwindcss/typography')]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way any draft components I’ve built but not yet used, won’t have any of its unique classes included in the final production css output. Until they’re actually included in one of the html pages.&lt;/p&gt;

&lt;p&gt;I also needed a way to allow component modification throughout the site (more so on Si Novi). To do that I used the Tailwind Merge &lt;a href="https://www.npmjs.com/package/tailwind-merge" rel="noopener noreferrer"&gt;package&lt;/a&gt; and created a helper function within &lt;code&gt;.eleventy.js&lt;/code&gt; that can be used on each component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eleventyConfig.addFilter("tailwindMerge", function(defaultClasses, overrideClasses) { 
  return twMerge(defaultClasses, overrideClasses)
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means I can pass in overriding css attributes at the point of using the component and the &lt;code&gt;twMerge&lt;/code&gt; function (as it’s tailwind aware), replaces the defaults with the overrides.&lt;/p&gt;

&lt;p&gt;Within a WebC component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//link-primary.webc
&amp;lt;a :href="href" :class="tailwindMerge('underline hover:text-blue-500', this.class)"&amp;gt;&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I used this component, like so...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link-primary class="hover:text-red-500"&amp;gt;example link hovers red&amp;lt;/link-primary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...the hover on that instance of the component would be red. &lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;I’m so glad I’ve found 11ty, it’s a great tool. Version 2.0 and WebC has made it sticky for me. &lt;/p&gt;

&lt;p&gt;I haven’t used any of the more advanced webc features such as scoped css or bundling, but I’m sure I’ll test them out in due course.&lt;/p&gt;

&lt;p&gt;Absolutely great job. &lt;/p&gt;

&lt;p&gt;Keep an eye out for the next post in this series, which explains how to set up 11ty, AWS and GitHub actions to create a CI/CD build pipeline to deploy to S3. &lt;/p&gt;

&lt;p&gt;You can view the &lt;a href="https://github.com/hicksy/martinhicks.net" rel="noopener noreferrer"&gt;source code of my site here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webc</category>
      <category>11ty</category>
      <category>eleventy</category>
      <category>staticsite</category>
    </item>
    <item>
      <title>Sharing Enhance elements between projects</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Thu, 15 Dec 2022 17:00:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai</link>
      <guid>https://dev.to/martinhicks/sharing-enhance-elements-between-projects-3aai</guid>
      <description>&lt;p&gt;This post introduces a new arc plugin for Enhance projects that I've recently published, with the catchy name &lt;a href="https://www.npmjs.com/package/@hicksy/shared-enhance-components-plugin" rel="noopener noreferrer"&gt;shared-enhance-components-plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read on for details about how to use the plugin and why I came to write it. &lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;While I &lt;a href="https://dev.to/articles/back-to-the-future"&gt;continued my exploration&lt;/a&gt; of the &lt;a href="https://enhance.dev" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt; framework, I started to wonder how you'd go about importing elements from a shared library or UI toolkit. &lt;/p&gt;

&lt;p&gt;In a React project, you'd import the library into the given component and then begin using its JSX tag within the render. &lt;/p&gt;

&lt;p&gt;However, Enhance elements are dependency free by design, for good reasons. &lt;/p&gt;

&lt;p&gt;So instead, any element you create within &lt;code&gt;/app/elements&lt;/code&gt; or define within &lt;code&gt;/app/elements.mjs&lt;/code&gt; &lt;em&gt;(* more on this later)&lt;/em&gt;, becomes automatically available in any Enhance page.&lt;/p&gt;

&lt;p&gt;That doesn't mean you can't use dependencies where appropriate. Let's say you have a couple of elements, one of which can be used standalone, the other you always want a particular element included, you could do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//app/elements/csrf-input

export default function CsrfInput({ html, state }) {
    const { attrs={}, store={} } = state
    const { name = 'csrf' } = attrs
    return html`
        &amp;lt;input type="hidden" name="${name}" value="${store.csrf_token}" /&amp;gt;
    `
}

import CsrfInput from "./csrf-input.mjs";

export default function CsrfForm({ html, state }) {
    const { attrs={} } = state
    const { action = '', method = '' } = attrs

    return html`
    &amp;lt;form action="${action}" method="${method}"&amp;gt;
        ${CsrfInput({html, state})}
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/form&amp;gt;
`
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can simply call the pure function to return its "render", e.g. &lt;code&gt;${CsrfInput({html, state})}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And I think looking at my browser's source, it means the dependent element is just the HTML entity, rather than being expanded into a web component. Which in this case is what I wanted, all be it by accident. &lt;/p&gt;

&lt;p&gt;Pretty cool. &lt;/p&gt;

&lt;p&gt;But what about if you want to bring in a shared UI library maintained by a different team or 3rd party?&lt;/p&gt;

&lt;p&gt;After a search on the &lt;a href="https://enhance.dev/discord" rel="noopener noreferrer"&gt;Enhance Discord&lt;/a&gt; &lt;em&gt;(which is great by the way - definitely head there for help / guidance / cool things people are building)&lt;/em&gt;, I couldn't find anything baked in. &lt;/p&gt;

&lt;p&gt;I did see a comment from Macdonst (one of the begin/enhance team), who was explaining how you could do all of your imports in an &lt;code&gt;/app/elements.mjs&lt;/code&gt; file, and then they would be available throughout your app just like a first-party element you've created in &lt;code&gt;/app/elements&lt;/code&gt; - nice. &lt;/p&gt;

&lt;p&gt;He went on... &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The above is begging to be a plug-in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I thought why not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is this elements.mjs file anyway?
&lt;/h2&gt;

&lt;p&gt;It's not mentioned much in their docs at all. In fact, at the time of writing, it's only referenced within the &lt;a href="https://enhance.dev/docs/learn/deployment/fastify" rel="noopener noreferrer"&gt;Deploy to Fastify section&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;However, if you look at the code for the &lt;code&gt;arc-plugin-enhance&lt;/code&gt; &lt;em&gt;(which is the plugin that orchestrates arc to have a catch-all lambda for each /app/api/ route, among many other things)&lt;/em&gt;, you'll see that 4 locations are checked for elements to enhance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let pathToModule = join(basePath, 'elements.mjs')
let pathToPages = join(basePath, 'pages')
let pathToElements = join(basePath, 'elements')
let pathToHead = join(basePath, 'head.mjs')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i.e. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/app/elements.mjs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/app/pages&lt;/code&gt; (any mjs file in here)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/app/elements&lt;/code&gt; (any mjs file in here)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/app/head.mjs&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The elements.mjs file is basically returning a keyed object mapping the tag name (e.g &lt;code&gt;my-component&lt;/code&gt;) to a corresponding element function (e.g. MyComponent) (which can be located anywhere, like &lt;code&gt;node_modules/some-package/elements/MyComponent.mjs&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import MyComponent from 'some-package/elements/MyComponent.mjs'

let elements = {

  'my-component': MyComponent
}

export default elements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So that's a pretty powerful in-built mechanism for linking things together right there. &lt;/p&gt;

&lt;p&gt;The need for a potential plugin is that you may &lt;code&gt;npm install&lt;/code&gt; a package with 10s, 100s of components you'd like to use, so having a plugin do most of the leg work is helpful. &lt;/p&gt;

&lt;p&gt;Also, defining a map between the tag name and pure function doesn't seem to have any draw back - I'm fairly certain elements are only processed if they're referenced within a page and / or element - so mapping a lot of potentially unused components seems to have no issues&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the plugin
&lt;/h2&gt;

&lt;p&gt;Enhance (the way I'm using it), is wrapped with &lt;a href="https://arc.codes" rel="noopener noreferrer"&gt;Architect&lt;/a&gt;. Meaning you can easily deploy your enhance app to &lt;a href="https://aws.amazon.com/lambda/" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;, or to &lt;a href="https://begin.com" rel="noopener noreferrer"&gt;Begin&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Because it uses Architect's sandbox to run locally, and its hydration mechanism to hydrate your Lambda function code before deployment, you can also tap into Arc's powerful plug-in system. &lt;/p&gt;

&lt;p&gt;I'd previously had a go at an Architect plugin and got a little lost. Since then &lt;em&gt;(18 months or so ago)&lt;/em&gt;, they've massively &lt;a href="https://arc.codes/docs/en/guides/plugins/overview" rel="noopener noreferrer"&gt;expanded their plug-in docs&lt;/a&gt; and improved the available lifecycle hooks you can tap into. &lt;/p&gt;

&lt;p&gt;I also had a good look at some of the &lt;a href="https://github.com/architect/plugins" rel="noopener noreferrer"&gt;plugins listed in their repository&lt;/a&gt;, which helped me figure things out. &lt;/p&gt;

&lt;p&gt;After an afternoon or so's work I had an acceptable &lt;em&gt;(just about)&lt;/em&gt; version of a plugin which works as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You add a component package or UI library to the .arc file under the plugin's unique @ pragma - &lt;code&gt;@shared-enhance-components-plugin&lt;/code&gt; - this informs the plugin which external packages to import component elements from.&lt;/li&gt;
&lt;li&gt;On arc hydration (which happens during sandbox start, and pre arc deploy), the plugin analyses the available functions that can be imported from the given package:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- either from a specific folder, in which case each .mjs / js file will be mapped into elements.mjs
- or from an index.js file, in which case each named export will be mapped into elements.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Tag names are inferred by de-camelcasing the function name&lt;/li&gt;
&lt;li&gt;Any existing elements you've manually added to elements.mjs will be preserved &lt;em&gt;(auto generated lines include an eol comment)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Example usage
&lt;/h2&gt;

&lt;p&gt;I think it's pretty easy to use. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First install the plugin and tie it into your project's arc file.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @hicksy/shared-enhance-components-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//.arc

@app
myproj

@plugins
enhance/arc-plugin-enhance
hicksy/shared-enhance-components-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;nb: you have to drop the preceding @ on scoped package names in the .arc file so they don't collide with the pragma names&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install some shared elements&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, we could install these example Enhance form elements - &lt;a href="https://github.com/enhance-dev/form-elements.git" rel="noopener noreferrer"&gt;https://github.com/enhance-dev/form-elements.git&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install git+https://github.com/enhance-dev/form-elements.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Tell the plugin to use this package
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//.arc

@app
myproj

@plugins
enhance/arc-plugin-enhance
hicksy/shared-enhance-components-plugin

@shared-enhance-components-plugin
hicksy/enhance-csrf 'elements'
enhance/form-elements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note in the above example the package we've just installed is referenced just by it's name &lt;em&gt;(dropping the @ again to avoid collision with arc pragmas)&lt;/em&gt; - we know by looking at this package all of it's components are named exports &lt;a href="https://github.com/enhance-dev/form-elements/blob/main/index.js" rel="noopener noreferrer"&gt;from a route index.js file&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;For &lt;a href="https://www.npmjs.com/package/@hicksy/enhance-csrf" rel="noopener noreferrer"&gt;@hicksy/enhance-csrf&lt;/a&gt; package, we pass a second arg to the plugin, this tells the plugin that the components are individual files within &lt;a href="https://github.com/hicksy/enhance-csrf/tree/main/elements" rel="noopener noreferrer"&gt;the specific folder&lt;/a&gt;. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;npm start&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The plugin-hook will fire, and the &lt;code&gt;/app/elements.mjs&lt;/code&gt; file will be auto-populated with the tag-name's and import declarations as required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /app/elements.mjs

import CsrfInput from '@hicksy/enhance-csrf/elements/csrf-input.mjs' //automatically inserted by shared-enhance-components-plugin
import CsrfForm from '@hicksy/enhance-csrf/elements/csrf-form.mjs' //automatically inserted by shared-enhance-components-plugin
import { CheckBox } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { FieldSet } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { FormElement } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { LinkElement } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { PageContainer } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { SubmitButton } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { TextInput } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin

let elements = {

  'csrf-input': CsrfInput, //automatically inserted by shared-enhance-components-plugin
  'csrf-form': CsrfForm, //automatically inserted by shared-enhance-components-plugin
  'check-box': CheckBox, //automatically inserted by shared-enhance-components-plugin
  'field-set': FieldSet, //automatically inserted by shared-enhance-components-plugin
  'form-element': FormElement, //automatically inserted by shared-enhance-components-plugin
  'link-element': LinkElement, //automatically inserted by shared-enhance-components-plugin
  'page-container': PageContainer, //automatically inserted by shared-enhance-components-plugin
  'submit-button': SubmitButton, //automatically inserted by shared-enhance-components-plugin
  'text-input': TextInput //automatically inserted by shared-enhance-components-plugin
}

export default elements

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Publishing to NPM
&lt;/h2&gt;

&lt;p&gt;I've never published a package to NPM before so this was all new to me.&lt;/p&gt;

&lt;p&gt;Previous node packages I've created have all been private and we'd opted to npm installing from the git repo address rather than purchasing a private packages account from npm. &lt;/p&gt;

&lt;p&gt;Publishing was really straight forward (aside from accidentally deploying it unscoped, and then deciding I'd prefer to release it scoped to my username).&lt;/p&gt;

&lt;p&gt;I followed this &lt;a href="https://zellwk.com/blog/publish-to-npm/" rel="noopener noreferrer"&gt;guide to npm publishing&lt;/a&gt; - thanks Zell!&lt;/p&gt;

&lt;h2&gt;
  
  
  And there you go...
&lt;/h2&gt;

&lt;p&gt;A small plugin that hopefully eases the use of shared Enhance elements and hopefully making it more compelling for authors to share libraries of their elements for others to use?&lt;/p&gt;

&lt;p&gt;There's no doubt a few wrinkles will be discovered, and the plugin code could do with a tidy, but I'm pretty happy with it. &lt;/p&gt;

&lt;p&gt;Get in &lt;a href="https://github.com/hicksy/hicksy-shared-enhance-components-plugin" rel="noopener noreferrer"&gt;touch on GitHub&lt;/a&gt; if there's any issues, improvements or feature ideas.  &lt;/p&gt;




</description>
      <category>enhance</category>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>progressiveenhancement</category>
    </item>
    <item>
      <title>Back to the Future</title>
      <dc:creator>Martin Hicks</dc:creator>
      <pubDate>Mon, 12 Dec 2022 17:12:00 +0000</pubDate>
      <link>https://dev.to/martinhicks/back-to-the-future-2coj</link>
      <guid>https://dev.to/martinhicks/back-to-the-future-2coj</guid>
      <description>&lt;p&gt;As 2022 draws to a close I wanted to reflect on a welcome change in the web framework world that shifts the needle back towards web-first principles. Which I'm all here for. &lt;/p&gt;

&lt;p&gt;This year I’ve worked on a large web application using &lt;a href="http://remix.run" rel="noopener noreferrer"&gt;Remix&lt;/a&gt;, and the past week I’ve finally had free time to properly explore &lt;a href="https://enhance.dev" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt; - something I've been super excited about since it was announced back in late Summer 2022.&lt;/p&gt;

&lt;p&gt;Both frameworks share core principles;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;web-platform aligned mechanism for storing state &lt;em&gt;(i.e forms, sessions and http verbs - those foundational web principles that somehow had been forgotten)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;progressive enhancement; a web app can &lt;em&gt;(and should)&lt;/em&gt; be usable without browser JS&lt;/li&gt;
&lt;li&gt;HTML served over-the-wire&lt;/li&gt;
&lt;li&gt;file based routing &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact as an industry we allowed ourselves to sleep-walk into a period where these principles weren't the norm is unbelievable. &lt;/p&gt;

&lt;p&gt;Thankfully a long overdue course correction is taking place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remix
&lt;/h2&gt;

&lt;p&gt;I really enjoyed building with Remix - it's well thought out, and makes working with React, well... more &lt;em&gt;enjoyable&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;We were able to scaffold out a broad approach to the app pretty seamlessly, and the build out was relatively pain free. We deployed the app to AWS using what Remix call their &lt;a href="https://github.com/remix-run/grunge-stack" rel="noopener noreferrer"&gt;Grunge stack&lt;/a&gt; - which basically pairs Remix with &lt;a href="https://arc.codes" rel="noopener noreferrer"&gt;Architect&lt;/a&gt; &lt;em&gt;(a tool I'm already very familiar with)&lt;/em&gt; and provides a catch-all Lambda to power the server-side code.&lt;/p&gt;

&lt;p&gt;I believe Remix can be deployed anywhere that supports the Node.js runtime, and I think it even runs at the edge on web-workers - which is pretty cool.&lt;/p&gt;

&lt;p&gt;It's amazing how much cruft Remix helps take away from a typical React project- no more nonsense client side Redux stores, optimistic UI is extremely easy (no loading spinners), and with the file based routing it's far simpler to move things around as you're building out an app. &lt;/p&gt;

&lt;p&gt;It's not without complexity, &lt;em&gt;it's still React, right?&lt;/em&gt; You have to be pretty disciplined to not over engineer things, particularly the functional component, as you can get drawn to using &lt;em&gt;(or overusing in my case)&lt;/em&gt; hooks etc to maintain state client-side rather than using the platform to best effect. &lt;em&gt;(Aware this is my issue not necessarily Remix's)&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;A lot to like and definitely looking forward to using it again in 2023. &lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance
&lt;/h2&gt;

&lt;p&gt;For those new to it, &lt;a href="https://enhance.dev" rel="noopener noreferrer"&gt;Enhance&lt;/a&gt; is a standards based web framework underpinned by Web Components from the folks at &lt;a href="https://begin.com" rel="noopener noreferrer"&gt;Begin&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;So far I've only had a chance to test out some very basic examples, but really like what I've seen, both in terms of playing around with it and the team's core mission.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our mission is to enable anyone to build multi-page dynamic web apps while staying as close to the platform as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;YES!! 🙌 &lt;/p&gt;

&lt;p&gt;I'll write up another post soon I'm sure, but here's a quick summary of wins:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No need for client-side code
&lt;/h3&gt;

&lt;p&gt;Components &lt;em&gt;(or Elements as enhance refers to them)&lt;/em&gt;, can be &lt;em&gt;just&lt;/em&gt; HTML if you like, there's no need for any client-side JS at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function ProseHeader({ html, state }) {
  const { attrs } = state
  const { title = '' } = attrs

  return html`
    &amp;lt;h1&amp;gt;&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&amp;lt;/h1&amp;gt;
  `
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Element would be usable in any Enhance page like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prose-header&amp;gt;Article title&amp;lt;/prose-header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to the browser, would be served up expanded ready for enhancement if required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;prose-header&amp;gt;
&amp;lt;h1&amp;gt;Article title&amp;lt;/h1&amp;gt;
&amp;lt;/prose-header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I wish were possible &lt;em&gt;(and it may well be, I just haven't looked hard enough)&lt;/em&gt;, is whether you could opt-out of the web component parent element all together for elements that don't require client-side enhancement&lt;br&gt;
&lt;em&gt;i.e. they only contain HTML&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;This is a feature I quite like in &lt;a href="https://www.11ty.dev/docs/languages/webc/#html-only-components" rel="noopener noreferrer"&gt;11ty's webc&lt;/a&gt; - meaning if you're using webc as only a templating language, the outputted HTML is just pure HTML. &lt;/p&gt;

&lt;p&gt;Like I say, I'm not sure if this is possible or not. Nor is it much of a pain point to be honest, just a nicety. &lt;/p&gt;
&lt;h3&gt;
  
  
  2. Progressive enhancement
&lt;/h3&gt;

&lt;p&gt;Easy to progressively enhance client-side capabilities by adding a script into the string returned from the Element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...rest of Element
&amp;lt;script type="module"&amp;gt;
  class ProseHeader extends HTMLElement {
    constructor() {
      super()
      this.heading = this.querySelector('h1')
    }

    ...whatever powers you want to give the new Web Component
  }

  customElements.define('prose-header', ProseHeader)
&amp;lt;/script&amp;gt;
...etc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Routing
&lt;/h3&gt;

&lt;p&gt;File based routing, with an API mechanism backed in.&lt;/p&gt;

&lt;p&gt;For example, let's say you have the following page &lt;code&gt;/app/pages/index.html&lt;/code&gt;, and the following api route &lt;code&gt;/app/api/index.mjs&lt;/code&gt; the returned JSON from either the &lt;code&gt;get&lt;/code&gt; handler, or &lt;code&gt;post&lt;/code&gt; handler of the API will be made available to any custom elements within index.html automatically. &lt;/p&gt;

&lt;p&gt;The server-side mechanism automatically routes to the &lt;code&gt;get&lt;/code&gt; handler if the request method is &lt;code&gt;GET&lt;/code&gt; and likewise to the &lt;code&gt;post&lt;/code&gt; handler if the request is a &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is similar in nature to Remix's actions (post) and loaders (gets), but split across different files. &lt;/p&gt;

&lt;h3&gt;
  
  
  4. Automatic state
&lt;/h3&gt;

&lt;p&gt;Server side state is populated into every custom element for a given route automatically &lt;/p&gt;

&lt;p&gt;I think this is really powerful. No need to drill props, no need to maintain some sort of client-side state, if it's returned in the &lt;code&gt;json&lt;/code&gt; prop from either a &lt;code&gt;get&lt;/code&gt; or &lt;code&gt;post&lt;/code&gt; handler it will be available in &lt;code&gt;state.store&lt;/code&gt; in every element on the current page. &lt;/p&gt;

&lt;p&gt;What a joy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Overall two very interesting frameworks that I'm delighted exist. &lt;/p&gt;

&lt;p&gt;They've both, for different reasons, helped me get excited about web development again and travelling back to the future. &lt;/p&gt;




</description>
      <category>webplatform</category>
      <category>webframeworks</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
