Loopwise Docs
Guides

Loopwise Pages

How Loopwise Pages deploys work — the deployment lifecycle, preview surfaces, why assets redirect to *.loopwise.page, how a page's route wins over built-in routes, propagation timing, and the real size limits.

Loopwise Pages lets you deploy a directory of static files (HTML/CSS/JS, plus a Vite build for SPAs) to a path on an organization's storefront — a landing page, a microsite, or a full single-page app. This page is the contract for how a deployment behaves: how it goes live, where you can preview it, why asset requests redirect off-origin, what wins when a page's path collides with a built-in route, how long changes take to propagate, and the size limits that are actually enforced.

You deploy either with the Loopwise CLI (loopwise pages push) or with the deploy_static_page / prepare_deployment MCP tools. The lifecycle below is the same for both, but note the size limits differ between the single-shot and multiphase paths (see Size limits).

Deployment lifecycle

A deployment is an immutable snapshot. Uploading a new bundle never changes what visitors see on its own — but how it goes live depends on is_preview. A non-preview deployment (the CLI and MCP default) goes live on completion, with no separate promote_deployment step. A preview deployment stays hidden behind the current live one until you promote it.

  • Prepare / push — the CLI's pages push walks the directory and uploads it; the multi-phase MCP flow is prepare_deploymentupload_asset × N → complete_deployment. deploy_static_page collapses all of that into one call for small bundles.
  • Preview vs live — deploying with --preview (CLI) or is_preview: true (MCP) marks the deployment as a preview. The page keeps serving its previous live deployment at its path_prefix, so a preview alone never changes what visitors see.
  • Promotepromote_deployment cuts live traffic over to a preview deployment. Rollback is the same operation pointed at an earlier deployment — each one is a complete snapshot. The switch itself is a single pointer update, but it is not visible everywhere at once: allow for the propagation window below.

Loopwise Pages is available to every organization — there is no plan gating on deploying or serving a page.

Preview surfaces

A deployment can be reached from two different hosts, and they behave differently. Pick the one that matches what you're testing.

SurfaceURL shapeAuth / cart / member contentCache
Tenant origin<tenant-origin>/_preview/<scriptName>/?pt=…Full — same-origin to the storefront BFF, so login, cart, and member-gated reads workNot cached (no-store)
Loopwise originhttps://<scriptName>.loopwise.pageAnonymous reads only — cross-origin to the tenantEdge-cacheable

The tenant-origin preview carries a signed pt token and is the URL you share for QA. The MCP deploy tools return it as preview_url, with the loopwise.page cross-origin fallback as preview_url_loopwise_origin. Use the tenant-origin surface whenever you need to test anything that depends on the member session (login, cart, gated content); use the loopwise.page surface for a shareable, anonymous preview before the organization has its own domain.

Both preview surfaces render the bundle without the storefront header/footer — that is true for embedded pages too. (The tenant-origin preview also injects a <base href="/_preview/<scriptName>/"> into HTML that has no author-provided <base>, so relative asset paths resolve — it is not a byte-for-byte copy.) A preview without header/footer is not evidence that embedded rendering is broken; an embedded page only renders inside the organization's chrome once a non-preview deployment is live.

Why assets redirect to *.loopwise.page

Every tenant host — *.kaik.io, *.myteachify.com, and a school's own custom domain — is served today through the Next.js storefront path. The asset 302 redirect and the root-page /-only match described here apply on all of them.

On a live deployment served over the Next.js storefront path (the shared *.myteachify.com / *.kaik.io origin, and any custom domain), the document is served same-origin, and requests for static asset files — those with a recognized asset extension (.css, .js, images, fonts, etc.) — 302-redirect to https://<scriptName>.loopwise.page/…. The redirect only changes where the asset bytes come from (the Cloudflare CDN); it is a 302 (not 301) because the scriptName changes on every deployment, and it carries Cache-Control: no-store so no intermediary pins a redirect to a now-stale scriptName. Only extensionless paths — including the routes that serve your HTML documents (for example / or /about) — and paths whose extension is not on the static-asset list (for example .csv) are served same-origin. A path with a recognized extension is redirected regardless of what it holds: .html, .htm, .json, .xml, and .txt are on the static-asset list too, so navigating directly to /about.html lands on loopwise.page and makes the document itself cross-origin. Link between pages with extensionless routes to keep the document same-origin.

The redirect does not change a script's execution origin. A <script> on a live tenant-origin document executes in that document's origin regardless of where its bytes were fetched from, so a root-relative fetch('/…') resolves against the tenant host and carries the member session — authenticated calls work on live pages with no special handling.

The cross-origin caveat applies only to the loopwise.page document surface: if you open the page itself at https://<scriptName>.loopwise.page, the document origin is loopwise.page, so calls back to the tenant are cross-origin and anonymous. The tenant-origin preview (/_preview/<scriptName>/…) keeps the document same-origin (and proxies its relative assets with no-store), which is why it is the surface for testing authenticated flows.

Use the loopwise.page host only for anonymous, shareable previews. For anything that depends on the member session (login, cart, gated content), keep the document on the tenant origin — the live URL or the tenant-origin preview (/_preview/<scriptName>/) — and use the Loopwise SDK for API calls. Do not open the document on loopwise.page and expect an authenticated session.

Route priority: what wins over built-in routes

A page mounted at a path_prefix shadows the storefront's built-in route at that path. Three rules decide who serves a request:

  1. Tenant URL redirects run first — except on origin-bound paths. For requests the storefront middleware handles, a configured redirect normally takes precedence over both a deployed page and a built-in route. The exception is a set of machine-readable, origin-bound paths that redirects never rewrite: /mcp and /mcp/…, /oauth/…, /.well-known/…, /skills/…, /account/graphql, and the discovery files /llms.txt, /llms-full.txt, /openapi.json, /auth.md, /pricing.md. /api/… is exempt for a different reason: the middleware matcher excludes it (api(?:/|$)), so those requests never enter findAndApplyRedirect at all — a redirect can never win for an /api/… path.

  2. A deployed page shadows built-in routes — but can only mount outside the reserved prefixes. You cannot create a page under a reserved system prefix: the backend rejects the mount at write time with "is reserved by the platform and cannot be used" (Page#path_prefix_not_reserved), so there is no way to end up with a silently unreachable mount. The reserved prefixes are:

    /api, /admin, /oauth, /auth, /cable, /graphql, /assets, /locales, /mcp, /account, /developer-portal, /developers, /_next, /_preview, /lw-preview, /_lw, /lw-pages, /.well-known

  3. The root path matches only /. A page mounted at path_prefix: "/" replaces the homepage. It matches only /, so built-in routes such as /courses and /checkout keep working and are never captured by a root page.

So a page mounted at /faq serves /faq in place of any built-in /faq, but a request to a reserved prefix such as /api/... or /oauth/... always reaches the platform. A path that is not reserved (for example /login) can be mounted by a page and will then shadow the built-in route — only the root page is restricted to an exact / match.

The reserved list lives in two places that are kept in step but do not mirror exactly: the backend (Page::RESERVED_PATH_PREFIXES) is authoritative for what you can mount, while the frontend (lib/loopwise-pages/excluded-prefixes.ts) omits /_lw on purpose — that namespace is handled by the frontend's mount-independent asset branch instead of being skipped.

Shadowing also moves the agent-facing surfaces

Shadowing a built-in route swaps out more than the HTML page. When a page mounts at, say, /faq, the platform stops treating that path as its own:

  • The markdown twin no longer serves built-in content. A request for /faq.md 307s to your deployed /faq; a request to /faq with Accept: text/markdown is served straight from your deployment by the Loopwise route match (no redirect). Either way an agent lands on the same page humans see, not the built-in FAQ markdown.
  • llms.txt drops the built-in twin. The per-tenant llms.txt / llms-full.txt stop advertising the shadowed path's built-in .md twin, because it would only redirect back onto your page.
  • Built-in structured data is gone. The built-in FAQ page emits FAQPage JSON-LD; a deployed page renders none unless you include it. If you rely on rich results or want agents to read structured data, embed your own JSON-LD in the page's HTML — a standalone (full-page) deployment's <script type="application/ld+json"> is served through untouched.

The twins reconciled in llms.txt are the concrete built-ins (/, /faq, /courses, /lecturers, /search) plus any per-slug family whose fixed ancestor you mount — mounting /courses also drops /courses/:slug, because the built-in course-detail markdown is no longer served under that prefix. What a .md request does depends on where it sits relative to the mount:

  • The mount's own twin (/faq.md for a /faq mount, or /courses/foo.md when /courses/foo is itself the mounted leaf) 307s to the HTML path.
  • Anything inside a mounted subtree (/courses/foo.md under a /courses mount, /faq/index.md under a /faq mount) is served straight from your deployment like every other path you own there, including .md files you deploy yourself — no built-in twin is involved, so there is nothing to redirect.

What llms.txt can't decide for a single-slug mount is whether to drop the whole /courses/:slug pattern, so it keeps the pattern unless the fixed ancestor itself is mounted.

Propagation timing

Promoting a deployment (or deploying a non-preview) is not instantaneous end-to-end. The route table and the CDN each hold the previous state briefly. The edge dispatch worker starts routing within ~30 seconds (Cloudflare KV), but a custom production domain is served through a CloudFront-cached lookup. Promote and rollback actively invalidate that lookup entry, so it normally refreshes within ~1–2 minutes; if the invalidation is delayed or retried (transient CloudFront throttling backs off progressively, up to ~10 minutes), the previous route can stay visible for up to the CloudFront TTL (~10 minutes). Allow that longer window before judging the result, and verify from a fresh or incognito session so you're not seeing a locally cached response. When verifying a live deploy, request <tenant-origin><path_prefix> directly (for example with curl).

Size limits

The limit that applies depends on how you deploy. Both paths cap a deployment at 10,000 files, cap any single file at 25 MiB (Cloudflare Workers Assets rejects larger files on every path), and require an index.html at the bundle root.

Deploy pathLimit
Single-shot — CLI pages push, MCP deploy_static_page100 MB request body, 25 MiB per file. The whole bundle is sent as one JSON request; binary files are base64-encoded and count ~33% larger than their raw size.
Multiphase — MCP prepare_deploymentupload_assetcomplete_deployment100 MB raw total, 25 MiB per file (raw staged bytes, no base64 inflation).

For a large binary-heavy bundle, prefer the multiphase flow — the single-shot 100 MB is measured on the base64-expanded request, so its effective raw capacity is roughly 75 MB.

Images: the MCP tools expose an image pipeline (upload_imageconfirm_upload) that returns a CDN URL — reference that URL in your HTML instead of bundling the bytes, which keeps the request small and applies metadata stripping. The CLI has no image command, so pages push bundles image files directly; keep them within the single-shot request-body limit above.

On this page