How I Built Yarrow
Yarrow is a Tarot reader that runs on Astro, Tailwind, and Svelte, with an LLM doing the reading.
Tarot’s had me for a few years. So I built Yarrow, a three-card reader.
Most spreads run past, present, future. Yarrow runs Root, Stem, Bloom: what’s beneath, where it stands, what it’s becoming.
Past, present, future never sat right with me. It treats time like a straight line you’re stuck on. A reading should feel like growth, not a sentence being read out.
So I stole the structure from a plant. Root holds what you don’t see. Stem is the part doing the work right now. Bloom is the thing you’re turning into, whether you meant to or not. Same three-card logic, better metaphor.
Also I wanted to build something. Tarot was the excuse.
The Stack
I wanted tooling I half-knew and wanted to know better. Astro for the structure, Tailwind for the styling, Svelte for the moving parts.
I’ve been on Astro about two years, since Henry recommended it at a 1RG Side Project Social. It’s fast to pick up and faster to get going. Yarrow was started with understory, my own Astro template, so most of the setup was already done.
Tailwind has taken over enough of my day-to-day that I’ve forgotten half my raw CSS. I spend less time on syntax and more on how things should behave.
Svelte handles the few interactive pieces on the site: the welcome widget on the landing page and the /where page. I left Vue behind when the 2-to-3 migration started to feel like learning a new framework anyway. If I’m relearning something, it might as well be new.
React was the other option. It carried more overhead than I wanted, and Svelte’s syntax is close enough to Vue that the switch was cheap.
The Design
I wanted the site simple. The cards are the whole point, so everything else gets out of the way. I opened Figma and sketched what I was after.
Figma sketch of three stacked card outlines labeled Root, Stem, Bloom on a blank canvas.
I keep meaning to do more in Figma, sketch through to final design, but it boxes me in. I’d rather work in the browser, where I can see and click the thing the way a visitor will. So I sketched once and went straight to building.
First deployed Yarrow: three plain cards on a white page, no styling.
The first deploy was bare bones. It drew a spread on page load, and a new spread meant refreshing the page. Fine as an MVP, not what I wanted it to be.
Yarrow’s final landing page: three cards on a warm parchment background.
I was going for a garden aesthetic. Didn’t quite get there. The parchment’s better anyway.
The Build
Pulling a spread needed to generate fresh content on click. I used Astro Actions for that, Astro’s way of writing backend functions.
Card detail modal open over the spread, showing one card’s full meaning.
The action takes a spread type and a deck. Right now it only does the three-card spread and the Rider–Waite deck, since the Internet Archive had a clean high-res scan of it. The spread-type argument is there for later, in case I ever build out the Celtic Cross.
The Svelte component calls the action and lays out the cards, each with a CSS flip I wrote so the draw has a bit of motion.
Final version of Yarrow with the card details modal open.
Click any card to go deeper on its meaning. The card data lives in Astro Content Collections, one entry per card.
LLM Integration
I wanted Yarrow to offer a basic reading, and I wanted a project with a user-facing LLM integration. Until now I’d only ever called an LLM inside a long-running task, never anything a person was waiting on in real time.
Yarrow with the reading modal open, generated text filling the panel beside the three cards.
Prompt injection had me wary, so there’s no free-text field anywhere. The model only gets structured input: each card’s position (Root, Stem, Bloom), whether it’s reversed, the card name, and its upright and reversed meanings.
The system message tells the model it’s Yarrow, explains the Root/Stem/Bloom metaphor, and asks it to read the spread as a growing plant: a reflection, not a prediction.
The user message looks like this:
The spread:
Root — Devil (reversed): Breaking free, recognising what binds you, or releasing an unhealthy pattern.
Stem — Five of Swords (upright): The Five of Swords asks what the victory actually cost. Winning through deception or cruelty leaves something broken that doesn't easily repair.
Bloom — King of Pentacles (reversed): Corruption, materialism, or stability that has become stagnation.
I wanted the text to stream in so it reads as coming straight from the model, which meant reaching for an Astro Endpoint. Between this, the Actions, and the Svelte components, I leaned on Astro’s Islands architecture for most of the project.
Yarrow’s live at yarrow.myles.garden. The thing I set out to build is the thing that runs: a reading that feels like growth, not a verdict.
The reading sits behind a flag I can flip off. If the LLM ever stops earning its place, the cards still pull and the meanings still hold, the generated reading just goes quiet. I’d rather the feature be optional than load-bearing.
I’d structure the LLM input the same way again. The garden look I’d push further. And the spread-type argument is still sitting there, waiting for a Celtic Cross.