Changelog
Every change to Folio, newest first. Versions follow semantic versioning: the major number changes when your existing settings would need attention.
All notable changes to Folio are recorded here. Versions follow
[semantic versioning](https://semver.org): the major number changes when a
merchant's existing settings would need attention.
[Unreleased]
Added
- Hero gallery — a hero built from more than one image, in two shapes:
- Split sets two or three full-height panels side by side under a single
caption centred across the seam.
- Rail scrolls the panels sideways with the next one showing at the edge,
which tells a shopper there is more without an arrow or a row of dots.
The caption is one overlay across the whole section in both, so it does not
travel when the rail scrolls and never has to be repeated per panel. Each
panel takes its own link, caption and alt text. Split stacks on a phone; the
rail keeps scrolling and neither makes the page scroll sideways.
Changed
- Fit and fabric now read the product, not the section. Both blocks took
their values from section settings, so every product on a template showed the
same model note, the same fit scale and the same composition — the jacket was
claiming the leggings' fabric. They now read
custom.model_note, custom.fit_scale, custom.fit_detail and
custom.composition from the product, falling back to the block for a shop
that has not set them.
- Placeholder copy replaced with real content throughout the templates:
product fit and shipping, the FAQ answers, the size-guide advice and the
story lead. Instructional text is fine as onboarding but Shopify prohibits it
on a demo store, and the same templates drive both.
Fixed
- The size-guide block held a measurements table in a
richtextsetting, which
Shopify rejects at upload — that setting type permits only <p>, <ul>,
<ol> and headings. The table moved to the size-guide page, which is what the
block's page picker was there for, and bin/validate-richtext.mjs now catches
disallowed tags at build time rather than at push.
Fixed
- One unclosed brace was breaking four overlays site-wide. The editorial
grid rebuild dropped the closing } of a media query in
snippets/product-grid.liquid. Shopify concatenates every {% stylesheet %}
block into one styles.css, so an unbalanced brace swallows every block
compiled after it — the search drawer, quick view, the size-guide modal and
the media lightbox all lost position: fixed and rendered inline in the page,
on every template.
Theme Check does not look inside stylesheet blocks and reported no offenses
throughout. bin/validate-css.mjs now checks every block for balance and runs
in CI, so this class of failure cannot ship silently again.
Fixed
- Quick view and the media lightbox had no accessible name. Both get
role="dialog" and aria-modal from JavaScript, but their aria-label sat
on an inner panel, so screen readers announced an unnamed dialog. Caught by
re-running Lighthouse after the header and menu work — accessibility had
slipped from 100 to 97 and is back at 100.
Added
- Three mega-menu panel shapes, selectable per dropdown through a new Menu
panel block:
- Compact dropdown — anchored under its trigger, sized to its content.
What a short menu like Help actually needs.
- Columns — every child a column heading with its links beneath.
- Shortcuts and columns — childless children set large down the left
against a rule, the rest as columns beside them.
Left on *Match the menu*, the shape is read from the menu structure: a child
with grandchildren is a column, a child without is a shortcut, and a menu made
only of shortcuts gets the compact dropdown. So a well-built menu gets the
right panel with no configuration, and the block is there to override it.
Fixed
- Panel columns were laid out with
repeat(auto-fit, minmax(9rem, 1fr)), which
stretched five links across the full viewport with nothing to structure them.
They now size to their content and group to the left.
Added
- Five header layouts, up from two. Logo left; logo left with the menu
centred; menu left with the mark centred; stacked, where the mark sits on its
own row above the menu; and minimal, which keeps the menu in the drawer at
every width for a long or seasonal menu.
The header markup was restructured to make this possible. The navigation used
to be written twice with a conditional choosing one — which does not scale
past two arrangements, and put the same landmark in the document twice. There
is now one nav, four fixed children, and every layout is a grid placement
over them.
Fixed
- The centred-mark layout put the navigation on a second row and doubled the
header height. The nav sits after the brand in the DOM but belongs in the
first column, and grid auto-placement never moves its cursor backwards, so
the row has to be pinned explicitly.
Added
- Three newsletter styles, chosen with one setting rather than assembled
from a dozen:
- Minimal — the existing quiet block, still the default.
- Centred band — centred, generous vertical padding, larger type. Made to
stop the scroll, and pairs with a contrasting colour scheme.
- Editorial split — full-bleed image beside oversized display type, with
the field reduced to a single rule and the submit to an arrow. Falls back to
bundled photography so it never collapses to one column before a merchant
picks an image.
- The form gained an
underlinetreatment, used by the editorial style, where
the input loses its box and the button becomes an arrow that slides on hover.
Fixed
- A
{% form %}tag'sclassargument is a plain string, not a template, so
{{ style }} inside it was emitted verbatim and the style modifier never
applied. The class is now built before the tag.
Fixed
- Bundled fallback images stopped rendering.
media.liquidlost its
fallback_asset branch, so the video hero, campaign split, product spotlight
and lookbook all fell through to Shopify's line-art placeholder instead of
their onboarding photography. Restored, with decoding="async" added.
- Hero copy was hard to read over a light photograph. The video hero used a
flat scrim, which dims the entire image to protect one corner of text. It now
uses a directional gradient anchored to the chosen content position —
heaviest where the copy sits, clearing to nothing across the rest of the
frame — plus a text shadow for the bright detail an average-based scrim
misses.
Fixed
- The editorial arrangement rendered identically to the even grid on any
collection with fewer than two full rows of products. Fixing the earlier
overlapping-tiles bug had gated the whole arrangement behind a product count,
so a merchant switching to Editorial on a four-product collection saw no
change at all — a control that does nothing is worse than one that looks odd.
Editorial is now two devices rather than one. Alternating tiles are offset
down the page, unconditionally, so the arrangement always reads differently
from the even grid whatever the product count. The double-width feature tile
layers on top of that once there are enough products for two full rows, and
is exempt from the offset so it anchors its row.
The offset uses margin-block-start with align-items: start on the grid.
Margin alone is not enough: while the grid stretches its items, a top margin
makes a tile shorter instead of moving it, so the cards ended up unequal
heights. Measured on the live store — even: four tiles all flush at the same
top; editorial: alternating tiles offset by 51 px, all heights equal, right
edge flush, no overlap.
Fixed
- The editorial product arrangement was broken in three ways at once. Every
fourth tile was widened to two columns regardless of how many columns the grid
had, so in a four-column grid the wide tile could not fit the one remaining
column, wrapped, and left a hole in the row above. Alternating tiles were
offset with transform: translateY, which reserves no space, so they
overlapped their neighbours and the baseline went ragged. There was no dense
packing to backfill anything.
It is now built to tile exactly: each row is one double-width tile plus
however many single tiles fit beside it, so the repeating unit is
columns - 1 items and a row is always full. The rule is written per column
count because nth-child cannot read a custom property. The wide tile keeps a
16:10 frame, which is almost exactly the height of a single portrait tile at
double width, so row tops stay aligned.
The arrangement also now needs two full rows of products before it engages.
Four products in a four-column grid gave one full row and a single stranded
tile, which read as a mistake; short runs stay on the even grid.
Added
- Home page, article and our-story templates now point at real collections, a
real product and the Journal blog rather than at nothing.
- Footer carries three menu columns — Shop, Help and Legal — wired to menus that
exist.
docs/demo-store-setup.mdrecords everything created on the demo store and
what still has to be done by hand.
Fixed — found by running the theme on a real store
- Product recommendations had never loaded.
.recommendations__target:empty
sets display: none on the placeholder, and a display: none element never
reports as intersecting, so the IntersectionObserver that triggers the fetch
never fired. "Pairs well with" and "You may also like" rendered a heading over
empty space on every product page, on every store, since the section was
written. The observer now watches the section rather than the hidden
placeholder.
- Twelve calls to action pointed at
href="#"when their link was unset —
hero, campaign split, slideshow, media banner, editorial grid, rich text,
countdown, shoppable story, the button block and the menu promotion. Every one
of them scrolled the shopper to the top of the page, which is the complaint
originally raised about the collection index. They now fall back to the
all-products collection, which exists on every store.
Fixed — found by diffing the live store against source
templates/customers/addresses.liquidwas never reaching the store. It
carried a {% javascript %} tag, which Shopify supports in sections, blocks
and snippets but *not* in templates. The upload silently dropped the file —
Theme Check does not flag it, the CLI reports success, and the only symptom is
that the addresses page is missing on the storefront. Six of seven customer
templates uploaded; this was the seventh.
The script moved to assets/folio-account.js, loaded on account routes beside
account.css.
- The delete control on that page was an
<a href="#">withrole="button"—
the same pattern that was sending shoppers to the top of the page from the
collection index. It is a real <button> now.
Added
- Collection index rows can expand. A row now opens a short description
instead of navigating, with the link to the collection inside it where it is
an explicit choice. Built on native <details>, so it is keyboard operable
and works with JavaScript off. Rows with no text stay plain links.
- Video hero accepts YouTube and Vimeo as well as an uploaded video. An
uploaded video still wins when both are set — it autoplays faster, carries no
third-party cookies, and can be driven by the on-screen pause control, which
a cross-origin embed cannot.
- Two about templates.
page.aboutruns mission → founder → values →
process → voices; page.our-story is chapter-led with alternating splits, a
film and a milestone timeline.
- Two more contact templates.
page.contact-supportleads with an FAQ so
most enquiries resolve before the form; page.contact-visit leads with a
banner and a map for a physical space.
page.policytemplate for terms, privacy, returns and shipping — a
long-form reading measure with a last-updated line and a route to a person at
the end.
- Starter journal posts in
docs/demo-blog-posts.md: five drafts written
for this category, ready to paste into Shopify admin.
- The performance icon set is now available to the benefits row and product
spotlight, not just the fabric block.
Fixed
- Empty recommendation sections left a heading over dead space. The
JavaScript removed only its own placeholder, so "Pairs well with", "You may
also like" and "Recently viewed" each kept a heading with nothing under it
when the API had no results. The whole section is now removed on the
storefront; in the theme editor it stays and shows demo cards, so the merchant
can still see and select it.
- The blog index had a bare sentence for its empty state; it now has a real one,
with a pointer to the starter drafts when viewed in the editor.
Fixed — found on a real store
- Custom elements were rendering as
display: inline. A custom element has
no default display, and thirty-two of them carried layout only through a
class. site-header had none, so the sticky header painted its background
behind text fragments instead of the whole bar — page content showed straight
through it — and transform: translateY(-100%) never applied, which also
broke the scroll-up hide. Declared once in base.css; any component that
wants flex or grid still sets it on its own class.
- Collection index rows scrolled to the top of the page. An unassigned block
fell through to href="#". Rows without a destination are no longer links,
and each block gained an optional link setting.
- The drawer and modal dim is now a merchant setting and defaults heavier
(0.35 → 0.6). A light storefront needs more scrim than a dark one, so colour
and strength are both controllable.
- The lookbook caption was stranded at the top of a tall image. It now
anchors to the foot of the frame, with a setting for beside, below or over
the image.
- The collection empty state told one story for two different problems. An
over-filtered result now says so and offers to clear the filters, rather than
suggesting the shopper browse everything and discard the intent they just
expressed.
- Threshold copy no longer prints a bare number with no currency
("orders over 100"), which reads as broken on every store.
Added
- Video hero. Full-bleed background video with an overlaid headline. The
poster is the LCP and loads eagerly; the video is lazy, muted, inline, never
autoplays under reduced motion, and always carries a pause control (WCAG
2.2.2). Degrades to a still banner when no video is set.
- Contact page rebuilt. Form beside a panel of blocks — contact details that
are tappable as mailto: or tel:, opening hours, an image, social links,
and a map that takes an embed URL, an image, or neither (address plus a
directions link). Optional subject and order-number fields route enquiries.
- Journal posts section. A merchant-insertable blog module for the home
page, landing pages or the foot of an article, where it excludes the piece
being read. Grid or rail.
- Blog index leads with the latest post in a full-width frame, on page one
only.
- Blog and article templates built out — the article page now carries
related reading and a newsletter; both were a single bare section before.
- Home page gained the video hero, and its journal block is now the real
journal section rather than rich text impersonating one.
Added
- Size guide. A
size_guideproduct block whose trigger renders beside the
size options in the buy box and opens a panel fed by a page the merchant
already maintains. Falls back to a standalone link if no product option
matches the configured name.
- Fit block. A three-point fit scale — runs small, true to size, runs large —
with a model note. Fit is comparative, and a position on a scale reads faster
than a sentence.
- Fabric block. A composition line and up to four icon-and-label
performance claims.
- Ten performance icons drawn to the existing 24 x 24, 1.25 px grid:
four-way stretch, compression, squat-proof, breathable, moisture-wicking,
lightweight, seamless, pockets, recycled and care.
- Breadcrumbs on product and collection, with
BreadcrumbListstructured
data. The product trail follows the collection the shopper arrived through,
so "back" means the collection they were actually browsing.
- Original athleisure campaign imagery for the home-page hero, movement story,
product spotlight, collection index and lookbook onboarding states.
- Four currency-aware demo product cards — Form Legging 25\", Studio Bra,
Airweight Zip Jacket and Sculpt Unitard — that are replaced automatically
when a merchant assigns a real collection.
- Complete athleisure launch copy, collection labels, benefit messaging and
lookbook stories for a convincing fresh install.
- Foundation: layout, design-token layer, colour-scheme system, base stylesheet
and five JavaScript modules.
- Global sections: announcement bar, header with mega menu and drawer, footer,
cart drawer.
- Commerce templates: product, collection, cart, search, collection index,
blog, article, page, contact, 404, password, gift card, and the seven
customer-account templates.
- Editorial sections: hero, campaign split, editorial grid, collection index,
lookbook, image hotspots, before/after comparison, media banner, video,
slideshow, marquee, timeline, testimonials, FAQ, rich text.
- Merchandising sections: featured collection, tabbed collections, product rail,
featured product, product spotlight, shoppable story, recently viewed,
product recommendations, complementary products, quick order list.
- Utility sections: newsletter (inline and pop-up), benefits row, countdown,
custom Liquid, app blocks.
- Alternate templates: product (editorial, pre-order), collection (editorial),
page (lookbook, FAQ, landing, size guide).
- Shopify account component and configurable customer-account menu.
- Follow on Shop using Shopify's native button.
- A single launch preset named after the parent theme, matching the fresh
install state.
Changed
- Display and body fonts are now a pair. Both were Assistant at one weight,
which collapsed the display-versus-body hierarchy the token layer builds.
Display is Jost; body stays Assistant.
- Type scale lifted — body from 15 px to 16 px, with the ramp and line
height raised to match.
- More air. Section rhythm, page margin and the buy-column gap all
increased. The fit and fabric blocks are separated by hairlines so each reads
as its own answer rather than one undifferentiated column.
- Utility tracking widened from 0.06 em to 0.12 em.
- The default product template leads with title, price, variants, size guide
and buy, then fit, then fabric.
Fixed
- Demo photography re-encoded to WebP. 11.16 MB of PNG became 272 KB, a
97.6% cut, taking the theme package from 11.78 MB to roughly 0.6 MB. The
fresh-install hero is the LCP element and was shipping 1.92 MB to phones,
which would have failed the performance gate on its own.
- The sticky add-to-cart bar now adds to cart. It was an anchor that
scrolled back up to the form. It is now a real product form inside a
data-variant-target region, so the same section render that updates the
price also keeps the posted variant, availability and button label correct —
no extra JavaScript, no client-side money formatting.
- Demo products can no longer reach a live storefront.
show_demo_content
now defaults to off; the cards still render in the theme editor through
request.design_mode, which is where they do their onboarding job.
- Removed fabricated footer links. An empty column invented English link
text and pointed at /pages/faq and /pages/size-guide, which 404 on a fresh
install, and the branch was chosen by comparing a merchant-editable heading
against the English string 'Shop'. Empty columns now stay empty on the
storefront and show a prompt in the editor.
- Localised the remaining hardcoded English in the hero alt text and the
demo product card.
- Removed unsupported colour-scheme role metadata that caused Shopify to reject
the dynamic scheme group on upload.
- Added a complete static design-token fallback and corrected stylesheet order
so a fresh Shopify upload cannot collapse into unstyled serif text when the
dynamic token layer is unavailable.
- Added meaningful fresh-install collection labels and a collection-preview
placeholder instead of blank numbered rows.
- Corrected pickup-availability URLs for stores mounted at the root locale.
- Restored live cart quantity changes and no-JavaScript cart update/removal.
- Made tax-inclusive messaging depend on
cart.taxes_included. - Repaired gift-card recipient disclosure, submitted values, validation state
and timezone scheduling; disabled incompatible accelerated checkout for gift
card recipient forms.
- Added Apple Wallet, recipient message and scheduled-send details to issued
gift cards.
- Added rich product media and app/Custom Liquid blocks to featured products,
and rich media plus dynamically loaded styles to quick view.
- Corrected literal merchant labels that were accidentally treated as locale
translation keys.
- Added schema and translation-reference validation to the build checks.
Notes
Nothing has shipped yet. The first public version will be tagged 1.0.0 on
Theme Store submission.
The display font handle jost_n5 has not yet been confirmed against Shopify's
hosted font library, because the theme has still never been rendered by
Shopify. Verify it on the first store push; if it is rejected it is a one-word
change in config/settings_schema.json and config/settings_data.json.