Minisites — Behavior
Request flow
- Visitor hits
https://<host>/. - Astro’s catch-all
apps/minisites/src/pages/[...slug].astroreads thex-forwarded-hostorhostheader. - It calls
GET ${INTERNAL_API_URL}/minisites/resolve?host=<host>on the FitKit API. - The API joins
minisite_contentbycustom_domainorsubdomainand returns the published payload plus aplatformDatablock (live programs, sessions, plans, courses, coaches) and the org metadata. - If
isPublishedis false orpublishedContentis null, the Astro page returns a 404. - Otherwise Astro renders the layout with the published sections + live data and serves the page with
Cache-Control: public, s-maxage=120, stale-while-revalidate=300.
Host resolution
The API attempts matches in this order:
- Exact
custom_domainmatch (case-insensitive). - Exact
subdomainmatch against<slug>.fitkit.fit.
If neither matches, the resolve endpoint returns 404. The minisite app surfaces a generic “Site not found” response.
Sections
The [...slug].astro page composes from a fixed component map:
| Section | Source data | Hidden when |
|---|---|---|
hero | Editor (title, subtitle, background image, CTA) | Never (always renders). |
about | Editor + org description + image gallery | All sources empty. |
classes | Live platformData.programs[] | Org has no active programs. |
schedule | Live platformData.upcomingSessions[] | Org has no upcoming sessions. |
pricing | Live platformData.plans[] | Org has no active plans. |
courses | Live platformData.courses[] | Org has no active courses. |
trainers | Live platformData.coaches[] | Org has no active coaches. |
contact | Editor (email, phone, whatsapp, address, hours) | Never. |
gallery | Editor (image list) | Never (renders empty grid). |
testimonials | Editor | Never. |
faq | Editor | Never. |
Sections additionally honor an isEnabled flag in the editor — owners can suppress a section entirely.
Theme
The theme block carried in minisite_content.theme (and overridable inside publishedContent.theme) is:
{
"primaryColor": "#000000",
"secondaryColor": "#f3f4f6",
"fontFamily": "inter",
"buttonStyle": "rounded"
}Tokens flow into the layout as CSS variables and font-family rules. The fontFamily enum is hardcoded in the layout — inter | playfair | montserrat | ....
Localization
resolveLocale() (apps/minisites/src/lib/i18n.ts) normalizes the published-content locale, then the org locale, then falls back to he. The layout renders dir="rtl" for he, ltr otherwise.
Draft vs publish
- The dashboard editor writes to
minisite_content.contenton every save (autosave). - “Publish” copies
content→published_contentand setsis_published = true. - The Astro page only reads
published_content; drafts are invisible to the public. - Republishing overwrites the snapshot atomically — no diff history kept.
Custom domains
- Owner sets
custom_domainvia the editor; the API persists it and instructs the owner to point a CNAME tocname.vercel-dns.com. - Vercel auto-issues a Let’s Encrypt cert once the CNAME resolves.
- Until then, requests to the custom domain return Vercel’s default 404; FitKit does not currently surface a setup-in-progress page.
Caching
- Astro emits
Cache-Control: public, s-maxage=120, stale-while-revalidate=300per response. - Vercel’s edge cache holds the response for 2 minutes; stale responses serve for an additional 5 minutes while a fresh fetch runs in the background.
- Editor publish does not invalidate the cache — owners see updates within the SWR window. Manual invalidation requires a redeploy or a CDN purge.
Outbound endpoints
| Endpoint | Purpose |
|---|---|
GET /minisites/resolve?host=<host> | Returns data: { isPublished, publishedContent, theme, locale, platformData, organization, ... }. Public. |
POST /minisites/leads (dashboard surface — not minisite app) | Contact form submissions land via the leads service. |
Performance posture
- Hero image preload (
<link rel="preload" as="image">) lands in the head so LCP fires <2.5s on a cold cache. - Section components are static Astro; no client-side JS framework.
- Only the floating WhatsApp button + scroll observers ship runtime JS — total payload <30KB gzipped.