Storelib Developer HubRules for agents

Rules before you ship

Read this before you write or change any code on a Storelib site. It applies to every agent: Claude Code, Codex, ChatGPT through GitHub, Cursor, and the builder's own AI. Each rule is enforced by the platform or checked by its tests, so code that breaks one is refused, or looks broken on the creator's site.

1. What you may change#

  • Section files only. You write sections/<type>.storelib, in lower case with digits, _ and -. Templates, config, layout, snippets and assets are the theme's core. Writes to them are refused.
  • Never a built-in section. The sections Storelib ships (header, footer, hero, product page, FAQ and the rest of the library) are what every site is built from. You can read them. A write to one is refused. To change how one works, write a new section beside it, such as sections/hero_custom.storelib, with the same settings so nothing the creator filled in is lost. Then tell the creator they can use it in place of the built-in one.
  • Everything you save is a draft. Nothing reaches a visitor until the creator presses Publish, and every save keeps a version.
  • One site. A connection reaches one website. It cannot reach orders, buyers, subscribers or the account, and it cannot delete, rename or publish anything.

storelib_list_files marks every file writable: true or false. Trust it.

2. Know the framework first#

  • Read Building with AI agents and the full reference at /llms-full.txt. Call storelib_site to learn what this website sells, what it is called, and how it looks.
  • A section is one file with four blocks: <template>, <style>, an optional <script>, and <schema>. The template language is {{ }}, {% if %}, {% for %} and filters. It is not Vue, JSX or Liquid.
  • Every value the creator might change is a setting in the schema. Every repeated thing is a block. A template reads only what its schema declares.
  • Compile before you save (storelib_compile). Fix the exact line the compiler names; don't rewrite the file.

3. Everything the creator sees must be editable#

The creator edits a section in the builder's sidebar, as they would in Shopify. If a word, a picture or a link is typed straight into the markup, they cannot change it.

  • Text is a text, textarea or richtext setting. Print richtext with | raw; everything else is escaped for you.
  • Pictures are image_picker settings. Never put a URL to a picture in a default.
  • Every link and every button has two settings: a text label and a url link. Read the link through the filter, href="{{ settings.button_url | default: '#' | url }}". A url setting gives the creator the link picker, with their pages and products in it.
  • Menus and link lists are blocks, one block per link, each with its own label and url. The creator adds, removes and reorders links in the sidebar, and the section draws whatever is there.
  • Colour comes from the colour scheme. Declare a color_scheme setting and use var(--scheme-*). Don't use hex codes for anything the scheme covers.

The navigation bar example below does all of this.

4. Copying a page from a screenshot#

When you are given a screenshot or a URL to copy:

  1. Split it into sections, top to bottom: header, hero, features, testimonials, pricing, FAQ, footer, whatever is there. Each becomes its own file with its own schema. Never write a whole page as one section.
  2. Look for a built-in section first. If the site already has a section that does the job (a hero, an FAQ, product cards), its settings are the change, not a new file.
  3. Give each new section a full schema. Every heading, paragraph, image, button and link from section 3 above, with defaults taken from the screenshot so it looks right the moment it is added.
  4. Build the header and footer the same way. The site's navigation is menu-link blocks, the logo is an image_picker, and the call-to-action is a label and a url. A header whose links are typed into the markup is one the creator can never change.
  5. Use the site's own words and products, from storelib_site. Never invent a product, a price, a review or a statistic.

5. Menus, dropdowns, modals and bottom sheets#

The builder draws the site inside a canvas in the editor, and the section must work there and on the live site.

  • Open things with HTML, not script: <details> for disclosure, radio inputs with :checked for tabs, and popovertarget with popover for menus, sheets and dialogs. The browser handles Escape, tap outside and focus for you. In the builder, a popover opens inside the canvas.
  • A phone menu is a sheet: popover, positioned with inset, height: 100%, overflow-y: auto, overscroll-behavior: contain, and env(safe-area-inset-bottom) padding at the foot. Style its open state with :popover-open.
  • A dropdown is positioned absolute under the thing that opens it, inside the section. It hangs over the next section on its own; don't raise it with an enormous z-index.
  • Set aria-expanded on a menu button when a script opens the menu.
  • position: fixed, 100vh and 100vw are the canvas's in the builder and the window's on the live site. Use them for overlays and full-screen menus, and nothing else.
  • Never write <style>, <link>, <base> or <meta> inside the template. They act on the whole page, and a save refuses them.

6. Motion#

  • Animate opacity and transform only. They are cheap, and they don't move anything else on the page.
  • Keep it short and settled: 150 to 300ms, on an easing that lands, such as cubic-bezier(0.32, 0.72, 0, 1). Nothing bounces, loops or moves on its own.
  • An entrance plays once, when the section scrolls into view. Nothing animates on every scroll.
  • Everything that moves stops under @media (prefers-reduced-motion: reduce).
  • Height from 0 to auto does not transition. Animate grid-template-rows from 0fr to 1fr instead.

7. Phones and iOS#

  • Build the phone layout first, then widen with @container (min-width: …). Put container-type: inline-size on the section's root.
  • Never scroll sideways. Use minmax(0, 1fr) rather than 1fr, min-width: 0 on flex children, and overflow-wrap: anywhere on long words.
  • Tap targets are at least 44px high.
  • Inputs are 16px or larger, or iOS zooms the page when one is tapped.
  • Respect the safe area: env(safe-area-inset-*) on anything pinned to an edge of the screen.
  • Images carry width, height or an aspect-ratio, so nothing jumps as they load. Use loading="lazy" except in the first screen.
  • No hover-only features. A phone has no hover, so anything that only appears on hover is missing there.

8. What good looks like#

Storelib sites should look like they were made by a person with taste, not produced by a generator.

  • Start from what the visitor needs. Remove before you add: every badge, icon, divider and line of copy has to earn its place.
  • No generic AI styling. No purple gradients, glassmorphism, bento grids of identical cards, giant empty heroes, pills everywhere, an icon beside every label, or cards inside cards.
  • Specific, plain copy. Say what the thing is and what happens. Never write unlock, elevate, seamless, powerful, effortlessly, supercharge or transform, and don't use em dashes.
  • Use the theme. Its type styles (var(--type-h2-size), var(--theme-font-heading)), its radii (var(--theme-button-radius)), its page width (var(--theme-page-width)) and its colour schemes. A section that brings its own palette looks pasted in.
  • Real content, real states. Empty states appear only in the builder ({% if page.in_editor %}), and the live site shows nothing rather than a placeholder.

9. Before you say it is done#

  1. It compiles with no errors, and storelib_compile is clean.
  2. Every visible word, picture, link and button is a setting or a block.
  3. It works at 375px, with no sideways scroll, 44px targets and 16px inputs.
  4. Menus, sheets and dialogs open and close with a tap, Escape and a tap outside.
  5. Motion stops under reduced motion.
  6. It uses the site's colour scheme and fonts, and the site's real products.
  7. You changed no built-in section and no file outside sections/.

Then say what you changed in one or two plain sentences, and remind the creator it is a draft until they publish.

A header done properly#

A navigation bar with a logo, menu links as blocks, a button with its own link, and a phone menu that opens as a sheet with no script. Every word and link in it is editable in the sidebar.

sections/nav_bar.storelib
<template>
<header class="nb" data-scheme="{{ settings.color_scheme }}">
  <div class="nb__in">
    <a class="nb__brand" href="/">
      {% if settings.logo %}<img class="nb__logo" src="{{ settings.logo }}" alt="{{ settings.brand }}" style="height: {{ settings.logo_height }}px;">{% else %}{{ settings.brand }}{% endif %}
    </a>
    <nav class="nb__links" aria-label="Main">
      {% for block in blocks %}
        <a class="nb__link" href="{{ block.settings.url | default: '#' | url }}" data-block-id="{{ block.id }}">{{ block.settings.label }}</a>
      {% else %}
        {% if page.in_editor %}<span class="nb__empty">Add a link from the panel.</span>{% endif %}
      {% endfor %}
    </nav>
    {% if settings.button_text != "" %}<a class="nb__btn" href="{{ settings.button_url | default: '#' | url }}">{{ settings.button_text }}</a>{% endif %}
    <button class="nb__open" type="button" popovertarget="{{ section.id }}-menu" aria-label="Open the menu">
      <span></span><span></span>
    </button>
  </div>
  <div class="nb__sheet" id="{{ section.id }}-menu" popover>
    <div class="nb__sheet-top">
      <span class="nb__sheet-brand">{{ settings.brand }}</span>
      <button class="nb__close" type="button" popovertarget="{{ section.id }}-menu" popovertargetaction="hide" aria-label="Close the menu">&times;</button>
    </div>
    <nav class="nb__sheet-links" aria-label="Menu">
      {% for block in blocks %}
        <a class="nb__sheet-link" href="{{ block.settings.url | default: '#' | url }}">{{ block.settings.label }}</a>
      {% endfor %}
    </nav>
    {% if settings.button_text != "" %}<a class="nb__btn nb__btn--wide" href="{{ settings.button_url | default: '#' | url }}">{{ settings.button_text }}</a>{% endif %}
  </div>
</header>
</template>

<style>
.nb { container-type: inline-size; background: var(--scheme-background); border-bottom: 1px solid var(--scheme-border); }
.nb__in { display: flex; align-items: center; gap: 16px; max-width: var(--theme-page-width, 1200px); margin: 0 auto; padding: 14px var(--theme-padding-x, 20px); }
.nb__brand { color: var(--scheme-heading); font-weight: 700; font-size: 18px; text-decoration: none; min-width: 0; overflow-wrap: anywhere; }
.nb__logo { display: block; width: auto; }
.nb__links { display: none; }
.nb__link { color: var(--scheme-text); text-decoration: none; font-size: 15px; padding: 8px 4px; }
.nb__link:hover { color: var(--scheme-heading); }
.nb__btn { display: none; align-items: center; justify-content: center; min-height: 44px; padding: 0 18px; border-radius: var(--theme-button-radius, 10px); background: var(--scheme-button-primary); color: var(--scheme-button-primary-text); text-decoration: none; font-weight: 600; }
.nb__open { margin-left: auto; display: grid; place-content: center; gap: 6px; width: 44px; height: 44px; border: 0; background: transparent; color: var(--scheme-heading); cursor: pointer; }
.nb__open span { display: block; width: 20px; height: 2px; border-radius: 2px; background: currentColor; }
.nb__empty { color: var(--scheme-text-muted); font-size: 14px; }

/* The phone menu: a sheet from the right, opened with no script. */
.nb__sheet { inset: 0 0 0 auto; width: min(360px, 100%); height: 100%; max-height: none; margin: 0; padding: 16px 20px calc(24px + env(safe-area-inset-bottom, 0px)); border: 0; background: var(--scheme-background); color: var(--scheme-text); box-shadow: -12px 0 40px rgba(0, 0, 0, 0.18); overflow-y: auto; overscroll-behavior: contain; }
.nb__sheet:popover-open { display: flex; flex-direction: column; gap: 8px; animation: nb-in 260ms cubic-bezier(0.32, 0.72, 0, 1); }
.nb__sheet::backdrop { background: rgba(0, 0, 0, 0.35); }
.nb__sheet-top { display: flex; align-items: center; justify-content: space-between; min-height: 44px; }
.nb__sheet-brand { color: var(--scheme-heading); font-weight: 700; }
.nb__close { width: 44px; height: 44px; border: 0; background: transparent; color: var(--scheme-heading); font-size: 26px; cursor: pointer; }
.nb__sheet-links { display: flex; flex-direction: column; }
.nb__sheet-link { display: flex; align-items: center; min-height: 48px; color: var(--scheme-heading); text-decoration: none; font-size: 17px; border-bottom: 1px solid var(--scheme-border); }
.nb__btn--wide { display: flex; margin-top: 16px; }
@keyframes nb-in { from { transform: translateX(24px); opacity: 0; } }

@container (min-width: 768px) {
  .nb__links { display: flex; gap: 20px; margin-left: auto; }
  .nb__btn { display: inline-flex; }
  .nb__open { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .nb__sheet:popover-open { animation: none; }
}
</style>

<schema>
{
  "type": "nav_bar",
  "name": "Navigation bar",
  "category": "Header",
  "settings": [
    { "type": "text", "id": "brand", "label": "Brand name", "default": "Studio" },
    { "type": "image_picker", "id": "logo", "label": "Logo" },
    { "type": "range", "id": "logo_height", "label": "Logo height", "default": 28, "min": 16, "max": 64, "step": 2, "unit": "px" },
    { "type": "text", "id": "button_text", "label": "Button label", "default": "Shop" },
    { "type": "url", "id": "button_url", "label": "Button link", "default": "/products" },
    { "type": "color_scheme", "id": "color_scheme", "label": "Colour scheme", "default": "white" }
  ],
  "blocks": [
    {
      "type": "link",
      "name": "Menu link",
      "limit": 8,
      "settings": [
        { "type": "text", "id": "label", "label": "Label", "default": "About" },
        { "type": "url", "id": "url", "label": "Link", "default": "/about" }
      ]
    }
  ],
  "max_blocks": 8,
  "presets": [
    {
      "name": "Navigation bar",
      "blocks": [
        { "type": "link", "settings": { "label": "Shop", "url": "/products" } },
        { "type": "link", "settings": { "label": "About", "url": "/about" } },
        { "type": "link", "settings": { "label": "Contact", "url": "/contact" } }
      ]
    }
  ]
}
</schema>
Rules before you ship | Storelib Developer Hub