A site is one record (datatype: "site") plus everything that names it: its pages (data.site = the site's name), its host mappings (site_domain records in the platform's root org) and, on live, a route for each custom domain. The site record's fields are in Sites. Copying a single page is Copy a page as a template. Connecting a domain is Connect a custom domain. The site's title, description and icons are Site identity, SEO tags and link sharing. Languages and currencies are Site languages and currencies.
"Tested" below means checked on a local engine with skilltest-site-* sites, which were deleted afterwards. Everything else is read from the code, with the file named. Paths are appengine src/site/ unless they say otherwise.
Some of this area has bugs that change what you should do. Read What it can't do yet before you start. The two that matter most:
- A clone copies only 50 pages, fails with 409 when the source has a navigation, and takes the "default site" flag away from the source (step 4).
- No delete removes the site's pages. A new site with the same name gets the old pages back.
The ways a site is made
| Way | Who uses it | Call | What it does |
|---|---|---|---|
| Sign-up | the platform, when an org is created | createSite (org-management.service.ts 2562) | The org's first site, named <first three letters of the org id>site (phazzer → phasite), with the sign-up's site template. |
| Create site | the API only | POST /site/create-site (site.controller.ts 201–207); POST /site/create is the same with name for siteName (496–501, site.service.ts 1908–1925) | Everything below: plan check, name check, platform host and mapping, template, starter pages, default site, two emails. |
| New Site in Sites & Dev Environments | a person, at /site-management (websitemint site-management/modern-site-app.tsx 802–808, 1013–1085) | POST /site/add with { siteName, title, description, domainName } (modern-site-app.tsx 192–213 → site.service.ts 1697–1803) | A bare site: plan check, name check, platform mapping, one email. No starter pages, no template, no hostName, never made the default. |
| Add Site in System Operations | platform staff (system-operations/modern-companies-management.tsx 487–502) | POST /site/add with { siteName, siteOrgId, title, template } | As New Site. The template is saved on the record and never applied. |
| Clone | the API only; Studio has no button (modern-site-app.tsx 474–477 only shows a notice) | POST /site/clone-site (site.controller.ts 214–220 → site.service.ts 500–541) | A copy of another site's record, pages and navigation. No plan check, no host, no mapping, no email. |
| Apply template | the API only | POST /site/apply-template (site.controller.ts 642–648 → site.service.ts 543–627) | Copies a template site's pages into an existing site. |
| Bare record | the API only | PUT /repository/create with datatype: "site" (repositories/repository.crud.service.ts 962–976) | The record and nothing else. Name rules apply; no plan check. Tested. |
What each one does, side by side:
| create-site | add | clone-site | repository create | |
|---|---|---|---|---|
Plan's site limit (403 Free plan allows maximum 1 site(s)…) | yes, first | yes, first | no (tested) | no (tested) |
Name must match ^[a-z][a-z0-9-]*$ | yes, 422 | yes, 422 | yes, after lower-casing (tested: Skilltest Site Bad → 422 Invalid site name skilltest site bad) | yes, after lower-casing |
| Name already used in the org | 409 | 409 | updates that site instead (step 4) | 422 Site name already exist |
data.hostName | <name>-<org>.<SITE_HOST_NAME> (site.appmint.app on live; 235, 1219–1221) | not set. Saves data.sharedDomain = <name>-<org>.site.appmint.app instead (1722, 1735) | the source's hostName (tested) | whatever you send |
Host mapping (site_domain) for the platform host | yes (263–274) | yes (1743–1755) | none (tested) | none |
domainName given | saved as data.domain, mapped, route deployed | same | the source's data.domain, unmapped (tested) | — |
| Template applied | yes, before the starter pages (253) | no | — | no |
| Starter pages | yes (256) | no | the source's pages | no |
| Made the default site | when it is the org's only site (417) | no | copies the source's flag (see step 4) | if you send isDefault: true |
| Emails | "Website Publish Notification" to the org's email and [email protected], plus the site-created notification (294–319). Not for a BusinessMade org (291). | site-created (1777–1794) | none | none |
The site's name and hosts
- The name is the site's key. Pages belong to a site by its name (
data.site), and the platform host is built from it. It is lower case, starts with a letter, and holds onlya–z,0–9and-(site.service.ts216–220). It can't be changed: Site Config shows it read-only ("Set when the site was created — not editable", websitemintsite-config-form.tsx229, 749). To "rename" a site, clone it to the new name, fix the clone (step 5), then delete the old one (step 6). - The platform host is
<name>-<org>.site.appmint.appon live (<name>-<org>.localhost:3200on a local engine). It answers only when asite_domainmapping for it exists. Read a site's mappings withGET /site/get-site-host-mappings/<site name>. - A route (Kubernetes ingress) is deployed for every mapped name that doesn't end in
.site.appmint.app(registerSiteDomainName889–898). On live that means custom domains only. On an engine whoseSITE_HOST_NAMEis something else (a local one), every new site's platform host gets a route deployed too. homePagenames the page/shows, by name, slug or id. It defaults toindex. A site with no page by that name answers/with a 404. See Sites.- The default site (
data.isDefault, Site Config › Default site, "Serves any request that matches no other site",site-config-form.tsx802) answers any host that no mapping claims. Only one site in an org holds it: the field isexclusivein the site schema, so writingtrueon one site sets it tofalseon the others (repositories/repository.crud.service.ts1316–1370).
A site name that doesn't exist finds the default site. getSite falls back to the org's default site when no site has the name asked for (site.service.ts 111–114). Tested: GET /site/get-site/skilltest-site-nosuch answered with phasite. The calls that change a site (create, add, clone, remove, apply-template, Repair Site Domain) look it up by its exact name instead, and answer 404 for a name no site has (findSiteByName). Read the answer's data.name before acting on it, and always send the exact name.
Starter pages
Only createSite (sign-up and /site/create-site) seeds pages (seedStarterPages, 341–433). It copies two pages from sites in the platform's shared org (SHARED_ORG):
| Copied from | Saved as | Wired to | Picked |
|---|---|---|---|
site landing-pages | landing-page | data.homePage | one page at random |
site account-pages | base-layout | data.myAccount.layout | one page at random, among pages that can frame others: isRoot, two or more screens, or no <main> (account-layout-starter.ts) |
{{siteName}},{{siteTitle}},{{orgId}}and{{companyName}}in the copied pages are filled in on the way (361–366).companyNameis the org's display name, else its id; a new org has only its id.- The org's first site is made the default (417).
- It never throws. If the shared org has no such pages, the site gets none and
homePagestaysindex, so/is a 404. - The template's own home page is overridden. The template is applied first, then
homePageis set to the randomlanding-page. PointhomePageat the template's home page afterwards if that's what the person wanted.
The starter My Account layout is named base-layout, but the feature pages fall back to a page named account-layout. A new site's /blog, /book and other feature pages draw with no header or footer until it has one. See Built-in pages and templates.
Site templates
A site template is a pointer kept in the shared org: a flexdata record with data.application: "site-template" and data.content: { siteSk, orgId, siteName, title, description, image, … } (makeSiteTemplate, 644–713). Applying one reads the source site live, so fixing the source fixes the template.
Which template a name finds (resolveSiteTemplate, 715–741):
- In the shared org, a template whose
content.siteSkequals the name, or whosedata.nameis<org>-<site>. Its source site is read from its own org. - Otherwise a site in the same org, by name or id. Tested:
templateName: "skilltest-site-src"and the source site's id both worked.
What applying copies (copySiteContent, 565–600):
| Part | Copied |
|---|---|
| Pages | The first 50 (the most recently changed first), each whole, with its style and post, re-pointed at the target site. Same names and slugs as in the template. |
| Main and footer navigation | yes, as new records, which fails when the org already has one by that name (see step 4) |
data.theme, data.settings | meant to, but not saved. Tested: the target site's record was unchanged (version 0) after the apply. |
| Everything else on the template's record (home page, features, My Account layout, languages, Storefront settings) | no |
Apply template always says it worked. Every error is caught and logged (site.service.ts 621–625), and the answer is "Template '<name>' successfully applied to site <site>". Tested: a template that doesn't exist got that answer, and so did applying the same template twice, which copied nothing the second time (a page name already on the site makes the first page's create fail and stops the copy). Count the target's pages before and after.
Registering a template doesn't work today. Studio's Make Site Template button (shown only in the shared org, websitemint welcome-screens/buttons/site-action-buttons.tsx 34–50, 176–184) calls POST /tools/make-site-template, which doesn't exist (tested: 404 Cannot POST). The engine's POST /site/make-site-template passes the whole body as the site name, so it never finds the site (tested: 404 Site not found: [object object] in org phazzer).
The template gallery shows page templates, not site templates. The setup wizard's "Let's build your website" step and the Page templates view list the pages[] inside each registered template (setup-wizard/create-website-step.tsx 230–250) and copy one page with create-page-from-template. A template registered by today's code has no pages[], so it adds nothing to the gallery. Copying a page template is covered in Copy a page as a template.
Ask first
- New, copy, or template? A blank site, a copy of one of their sites (which one, by name), or their site with a template's pages added.
- The new site's name (lower case, letters, digits and hyphens, starting with a letter). It becomes part of the platform address and can't be changed later. Also its title and one-line description.
- Should it become the default site? Usually not: the default site answers every address nothing else claims.
- A custom domain? Connect it afterwards, with Connect a custom domain. Don't pass
domainNameat creation. - For a copy: what must change in it (domain, title, which pages), and whether the source must stay the default site.
- For a delete: the exact site name, and whether its pages, navigation and files should go too.
Steps
1. Sites & Dev Environments: what's there now
- The org's sites:
call_appengineGET /site/org-sites/<org>. Each hasdata.name,data.hostName,data.isDefault,data.homePage,data.domainandlinks(the addresses it answers on,site.service.ts182–207). - The plan's room:
GET /site/limits. Thesitesrow hasused,limitandstate(fullmeans no more sites). Tested on the local org:used 1, limit 1, state full. - For a copy, the full source record:
repo_find_oneonsitewith its id. NoteisDefault,domain,hostName,mainNavigation,footerNavigation,features,myAccount,storefront. - For a copy, its pages:
repo_findonpagewith{ "data.site": "<source>" }and a page size of 200. Count them.
Hands on: the site list, the plan's room, and for a copy the source record and its page count.
Check: Studio › Sites & Dev Environments (/site-management) shows the same sites.
Stop: if the plan is full, say so and stop. Adding a site needs a plan upgrade. Don't go around the limit with a clone or a bare record.
2. Sites & Dev Environments: create a blank site
Stop: creating a site emails the account holder (and, for create-site, Appmint support). Say so, and wait for yes.
Make it with call_appengine POST /site/create-site:
{ "siteName": "spring-shop", "title": "Spring Shop", "description": "Our spring pop-up shop.", "template": "<optional template name>" }siteOrgId defaults to the calling org. Another org's is refused with 403 Cannot change a site in another organization (assertOwnOrg). Every site route here is staff only (ContentAdmin or ConfigAdmin).
Answers to expect:
| Answer | Means |
|---|---|
200 { site, domainRecords, sharedDomain, message } | made. sharedDomain is the platform host. |
403 Free plan allows maximum N site(s)… | the plan is full (checked before anything else) |
422 Invalid site name… | fix the name |
409 Site <name> already exists in organization <org> | a site has that name |
A bare site record (for a copy by hand, step 4): make the record and give it its address:
PUT /repository/create{ "datatype": "site", "isNew": true, "data": { "name": "spring-shop", "title": "Spring Shop", "description": "…", "homePage": "index", "publishedPagesOnly": false } }. Don't sendisDefault,domainorhostName.POST /site/fix-domains{ "siteName": "spring-shop" }(Studio: Site Config › Address › Repair Site Domain). It maps<name>-<org>.site.appmint.appand writesdata.hostName(site.service.ts1942–2032). See Connect a custom domain.- Give it pages: step 3, or Build a website page.
This path has no starter pages and sends no email. Not tested here: the local engine's hosts would deploy a route.
Studio's New Site (POST /site/add) makes a site with no pages, no hostName and no template. Use the API call above instead.
Hands on: the new site's name, its id and its platform host.
Check: GET /site/get-site-host-mappings/<name> lists the platform host. GET /site/get-site/<name> answers with that name, not the default site's. Its pages: landing-page and base-layout (when the shared org has starters), plus the template's.
3. Build Studio: start from a template
To add a template site's pages to a site that exists:
POST /site/apply-template
{ "siteName": "spring-shop", "templateName": "<shared-org template name or siteSk, or a site of this org>" }Count the site's pages before and after, because the answer is the same whether it worked or not. Pages whose names the site already has stop the copy. Then set homePage to the template's home page (repo_update on site, patch: { "homePage": "<page name>" }). Copy any theme or settings by hand: they aren't copied.
For a single page from the platform's page templates, use Copy a page as a template.
Stop: on a site that serves drafts (publishedPagesOnly not true), the new pages are live at once. Name the URLs and wait for yes.
Hands on: the site's page list.
Check: repo_find on page with { "data.site": "<site>" } shows the template's pages. Open the home page on the site.
4. Sites & Dev Environments: clone a site
POST /site/clone-site:
{ "sourceOrgId": "<this org>", "sourceSiteName": "phasite", "destOrgId": "<this org>", "destSiteName": "phasite-redesign" }What it does (site.service.ts 500–541, 565–600, 786–833):
| Part | What the clone gets | Tested |
|---|---|---|
| The site record | all of the source's data, with only name changed: hostName, domain, isDefault, features, myAccount, storefront, languages, currencies, tracking, homePage… | yes: hostName and domain were the source's |
| Pages | the first 50 pages of the source, each whole (screens, html, style, post), with data.site changed, same names and slugs | yes: of 53 pages, the 3 oldest were left out; style and post came across |
| Page state | draft, whatever the source's was. A published copy isn't made. | yes |
| Main and footer navigation | new records with the same name. Navigation names are unique in an org, so this fails with 409 A navigation with name "…" already exists, after the site record was made and before any page was copied. | yes |
theme, settings | yes (they are in data) | yes |
| Host mappings | none. The clone's own platform address is a 404, and it borrows the source's hostName. | yes |
| Uploaded files | not copied. Pages still point at the source's file URLs, which keep working while those files exist. | code |
| Posts, products, forms, collections, CRM records | not copied. They belong to the org, not to a site. | code |
Into a site name that already exists, the clone replaces that site's data with the source's, renames each of its pages to old_<name> (first 50 only), then copies the source's pages in (site.service.ts 517–525, 786–796). Tested: the renamed page kept its slug, so it still answers at its old address.
Cloning the default site makes the clone the default. The copy carries isDefault: true, and that field is exclusive, so saving the clone sets it to false on the source (site schema isDefault.exclusive; repositories/repository.crud.service.ts 976, 1514). The source stops answering unmapped addresses. Read from the code; not tested, because the only default site on the test org was off limits.
So check before you clone, and pick the path:
| The source | Path |
|---|---|
≤ 50 pages, no mainNavigation or footerNavigation, not the default site | POST /site/clone-site, then step 5 |
| any of those fail | copy by hand: make the site as a bare record (step 2's bare record) with the source's data minus name, hostName, domain, isDefault, mainNavigation and footerNavigation, then copy each page as in Copy a page as a template with site set to the new name, then step 5 |
sourceOrgId and destOrgId must both be the calling org; another org's is refused with 403 Cannot change a site in another organization.
Hands on: the clone's name and id (repo_find on site by name; the clone's answer may not carry sk).
Check: the clone's page count equals the source's. Read the source again: its isDefault, version and page count are unchanged.
5. Site Config: fix what a clone copied
Each of these is a write to the clone only, except the last:
- Its own address. Clear the borrowed domain, then repair:
POST /repository/update-partial/site/<clone sk>{ "data.domain": "" }, thenPOST /site/fix-domains{ "siteName": "<clone>" }. Without the first, the clone's emails and links use the source's domain (tested:GET /site/base-url?siteName=<clone>answered the source's domain), and Repair tries to map the source's domain and reportsalready registered to another site. - Not the default site.
{ "data.isDefault": false }on the clone. If cloning took the flag from the source, write{ "data.isDefault": true }on the source, which also clears it on the clone. - Its title, description and Site Features, as the person asked.
featurescame from the source. Changing them is in Built-in pages and templates. - Missing pages. Copy any page past the first 50 with Copy a page as a template.
Use dot-path keys only. { "data": { … } } replaces the whole record's data.
Stop: writing the source's isDefault changes which site answers unclaimed addresses. Say so and wait for yes.
Hands on: the clone's mappings and its corrected record.
Check: GET /site/get-site-host-mappings/<clone> lists <clone>-<org>.site.appmint.app. GET /site/base-url?siteName=<clone> answers that host. Open it: the home page shows. Before the repair, a page can be previewed through any working host of the org at /__preview/page/<clone>/<page> (tested).
6. Sites & Dev Environments: delete a site
Stop: deleting is permanent for the site's addresses and sends the account holder a "site deleted" email. Name the site, its addresses and its page count, and wait for yes.
Studio: Sites & Dev Environments › the site's … › Delete Site ("This will permanently delete the site … and all its resources", modern-site-app.tsx 713–726, 1273–1300). It calls:
POST /site/remove
{ "siteName": "spring-shop", "deleteResources": true }call_appengine asks for the person's confirmation first (a remove path), then send it again with "confirmed": true.
What it does (site.service.ts 1805–1906, 435–465): finds the site by its exact name (404 Site <name> not found in organization <org> otherwise), deletes the route of every mapped name, deletes the mappings, with deleteResources deletes the deployment, service, config map, secret and volume named after the site in a namespace named after the org, deletes the site record (to the trash), and sends site-deleted.
What no delete removes, so do it after (tested: the pages of a deleted site were all still there):
- Its pages:
repo_findonpagewith{ "data.site": "<name>" }, thenrepo_deleteeach one. Include theold_pages a clone made. Pages left behind come back on any later site given the same name. - Its navigation records, by the ids in
mainNavigationandfooterNavigation. - Its files, if they were only for this site.
DELETE /site/delete-site/:siteOrgId/:siteId doesn't work: the route reads siteOrgId into both values (site.controller.ts 230). Tested: 400 invalid id phazzer. Studio doesn't use it.
Hands on: nothing.
Check: GET /site/org-sites/<org> no longer lists it, GET /site/get-site-host-mappings/<name> has no rows, and repo_find on page with its name finds nothing. If it was the default site, make another site the default (step 5).
When it's done
Tell the person: the new site's name, its platform address (and that a custom domain is a separate step), which pages it has and which is the home page, whether it's the default site, and what emails went out. For a clone, also say which of the source's settings it kept and which were fixed. For a delete, say what was removed and that the platform address no longer answers. Open Sites & Dev Environments and the new site.
What it can't do yet
- Studio's New Site makes a site with no pages and no
hostName. It calls/site/add, which seeds nothing and savessharedDomaininstead ofhostName. Atemplatesent to/site/addis saved and ignored. - No Clone button in Studio, and no apply-template screen.
- Clone copies 50 pages at most, fails with 409 when the source has navigation (after making the site record), copies the source's
hostName,domainandisDefault, and creates no mapping. It has no plan check. - Apply template doesn't copy
themeorsettings, copies 50 pages at most, stops at the first page name the site already has, and always reports success. - Site templates can't be registered: Studio calls a route that doesn't exist, and the engine route misreads its body. The page gallery reads a
pages[]list that registration no longer writes. - No delete removes pages, navigation or files.
/site/delete-site/…doesn't work at all. - The starter layout is
base-layout, but features fall back toaccount-layout. - The site name can't be changed.
Don'ts
- Don't call
/site/removeor/site/apply-templatebeforeGET /site/get-site/<name>has answered with that exact name. - Don't clone a source that is the default site, has navigation, or has more than 50 pages. Copy it by hand.
- Don't leave a clone with the source's
domainorhostName. - Don't use a clone or a bare record to get around a full plan.
- Don't pass
domainNamewhen creating a site; connect the domain afterwards. - Don't trust apply-template's answer. Count the pages.
- Don't delete a site without deleting its pages.