Gate pages

Build translated coming-soon, password and maintenance pages, render hosted form outcomes, and preview every gate state.

Gate pages are theme surfaces: coming_soon, password, and maintenance. Ship templates/coming_soon.json, templates/password.json, and templates/maintenance.json, and declare their required section types in theme.json surface policies. Section names are your choice. The platform selects the surface from storefront status and access rules; a gate is not an ordinary catalog page.

Composition and data#

Gate pages do not inherit the normal storefront header, footer or overlay. A scoped composition such as coming_soon@@global_body_start can explicitly supply chrome. Build a complete page with its own logo image setting and store name. shop.name is the safe baseline; do not require a cart, customer, catalog, or visitor session. Declare each CTX root you read in ctx_needs, including shop and csrf_token for the examples below. Runtime gate copy and outcomes belong to section settings, not new CTX roots.

Translated title and message#

The platform overlays the active locale’s gate translations onto settings.title and settings.message on the sections required by the surface policy, including required-one-of choices. Declare those exact IDs as text settings and render their values. Theme defaults supply source copy when no translation is present. On password pages, blank title/message and blank password placeholder/button label receive localized platform copy. Do not use heading or text as substitute IDs for gate copy. Translation resource types are coming_soon_page, password_page and maintenance_page.

vascula
<section data-theme-gate="coming_soon">
  <p>{{ shop.name }}</p>
  <h1>{{ settings.title }}</h1>
  <p>{{ settings.message }}</p>
</section>

For an existing section, rename the saved settings with schema migrations, retaining stable migration IDs across versions. Update templates and source reads in the same theme version. The example shows only the migration entries; include them in the section schema’s migrations array.

json
[
  {
    "id": "heading-to-title",
    "op": "rename_setting",
    "from": {
      "setting": "heading"
    },
    "to": {
      "setting": "title"
    }
  },
  {
    "id": "text-to-message",
    "op": "rename_setting",
    "from": {
      "setting": "text"
    },
    "to": {
      "setting": "message"
    }
  }
]

settings.language_options is a list of {code, label, native_name, url, selected}. Use its URLs for language links and hide the switcher when there is only one choice. settings.locale preserves the active language on forms. settings.social_links_list provides configured social links; check optional data before rendering it. Theme-authored labels still belong in theme locales.

Coming soon: email signup#

Declare collect_emails as a checkbox setting and show the form only when its resolved value is true. The server checks the published coming-soon composition using the section schema default when the saved value is absent; an explicit false overrides that setting’s default. Another section or visible block with resolved collect_emails: true can still enable collection. It accepts signup only while the storefront is in coming-soon mode. Display the platform’s email_placeholder, button_label, and consent_label settings; blank values receive localized defaults. Consent must be an unchecked, visible checkbox named consent, submitted with a nonempty value when selected. Never pre-check it or replace it with hidden consent.

vascula
<div data-notify-state="{{ settings.notify_state }}"></div>
{% if settings.notify_success != blank %}
  <p role="status" data-notify-success>{{ settings.notify_success }}</p>
{% endif %}
{% if settings.notify_error != blank %}
  <p role="alert" data-notify-error>{{ settings.notify_error }}</p>
{% endif %}
{% if settings.collect_emails %}
  <form method="post" action="/notify">
    <input type="hidden" name="_csrf" value="{{ csrf_token }}">
    <input type="hidden" name="locale" value="{{ settings.locale }}">
    <label>{{ settings.email_placeholder }}
      <input type="email" name="email" autocomplete="email" required maxlength="254">
    </label>
    <label><input type="checkbox" name="consent" value="true" required>
      {{ settings.consent_label }}
    </label>
    <input type="text" name="website" hidden tabindex="-1" autocomplete="off">
    <button type="submit">{{ settings.button_label }}</button>
  </form>
{% endif %}

POST /notify uses Post/Redirect/Get: a 303 redirect and an HTTP-only, one-shot flash outcome. The next gate render exposes settings.notify_success for ok, or settings.notify_error for invalid_email, consent_required, retry_later, or unavailable. The stable outcome code is settings.notify_state, absent when no flash was supplied. The success/error values are already localized messages. Read the rendered settings, not a cookie, query flag, or guessed English text. The platform adds a _csrf field to every form posting to /notify on the published coming-soon page, so themes need no change; a form rendered with csrf_token keeps working. A missing or stale token returns retry_later. After a few signups from one visitor in an hour the platform asks the browser to complete a short proof-of-work challenge before accepting another (its script and challenge_token field are added when needed), and repeated attempts are refused with retry_later. With each signup the platform stores proof of consent built on the server: the consent_label the page showed in that language (or the platform default label), its version, the language, time, IP address, user agent and page. Show the label exactly as the setting provides it. Do not confuse this form with newsletter subscription or /cart/notify-me restock alerts. See Gate actions for all outcomes.

Password unlock#

POST /password unlocks storefront access; /unlock is an equivalent route. This is separate from customer sign-in. Submit password, locale and the platform-provided return_to. A successful unlock sets the access cookie and redirects with HTTP 302 to the validated destination. Missing, incorrect, temporarily unavailable and rate-limited attempts re-render the password page with localized settings.password_error; rate limiting uses HTTP 429. Show the returned error even when the response is not 2xx.

vascula
{% if settings.password_error != blank %}
  <p role="alert" data-password-error>{{ settings.password_error }}</p>
{% endif %}
<form method="post" action="/password">
  <input type="hidden" name="_csrf" value="{{ csrf_token }}">
  <input type="hidden" name="locale" value="{{ settings.locale }}">
  <input type="hidden" name="return_to" value="{{ settings.return_to }}">
  <label>{{ settings.password_placeholder }}
    <input type="password" name="password" autocomplete="current-password" required>
  </label>
  <button type="submit">{{ settings.button_label }}</button>
</form>

New or changed storefront passwords have a minimum of 8 characters (the existing maximum is 72 bytes). That is the merchant’s password-setting rule. An unlock form should require a value and let the server verify it; avoid imposing the new minimum on attempts to unlock an older saved password. Do not trim or alter the password.

Progressive enhancement and returned HTML#

Both forms work as plain HTML POSTs. They are not Vascula hosted-action form kinds and do not promise the standard {success, response, user_errors} JSON envelope. If adding fetch, request text/html, use same-origin credentials, follow redirects, and parse the returned document even for 429/503. Show its success/error nodes and refresh the form’s _csrf field if present. The flash is consumed by that redirected GET: reloading immediately can lose it. A 2xx response or a followed redirect alone never proves signup or unlock succeeded. Expose settings.notify_state on a theme-owned data attribute so enhancement code can read the exact outcome. If no recognized outcome or destination is present, keep the form usable and show a retry message.

Maintenance: HTTP 503 and checking for return#

Public maintenance responses use HTTP 503 Service Unavailable with Retry-After. This communicates a temporary outage to clients and crawlers; do not treat it as a broken theme. If implementing a “Check again” control, retain a normal link for visitors without JavaScript. For optional background checks, request the current storefront URL with cache: "no-store", honor Retry-After (seconds or an HTTP date), and space requests out, for example at least 30 seconds. Pause while the document is hidden.

vascula
<section data-theme-gate="maintenance">
  <h1>{{ settings.title }}</h1>
  <p>{{ settings.message }}</p>
  <a href="">{{ 'gate.check_again' | t }}</a>
</section>

Choose your own theme marker, such as data-theme-gate="maintenance", and inspect the returned HTML. The maintenance marker means “still in maintenance”, whether the HTTP status is 200 or 503. Absence of that marker is not sufficient: also require a known storefront document marker supplied by your theme on destination pages, or a recognized platform storefront document. A proxy error, blank response, network failure, or unrelated HTML must not be announced as “back”. A known storefront page without the maintenance marker means maintenance has ended; it may now show a password or coming-soon gate, so describe the actual destination. Offer a Continue link instead of forcing a reload. These markers are theme conventions, not platform attributes.

Status codes and background requests#

Gate

Page request

Background request (JSON)

Search engines

Password

Password page, HTTP 200.

HTTP 401, STOREFRONT_PASSWORD_REQUIRED.

X-Robots-Tag: noindex, nofollow; robots.txt disallows crawling and the sitemap is not served.

Coming Soon

Coming-soon page, HTTP 200 (HTTP 403 for a refused form post).

HTTP 403, STOREFRONT_COMING_SOON.

X-Robots-Tag: noindex, nofollow; robots.txt disallows crawling and the sitemap is not served.

Maintenance

Maintenance page, HTTP 503 with Retry-After: 3600.

HTTP 503 with Retry-After: 3600, STOREFRONT_MAINTENANCE.

No noindex: the 503 marks the outage as temporary. robots.txt and the sitemap stay as they are when live.

A background request is a fetch or XMLHttpRequest (Sec-Fetch-Mode other than navigate, or X-Requested-With), a request that asks for JSON (Accept, Content-Type or _format=json), runtime and app-surface actions, search suggest, and the checkout status poll (?poll=1). It gets the runtime action envelope instead of HTML: {"success": false, "storefront_status": "password"|"coming_soon"|"maintenance", "user_errors": [{"code", "message"}]} with a localized message. Theme scripts should stop and reload the page (location.reload()) on these codes so the visitor sees the gate page. A request with Accept: */* alone, as crawlers and curl send, gets the page.

Coming Soon closes the catalogue and commerce: product, collection and search pages, search suggest, cart, express checkout, runtime actions and hosted actions other than the gate page’s own (/notify, newsletter signup and confirm, consent, language and country, logout). Maintenance closes the same routes and new checkouts, but orders already placed can be completed: the checkout return (including ?poll=1), /pay/ links, order confirmation and order status pages, and order lookup stay open. In both modes unsubscribe, text opt-out and privacy-request links keep working. A storefront password does not block links that carry their own authorisation: the checkout return (including ?poll=1), /pay/ links, order confirmation and order status pages, newsletter confirmation and unsubscribe links, text opt-out and privacy requests; these responses are never stored at the edge and send X-Robots-Tag: noindex. Connected product feeds stay public while a password is on. Every gate response is Cache-Control: private, no-store and is never stored at the edge.

OPTIONS requests are answered with HTTP 204 and no body. A page path serves only GET and HEAD: any other method on a path without its own action route gets HTTP 405. Shoppers see a status or password change on their next request. Verified search bots and AI crawlers can still be served a previously stored copy of a page for up to about a minute after the change, while the change reaches every edge location.

Staff and editor previews#

The platform owns staff access and its preview bar. A signed-in staff member who opens Preview store sees the real store while it is in maintenance, coming soon or password protected, with a slim bar at the top (#tf-staff-preview, data-gate = maintenance, coming_soon or password) and an Exit preview button that posts to /__staff-access/exit. The bar is injected right after <body> with inline styles, and staff responses are Cache-Control: private, no-store with X-Robots-Tag: noindex, nofollow. Preserve that UI and allow space for it in fixed or sticky layouts; do not hide or restyle it. Staff access bypasses the status and password gates; an authenticated staff view is not proof of the public visitor experience. Verify all three surfaces in a fresh private browser session as well as the editor. design_mode identifies editor rendering, not a staff authorization signal. Themes must not implement their own staff bypass.

Local fixtures and checks#

bash
tringify-theme context . --page coming_soon --state ok
tringify-theme context . --page coming_soon --state consent_required
tringify-theme context . --page password --state incorrect
tringify-theme context . --page maintenance

The tooling preview includes default and all five notify outcomes; password states are default, required, incorrect, retry_later, and unavailable; maintenance has default. theme context returns minimal gate context and a separate section_settings runtime overlay applied to required gate sections. Fixtures use English platform copy, with your demo locale preserved for form routing. They do not send forms, set access cookies, or simulate HTTP outages. Test language overrides, schema-default collection, explicit false, consent, rate limiting, and 503/Retry-After on a development store. themecheck warns with gate-text when a required gate section does not read title/message and notify-consent when its notify form lacks explicit checkbox consent. These are advisory source checks; inspect dynamic markup yourself.

For shoppers and checked search crawlers, reuse of a stored page requires the current storefront access version. A committed password or mode change takes effect on the next version check. AI crawlers served through the separate eventual-consistency path can still see a previously stored page while version updates propagate.