docs
/
AI skills

How a site's addresses route — pages, features and 404s

How a URL on an Appmint-hosted site becomes what renders — a built-in screen, a switched-on feature, a stored page, the home page or a 404 — what turning a feature on or choosing its page really does, which engine emails link where, and how to find out why a link shows "page not found".

Every URL on a hosted site is answered by one of four things: a built-in screen, a switched-on feature, a stored page, or nothing (a 404). Get the owner of the URL right before changing anything.

This page is about who owns an address. To build the page itself, see Build a website page; for a shop of your own design, Catalog pages from your products; for a form's link, Shared form from a collection. The site record's fields are in Sites, and page shapes in Pages and rendering.

The model in plain words

  • A feature that's on owns its address. Blog on means /blog shows the built-in blog. No page is needed.
  • Choosing a page for a feature moves the feature to that page's address and makes that page its template. The page's first screen is drawn above the feature and its second screen below. The old default address is freed for a normal page.
  • A feature that's off gives the address back to normal pages. If no page has that address either, it's a 404. That 404 is correct.
  • A page answers at /<slug> and at /<name>, ignoring case. The first URL segment decides; the rest is ignored for ordinary pages.

Forms, Unsubscribe and a few others bend these rules. They are listed under Exceptions.

Worked example: Blog

Tested on a local site. "Page blog" means a page record named blog; news stands for any other page.

Blog switchPage blog existsBlog's page in Site Features/blog shows/news showsWhy
onnononethe built-in blog (200)the news pageOn, at its default address.
offnonone404the news pageOff, and no page has the address.
onyesnonethe built-in blog, with the blog page's first screen above it and second below (200)the news pageThe feature wins its address; the page becomes its wrapper.
offyesnonethe blog page (200)the news pageOff gives the address back.
onyesnewsthe blog page (200)the built-in blog inside the news page (200); /news/<post> opens a postChoosing news moved the blog there and freed /blog.
row kept with enabled: falseyesnewsthe blog pagethe news pageOff keeps the choice for later but claims nothing.

The live example: Storefront

On PhaZZer's local site, data.features is ["e-commerce"] (e-commerce is the old name for Storefront):

  • /store renders the built-in catalog (200). There is no page named store; the catalog is drawn inside the site's account-layout page.
  • PhaZZer's own products and product pages are separate addresses, /products and /product. They render as themselves (200). The built-in store does not touch them.
  • Use my own page in Site Features opens a page picker. Picking products writes { "feature": "storefront", "enabled": true, "page": "products" }. Tested: /store became a 404, /products rendered the built-in catalog inside the products page, /products/checkout was the store's checkout, and /store/checkout became a 404. The top-level /checkout stayed at 200.

The store's own addresses hang off its page: /<store>/<product slug> is a product, /<store>/checkout, /<store>/checkout/success and /<store>/order/<id>. A product slug that doesn't exist falls back to the catalog with a 200, not a 404. Categories and search are query parameters on the catalog (?categories=, ?tags=, ?keyword=, ?sort=).

Ask first

  • Which URL, exactly, and from where? The full address, including the host and the query string. A link from an email: which email (form invitation, booking confirmation, invoice, gift card).
  • Which site? An org can have several. Engine emails use the org's default site (step 1).
  • What should be at that address? The built-in feature, their own page, or nothing. Never guess this: moving a feature changes which page a live URL shows.
  • For a feature: the default address, or their own page? If their own page, which one. Its screens will wrap the feature and its own content will not show there as a page.

How a request is answered

For a request to <host>/<path>, the site checks these in order. The first match answers.

  1. The site, from the host. The host is looked up among the platform's domain records (www. is stripped). The platform host is <site>-<org>.site.appmint.app, set as the site's data.hostName when the site is made. A custom domain is data.domain. Locally it's <site>-<org>.localhost:3200. An unknown host falls back to the org's site flagged data.isDefault. No site at all: a plain 404. (appengine site.service.ts getSite 88, getSiteByDomainName 467; base-app lib/get-page-data.ts 149)
  2. A fixed route. /login, /account, /checkout, /api/… and the others in Routes that are not features. These are their own Next.js routes, so a stored page with the same name never shows.
  3. /<name>.html is redirected (301) to /<name>; /index.html goes to /. (base-app src/middleware.ts)
  4. A static prefix. A first segment that starts with _next, assets, favicon.ico, robots.txt, sitemap, site.webmanifest or tt renders a blank page with a 200. So /sitemap-page, /assets-guide and /ttravel can never be pages. (base-app lib-client/helpers.ts 3, [[...slug]]/page.tsx 256)
  5. The stored page is looked up by the first segment: a page on this site whose slug or name matches, ignoring case, or whose id matches. / and /index look up site.data.homePage instead. With publishedPagesOnly: true, only published copies count. (appengine site.service.ts getPage 1054–1085, findPage 1170)
  6. The features are asked, in a fixed order, and the first one that claims the URL wins: post preview, Storefront, Payment, Support Tickets, Service Menu, Tables, Forms, Blog, Unsubscribe, promotion sign-up, Gift Cards, Reservations, Rental, Content Player, Community. (base-app [[...slug]]/page.tsx 114–132)
  7. What renders:
    • A feature claimed it and a page with content was found: the page wraps the feature. Its first screen goes above, its second below. A page with one screen, or only data.html, is drawn whole above the feature. (page.tsx 293–294; build-studio/html-renderer.tsx 50–75)
    • A feature claimed it and no page was found: the feature renders inside the page chosen for it, else inside account-layout, else bare. (page.tsx 158–174, 303)
    • No feature, and a page with content: the page.
    • No feature, no page: 404, drawn as the site's own not-found screen (its name, logo and description). (page.tsx 296–302; app/not-found.tsx)

A page record with nothing in it to draw is meant to count as missing ([[...slug]]/feature-template.ts hasRenderableBody 10–18), but on a site with a root page it doesn't: the check also counts root, so an empty page is a blank 200 with no header, and an empty page at a feature's address strips the feature's header and footer. Tested. See Built-in pages and templates.

How a feature claims a URL. It must be on, and the first URL segment, or the name of the page the lookup found, must equal the feature's page. The feature's page is the row's page, else the old per-feature setting (e.g. site.data.storefront.storePage), else the default address. (base-app lib/site-feature-pages.ts isFeatureRoute 107–119, getFeaturePage 93–99, LEGACY 50–57)

When a feature is on. site.data.features comes in two shapes, and both work:

ShapeExampleOn when
A bare name (older)["e-commerce", "forms"]the name is in the list
A row (what Studio writes)[{ "feature": "storefront", "enabled": true, "page": "products" }]the row exists and enabled isn't false

e-commerce means storefront, and ticket-support means ticket. (site-feature-pages.ts ALIASES 44–47, isFeatureEnabled 86–90)

The engine reads the list more narrowly than the site does. When it builds a link (featurePageUrl, appengine site.service.ts 1498–1515), it only counts a row, under the exact key. Aliases don't count. Bare names only count for forms. It only knows a default page for forms (site.data.forms.page, else form) and reservations (book). For every other feature, the row must name a page, or the engine has no link to give. A site can serve /pay happily while the engine says "No Payment page is configured".

The features

All 15 features in Studio's Site Features, plus the feature routes the code has that Studio doesn't list. "Tested" means checked on the local PhaZZer site with the status shown.

Studio nameKey(s)Default addressPage needed?Choosing a pageEngine links and emails that point here
Blogblog/blog, /blog/<post>No. Tested: 200 on, 404 off.Moves the address; the page is the template. Tested.None found.
Paymentpayment/pay, /pay/<anything>No for the site (tested 200). Yes for engine links.Moves the address. Tested: /pay 404, /<page> 200.Invoice payment link <page>?invoiceNumber=<n>. Payment request <page>?ref=<id>. The receipt's "open it again" link. Without a row with a page: invoices use the platform's hosted /checkout/payment?orgid=<org>, and a payment request is refused with 400 "No Payment page is configured on the site". (invoice.service.ts 1686–1701, storefront.payment.service.ts 59, 211–215, 335–343)
Service Menuservice-menu/servicesNo. Tested 200.Moves the address. Not tested.None found.
Formsforms/form, /form/<form name>, /form?form=<name>No. Tested: 200 on, 404 off.Both addresses answer, and the page is /form's template. Tested. See Exceptions.Form invitations <page>?form=<name>&code=<code>. Access link and code emails <page>?form=<name>&token=<token>. Signing requests <page>?form=<name>&sign=<token>. Blueprint form links. GET /site/base-url?feature=forms. With Forms off, the emails fall back to <site>/form, which is a 404 on that site. (crm-form.service.ts 456–459, 540–548; crm.signed.document.ts 861; blueprint.service.ts 784–790; site.controller.ts 136–143)
Live Chatlive-chatnone: it's a widgetNo address.Studio offers no page picker.None. When on, the chat widget loads on pages. (base-app page-client.tsx 23)
Communitycommunity/communityNo. Tested 200.Moves the address. Not tested.None found.
Unsubscribeunsubscribe/unsubscribeNo. Tested: 200 even when off.Moves the address. Not tested.After a broadcast unsubscribe, the person lands on <page>?status=…, or on <site>/unsubscribe without a row with a page. The unsubscribe link itself goes to <site>/api/broadcast/unsubscribe. (broadcast.service.ts 216–220, 389–405)
Gift Cardsgift-card/gift-cardNo. Tested: 200 on, 404 off.Moves the address. Not tested.Gift card emails <page>?code=<code>. Without a row with a page they use <site>/gift-card?code=…, which is a 404 while Gift Cards is off. (giftcard.service.ts 2547–2558)
Storefrontstorefront = e-commerce/store, /store/<product>, /store/checkout, /store/checkout/success, /store/order/<id>No. Tested 200.Moves the whole store. Tested.Gift card emails' shop link (needs a storefront row with a page). Store links from storefront.url.service.ts 47 read only the old site.data.storefront.storePage.
Support Ticketsticket = ticket-support/support, /support/new, /support/track, /support/<ticket number>No. Tested 200.Moves the address. Not tested.None found.
Tablestables/tableNo. Tested 200.Moves the address. Not tested.None found.
Affiliate Marketingaffiliate-marketingnone—Studio offers a page picker, but the site has no route for it and never reads this switch. Choosing a page does nothing to routing.None.
Reservationsreservations/bookNo. Tested: 200 on, 404 off.Moves the address. Tested: /book 404, /<page> 200.Booking confirmations and notices link to the row's <page> (or book for a row with none), else <site>/book. That's a 404 while Reservations is off. Blueprint booking links need a reservations row. (reservations.service.ts 509–517, blueprint.service.ts 785)
Audio Playeraudio-playernone: it's a widgetNo address.Studio offers no page picker.None. The site never reads this switch; the bottom player shows unless a page hides the audio-player control. (base-app components/layout/AppChrome.tsx 71)
Content Playercontent-player/content-player, /content-player/<post>/<item>No. Tested 200.Moves the address. Not tested.Content Studio participant invitations <page>/<post>?code=<code>, and course links from training <page>/<post>. Without a row with a page: invitations use <site>/content-player/…, and training course links are left out. (content-studio.service.ts 80–92, 128–139; post-course.service.ts 146–148)
Rental (not in Studio's list)rental/rentalNo. Tested 200.Moves the address. Not tested.None found. Studio lists it only if the record already carries it.
Promotion sign-up (no switch)—/join?promotion=<promotion name>, /confirm-subscription?token=<t>No. Tested: /join?promotion=x 200, /join 404.Not configurable. With Storefront on, it borrows the store page as its template.The double opt-in email (promotion-subscribed) links <site>/confirm-subscription?token=<t>; the promotion emails' unsubscribe link is <site>/unsubscribe?email=…&promotion=<name>; Studio's Copy sign-up link gives <site base url>/join?promotion=<name>. (appengine resources/email-templates/promotion-*.html; Studio promotions/promotion-list.tsx 80–86) See Promotions and the sign-up page.
Post preview (no switch)—/__preview/post/<slug or id>?key=…No.—Content Studio's Preview button.

A page you name in a row doesn't have to exist. The feature answers at /<that name> either way, inside account-layout when there's no page to wrap it.

Two features pointed at the same page: the one earlier in the order wins (step 6 above). Tested: Blog and Content Player both set to one page, and Blog rendered.

Routes that are not features

These are fixed. No setting moves them, and no page can take their address.

AddressWhat it is
/login, /register, /forgot-password, /reset-password, /login/error, /auth/…, /demoSign-in screens. Tested /login and /register: 200.
/accountRedirects to /account/overview. Signed out, any /account/… redirects to /login?redirect=…. Tested: 307.
/account/<section>A section of the customer portal: overview, orders, reservations, tickets, forms, profile and the rest in app/account/[slug]/page.tsx 30–63. An unknown section is a 404. A section switched off in Site Config › My Account redirects to /account/overview (85).
/checkout, /checkout/successCart checkout. Needs Storefront or Rental on, else redirects to / (app/checkout/page.tsx 22–24). Tested: 200 with Storefront on.
/order/track, /order/<email>/<number>Order tracking. Tested /order/track: 200.
/api/…The site's API proxy. It adds the org and the site's app token; custom request headers are not passed on.
/sitemap.xml, /sitemap-pages.xml, /robots.txtBuilt per site. Tested: 200.
/favicon.ico, /favicon.png, /logo.png, /apple-touch-icon.pngFrom the site record.
/__preview/page/<site>/<page name, slug or id>Preview of a page through the site's own pipeline. It checks no sign-in, and on a site with publishedPagesOnly: true it shows only the live copy (a page never published is a 404). See Publish, unpublish and restore a page. Tested: 200.
/ and /indexThe page named by site.data.homePage, matched by name, slug or id. Studio's picker stores the page's name. If nothing matches, / is a 404. Tested.

For the shared header and footer around these screens, see Shared header and footer.

Exceptions

Forms: the chosen page is a template, and /form keeps answering. With Forms on, /form always belongs to the form feature, whatever page is chosen. The chosen page also answers, and its screens wrap the form at both addresses. (base-app components/schema-form/SchemaFormRoute.tsx isSchemaFormRoute 94–107; [[...slug]]/page.tsx 289–292). Tested with a page chosen: /form and /<page> both 200, and both drew the form inside that page. The engine's form emails link to the chosen page, not /form (site.service.ts 1509). So pointing Forms at a page the site uses for something else, such as its own registration page, takes that address away from the page: /registration would show the generic form, and the page's own content would only be the frame around it. Leave Forms on its default unless the owner wants a page that exists only to frame forms.

Forms: a page with data.formName set is always a form, at any address, while Forms is on (SchemaFormRoute.tsx 103).

Unsubscribe answers /unsubscribe even when it's off, so links already sitting in inboxes keep working (components/unsubscribe/UnsubscribeRoute.tsx 44–46). A page you put at /unsubscribe never shows as a page; it only frames the unsubscribe panel (first screen above, second below). Tested: 200 with the feature off.

Storefront can be opted out entirely with site.data.storefront.template: "none", even while the switch is on (components/storefront/StorefrontRoute.tsx 38). Under the store, checkout and order are never product slugs (54), and an unknown product slug shows the catalog with a 200 (135).

Promotion sign-up and post preview have no switch. /join?promotion=…, /confirm-subscription?token=… and /__preview/post/… answer on every site (components/promotion-signup/PromotionSignupRoute.tsx 16–26; [[...slug]]/page.tsx 116). A page named join only shows when the URL has no promotion parameter.

Studio shows an address only for Storefront, Unsubscribe and Gift Cards (welcome-screens/buttons/site-features-section.tsx 72). For the rest it says "Choose page" while the feature is already live at its default (/blog, /book, /form, /pay…). The code comment there saying "there is no default path" (292) is wrong. Trust the table above.

"Link it from the footer / the menu" needs a row. The engine refuses a feature that's only a bare name ("Turn storefront on in Site Features first"), and refuses Storefront with no page chosen ("storefront has no page to link to"), because its built-in list knows only unsubscribe and gift-card (site.service.ts 1245, 1264–1266). Not tested: it edits every page of the site.

Steps

1. Site Config: read the site

Find the site with repo_find on site ({} for all; match the host to data.hostName or data.domain). Then read it whole with repo_find_one, because find results are partial. Note:

FieldWhy
data.namePages belong to the site by this name (data.site on each page).
data.featuresWhat's on, and each row's page. Note the shape: bare names, rows, or both.
data.homePageWhat / shows.
data.publishedPagesOnlyIf true, only published pages answer.
data.isDefaultThe site engine emails link to. With none flagged, the engine uses the oldest site.
data.storefront, data.forms, data.payment, data.ticket, data.serviceMenu, data.tablesOlder per-feature settings. The site still reads their page (or storePage) when the row names none.

call_appengine GET /site/base-url gives the origin engine links are built on. GET /site/base-url?feature=forms gives the forms link, or url: null with a reason when Forms is off.

Hands on: the site's id (sk), name, its features list as stored, and whether it's the default site. Check: open Studio at /app/welcome → Site Config → Features ("what's switched on"). The switches match data.features. In Build Studio the same drawer opens from the site switcher's "Site config for ".

2. Diagnose: why this URL shows what it shows

Take the URL's host and first path segment and work down. Stop at the first match.

#AskHowIf yes
1Is the host this site's?Step 1: data.hostName / data.domain.Go on. If no site has that host, the domain isn't mapped; that's a 404 for every path. See Connect a custom domain.
2Is it a fixed route or static prefix?The tables above.That screen answers. A page can't take it: rename the page.
3Is it / or /index?Find a page whose name, slug or id equals data.homePage.None: / is a 404. Fix homePage (Stop), or create the page.
4Does a feature claim the first segment?For each feature in the table: on? Its page = row page → old setting → default. Equal to the segment?The feature answers. A page there only wraps it.
5Is there a page for it?repo_find on page with { "data.site": "<name>", "$or": [{ "data.slug": "<seg>" }, { "data.name": "<seg>" }] }. Case doesn't matter on the site, so also try other casings. Then repo_find_one it.It answers, if it has content (screens with HTML, or data.html) and, on a publishedPagesOnly site, is published. An empty one is a 404.
6None of these—404, and it's correct.

Then work out what the person wanted:

FindingWhat to do
A feature's default address and the feature is off, no page (e.g. /form, /book, /gift-card)The 404 is right. Ask whether they want the feature on (step 3). Don't create a stand-in page.
The link came from an engine emailFind its builder in the features table. Check the feature is on on the default site, under the exact key, with a page in its row if that feature needs one. A link to a feature that's off is a 404 by design. Ask before switching it on.
A feature took over their page's addressEither move the feature to another page (step 3), or switch it off if they don't use it.
Their page is at the feature's chosen page, and its content is goneThat's the template rule. Choose a different page for the feature, or none.
The page exists but is empty, unpublished, or under another site's nameFix the page in Build Studio (Build a website page).
The link names a page by a slug that differs from its nameBoth work on the site. Check spelling only.
Wanted a new page at that addressBuild a website page. Pick an address nothing above owns.

Hands on: the owner of the URL (fixed route, feature, page or nothing) and the fix the person chose. Check: curl -s -o /dev/null -w "%{http_code}" <url> gives the status you predicted. Then look at the rendered page in a browser: the source contains page HTML in a script payload even when nothing is drawn.

3. Site Config: switch a feature on, off, or move it

Only when the person asked. Build the whole new list from the list you read in step 1:

  • Keep the shape. In a list of bare names, add the bare name ("reservations"). In a list of rows, add a row ({ "feature": "reservations", "enabled": true }). Mixing them is valid; the site reads both.
  • Use a row with a page when engine links need one: Payment, Gift Cards, Content Player, Storefront, or an Unsubscribe landing page. Use the Studio key (storefront, ticket), not the old alias.
  • To move a feature, set the row's page to the page's name. Remove page to go back to the default address.
  • To switch off and keep the choice, set enabled: false. To switch off fully, remove the name or row.

Write it with repo_update on site: { "datatype": "site", "id": "<sk>", "patch": { "features": [ …the whole new list… ] } }. repo_update merges patch into data one level deep, so the site's other settings stay. The same write by API is POST /repository/update-partial/site/<sk> with the dot-path key { "data.features": [ … ] }.

Never send { "data": { "features": [ … ] } } to update-partial. It replaces the whole data object, and the site loses its name, home page, domains and every other setting.

When the owner next changes any switch in Studio, Studio rewrites the whole list as rows and turns e-commerce into storefront (site-features-section.tsx 77–80, 125). That's expected.

Stop: this changes the live site. Moving a feature changes what an existing URL shows. Before writing, tell the person which addresses will change ("/book will be a 404; bookings move to /appointments, inside that page's header and footer"), and which emails will link where. Wait for yes.

Hands on: the feature's live address. Check: repo_find_one on the site shows the new data.features and everything else as it was. Site Config › Features shows the switch.

4. Check it on the site

  • Open the feature's address. It renders the feature, wrapped by its page or account-layout.
  • Open the old address if it moved. It now shows the page of that name, or 404.
  • For a feature engine emails use, check the link the engine would build. For Forms: GET /site/base-url?feature=forms. For the others, apply the rule in the warning under How a request is answered.
  • After a change, open the site's other main pages once to be sure none lost its address to the feature.

When it's done

Tell the person what owns the URL and why, in one line ("/form is the Forms feature, and Forms is off, so it's a 404"). If something changed, say which addresses changed, what's at each now, and which emails link there. Open Site Config › Features and the address on the site.

What it can't do yet

  • The page lookup treats the address as a pattern. /h.me opens the home page with a 200. A ( in the first segment gives a 500 instead of a 404. Tested. (appengine site.service.ts findPage 1170–1182)
  • Extra segments under an ordinary page are ignored. /home/anything/else shows the home page with a 200, never a 404.
  • A signing request for a document with no form links to <site>/sign?token=… (crm.signed.document.ts 864). The site has no /sign route, so that's a 404 unless the site has a page named sign.
  • Gift card emails fall back to /gift-card while Gift Cards is off, which is a 404. The engine's comment there says the store always answers it; it doesn't.
  • The engine only reads rows under the exact key. A site whose list says e-commerce, or a bare payment, reservations or gift-card, works on the site but gets no feature page in engine links (see each row's fallback).
  • Affiliate Marketing and Audio Player switches do nothing to the site. Live Chat only loads the widget.
  • A site can't show its own designed 404. The not-found screen is fixed: the site's name, logo and description.

Don'ts

  • Don't create a page to "fix" a 404 at a feature's address. Switch the feature on, or leave the 404.
  • Don't point a feature's page at a page the site uses for something else. Its content stops showing there. For Forms it also stops showing at its own address.
  • Don't trust Studio's missing address line: a feature that's on is live at its default address.
  • Don't send data: { … } to update-partial on a site. Use repo_update with patch, or dot-path keys.
  • Don't write one feature as a row and drop the rest of the list. Always send the whole list.
  • Don't put a page at a fixed route, a static prefix (sitemap…, assets…, tt…) or a feature's address.
  • Don't judge a route by a grep of its HTML source. Check the status, then look at it in a browser.