A theme is a document, not a deploy
I keep explaining this to people who just want a deploy button. A Feeef theme is the JSON the shop already knows how to render.
I work on the storefront at Feeef. The first question I get from someone who has shipped a Shopify theme, or a Next app, is some version of "where do I deploy this?"
Lithium is already running. What you hand us is a JSON document, and the shop loads that. The folder on your laptop looks like an app, which is why the question keeps coming up. I still have to say the sentence out loud.
Here is why I stopped fighting it. After you give a merchant a theme, they change a heading. They hide the email field. They drag the buy button. A month later they install your update. If any of that needs a pull request, or needs them to know React, you are stuck in their shop forever. So the thing that leaves your machine has to be data. The TypeScript is how you produce the data.
The document
We call it TemplateData. I kept the shape small on purpose, because I have to hold it in my head while reviewing other people's themes:
{
props: { theme: { mode: "system" } },
i18n: { defaultLocale: "ar", locales: ["ar", "en"], messages: {} },
pages: {
home: {
props: {},
sections: {
main: { components: [/* nodes */] },
},
},
},
}A node is one block on the page. type tells Lithium which renderer to use. props is
what the merchant changed in the editor. code is your function, stored as a string, and
only when type is "custom". instanceId is the name we keep stable. The editor's
selection hangs off it, and so do React's keys. If you regenerate that id on every save,
the block remounts and the merchant loses their place. I have done that. It feels like the
page is haunted.
When I review a theme I use one rule. Merchant content lives in props. The identity of
the component sits next to props: code, propsSchema, slots, slotsSchema,
slotsLayout. The day someone nests code under props, the inspector goes blank and
the renderer skips the function. Both sides look for those keys at the top of the node. I
would love to be loose about it. Two code paths is how this drifts, so we don't.
Schema is the other file
People mix TemplateData up with the schema. I do it too, usually late.
schema.ts in a theme package is an overlay. The build merges it with Lithium's built-in
catalog and writes dist/schema.json. The merchant editor reads that file to know which
pages exist and which controls to draw. The shop paints dist/data.json. Different file,
different job.
There is a sharp edge I still walk into. If your overlay includes a pages object, that
object replaces the page map. It does not fill in the pages you forgot. I shipped a
theme that rendered / and then stared at an empty /p/:id, because product was never
in schema.ts. The route exists in Lithium either way. An empty main is what you get
when the document has nothing to put there.
The blank starter lists home, product, and thank_you so a new theme can take an
order and land on /thanks without that particular afternoon.
What actually runs
On a request, Lithium loads the store and picks a document. Draft or live, that is the
next post. It resolves any type: "reference" nodes against template_components, then
walks the page. Layout types recurse through children. Built-ins hit the registry.
custom is precompiled on the server and evaluated with a fixed scope: props, React,
t, useStore, useFeeefCart, Link, and the rest of the list in the
components guide.
Your source can import. The string we store is one function, with those imports already
bundled in, and React provided by the page. I prefer React.useState in that string. The
scope always has React. A bare useState depends on whoever last edited the prelude,
and I don't want your theme to break because of me.
If you remember one thing
Treat the theme package as input to a compiler. dist/data.json is the output. The
merchant editor is another writer of the same document, and it only gets to touch props,
slots, and layout children. If the only way to change your component is to edit the
function, the merchant is locked out of the thing they bought.
The long version, with the field table and the $ref versus reference split, is
How the system works. The commands are
Create a template.