
Digital Tide, flagship engagement
That Paleo Chick
A Ghost publication with a theme I built, a content pipeline that fills it, an iOS app that reads the same structured data the theme renders, and the infrastructure all three run on. I designed it, I operate it, and I am the person who gets paged.
- Foundation
- Ghost 6, on a theme I build
- Custom templates
- Sixteen
- One recipe, rendered by
- Search, the site, and the app
- App status
- In TestFlight, submitted for review
This is a Ghost site first. Everything else here exists to serve it: the pipeline creates drafts in Ghost, the social scheduler distributes what Ghost publishes, and the iOS app reads Ghost through its content API. If you want to understand the system, start with the theme, because the theme is where the decision that shaped everything else got made.
That decision was to make a recipe one piece of structured data rather than one page. Everything after it follows.
The theme
01
One recipe feeds search, the site and the app
Every recipe post carries a schema.org Recipe block: ingredients, method, times, yield, nutrition. Three separate consumers read that same block. Search engines read it for rich results, which is the entire organic acquisition strategy for a recipe publication. The theme reads it in the browser to build the recipe card a reader actually cooks from. And the iOS app parses it out of the content API to render the recipe natively. One canonical representation, three renderers, and no second database anywhere. Change an ingredient and it changes in all three, because there is only one place it was ever written.
02
Recipe cards render from structured data, not post fields
The recipe card in the theme is a Handlebars partial with no content in it at all. It is structure, hydrated in the browser from the structured data. That is not a cute trick, it is what makes the previous point true: if the template rendered the ingredients from post fields, the post would be the source of truth and the structured data would be a copy that drifts.
03
Parsing schema.org when the real data is messy
Schema in the wild is looser than the specification suggests. The type can be a plain string or an array, and the block can be a single node, a graph, or a bare top-level array. All of those are valid and all of them occur, so the parser accepts every one. If the card lands on a page whose structured data is missing or malformed, it removes itself rather than showing a reader empty Ingredients and Instructions headings above a Print button that prints nothing. A missing card is better than a broken one.
04
Sixteen custom templates on a commercial base theme
The base is a commercial Ghost theme, and I want to be straight about that because the work worth describing is not the typography. It is the sixteen custom templates on top of it: recipe posts, meal plan posts, guided program posts, the hubs those two sit in, the membership and premium landing pages, sign-in, sign-up and account, the tag archive, and several post header treatments. Around 125 commits, roughly 1,700 lines of JavaScript and 5,000 lines of stylesheets.
05
Category index: working around a 100-row query limit
Ghost's template query returns at most 100 rows and ignores a request for more, with no pagination and no warning. The publication carries about 490 tags, so the category index chunks its query by first letter using starts-with filters, keeping every block under the cap and stating the limit explicitly in each one rather than relying on a silent default.
The pipeline
The brief was a publication that could keep a real posting cadence across the website and six social networks without a staff. The obvious build is a bot that writes and publishes. I did not build that, and the reason is the whole point of the system.
An automated publisher that is wrong is not embarrassing in the moment. It is embarrassing for as long as it takes somebody to notice, on a site with real readers and a real person's name on it. So the pipeline automates everything up to the decision and nothing past it.
The content pipeline
What the machine cannot do
A scheduled run cannot publish.
Every orchestrator ends at a draft. There is no code path from the cron trigger to a live post. The only thing that publishes is a person pressing a button.
Publishing cannot email the subscriber list.
The publish function is web only and does not carry the parameter that mails anyone. A second, separate function is the only thing that can mail the list, and its single caller is a human sending the weekly recap. It is two functions rather than one function with a flag, precisely because a flag is one typo away from mailing every subscriber on every recipe.
Being in the room is not authorization.
Every button that publishes, spends on model calls or edits a post checks an explicit allowlist and refuses everyone else. Membership of the workspace is not a trust boundary, and treating it as one means the access control list is whoever happened to get invited to a channel.
Nothing reaches social until the captions have been read.
Scheduling generates per-network copy and then stops on a preview. Nothing is created in the scheduler until somebody confirms it, and any network that already carries that post is pre-unchecked so the same thing cannot go out twice.
One taxonomy, held in place by a test
Four surfaces name a category: the site, the newsletter, social routing, and the theme's own archive template. All four read one file, which is the single source of truth for the whole taxonomy. The contract is strict because routing depends on it. A post carries exactly one section tag and it has to be first, because the first tag is what distribution keys on.
A drift test asserts that contract across all four surfaces at once, parsing the theme and reading the live site navigation to do it. The four cannot import each other, so nothing structural stops them drifting apart. The test is what does.
Design decisions
01
Image prompts: candid photos beat professional ones
The instruction that produces a believable food photograph is the opposite of the obvious one. Not crisp, bright and professional, which reads as synthetic at a glance, but a candid homemade phone photograph: natural window light, imperfect, crumbs and drips, not studio. Style is picked deterministically from the dish, so the feed varies while rerunning the same recipe produces the same image choice. Reruns stay predictable, which is what matters in a pipeline you have to debug.
02
Affiliate links: one registry for site and newsletter
Product links are generated from a registry that lives in the pipeline repository rather than the theme, which looks backwards until you notice that the weekly newsletter has to build the same links and email clients run no theme JavaScript. One registry and one matcher feed both surfaces, so the same sentence links to the same product in the email and on the page. The generated theme file carries a fingerprint of the sources it came from, and a test asserts that the fingerprint in the shipped bundle is current, so a regenerated registry cannot go out without its rebuilt bundle.
03
Ad placement at section breaks, requested lazily
In-content advertising is placed immediately before a section heading, so it reads as a break between sections rather than something dropped mid-thought, and never beside the recipe card. Each slot is handed to the ad network only when it comes near the viewport, so a reader who bounces off the introduction never paid to download three advertisements they scrolled past. The signed-in member check is the absence of the configuration rather than a second condition: the config renders nothing at all for a member, so there is one place that decision is made.
04
A companion planner instead of forking the scheduler
Scheduling published articles out to social needed a visual board, and the scheduler has no plugin system, so extending it would have meant forking and maintaining a container image. I built a small companion application beside it instead, driving it through its API, reachable only through the reverse proxy and authenticated against the scheduler itself. It shows every published article against live per-network status, generates captions with a model, and creates the scheduled posts after a human edits them.
05
Why I did not build Google sign-in
Single sign-on into the scheduler looked like an afternoon. It is not. The provider model is one authentication method per account, so linking an existing account breaks its password login outright; the sign-in flow reuses an unrelated integration client and redirects to a page that is itself behind authentication, so a logged-out user never completes it; and registration is disabled by design. Making it work would mean patching the callback and forking the project. I wrote down why, and left password authentication alone. Per-network connections still use OAuth, because there they genuinely work.
06
Billing on the web to stay inside App Store rules
Membership is billed through Stripe on the website. The app reflects what somebody has paid for and never sells anything, which keeps it inside the reader-app rules and keeps a payment platform out of the mobile release cycle. The gating ships switched off: every gate is currently inert, and it stays that way until the tiers are deliberately turned on.
Deploying the theme
Merging to the main branch zips the working tree and posts it to the production publishing API. There is no staging environment, which is a deliberate trade for a one-person operation and means the discipline lives in the repository instead.
Compiled bundles are committed artifacts rather than built in continuous integration, so the standing rule is that a source change ships only when its rebuilt bundle ships with it, and that rule is stated on the first screen of the repository rather than buried. A verification script runs before every push: required template files present, every script syntax-parsed, and the platform's own theme validator run as a ratchet, where any violation not already on the known list blocks the push.
What runs where
Operating it
Continuous integration reaches the secret store through federated identity rather than a stored key, which means there is no long-lived credential to leak from the pipeline, and nothing on either host holds a copy of anything.
It is also documented the way I would want a team to document something. There is an operator runbook written for somebody who did not build it and is looking at it under pressure at two in the morning: symptom, which host, what to do. The failure modes that cost real time are written down as failure modes, including the silent ones.
The iOS app
React Native and TypeScript, reading the same content API the website is built on, and parsing the same structured recipe the theme renders. Currently in TestFlight and submitted for review.






Home. Today's plan up top, then the catalog.
How the app is built
01
Offline-first caching, for a kitchen with no signal
Reads go through a cache with a time-to-live: inside the window it serves from cache, past it, it refetches and falls back to the expired cache on any network error. Favorites, the shopping list and the meal plan live on the device. The result is that a kitchen with no signal is a working app rather than a spinner, which is the actual context this gets used in. The content API also caps every page at 100 rows, the same cap that broke the category index, so the client paginates rather than trusting a request for everything.
02
Feature flags and automatic degraded-service detection
Each area of the app is gated by a flag that combines an owner override, remote configuration and a shipped default, and that is then combined with automatic degraded-service detection, where repeated network failures mark a capability degraded on their own. Both layers are offline safe and neither can throw or block a screen. The standing rule is that a new feature ships with its flag, its gate, its health wiring and a three-case test, or it does not ship.
03
Release signing without a shared certificate repo
Releases authenticate and sign through an App Store Connect API key with automatic provisioning, rather than a shared certificate repository, which does not work on this team's account. Version comes from the git tag and the build number is derived from what is already uploaded, so two people cannot collide. A tag produces a TestFlight build; submission is a deliberate, separate action.
Stack
- Publication
- Ghost 6, Handlebars theme, sixteen custom templates, SCSS and Gulp, schema.org structured data, MySQL, nginx, Stripe memberships
- Pipeline
- Node.js and TypeScript, Claude, Imagen 4, sharp, GitHub Actions on a schedule, AWS Secrets Manager over federated identity
- Distribution and control
- Self-hosted social scheduler, Per-network OAuth connections, A Slack app over a socket connection, no open ports, A companion planning application behind the proxy, Postgres and Redis
- Edge and revenue
- Cloudflare DNS, WAF and CDN, Object storage on the CDN, Amazon Associates, generated from one registry, AdSense, placed by section, Transactional email
- App
- React Native, TypeScript, React Navigation, On-device persistence, Passwordless sign-in and universal links, Fastlane and GitHub Actions