Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesMinisitesMinisites — Behavior

Minisites — Behavior

Request flow

  1. Visitor hits https://<host>/.
  2. Astro’s catch-all apps/minisites/src/pages/[...slug].astro reads the x-forwarded-host or host header.
  3. It calls GET ${INTERNAL_API_URL}/minisites/resolve?host=<host> on the FitKit API.
  4. The API joins minisite_content by custom_domain or subdomain and returns the published payload plus a platformData block (live programs, sessions, plans, courses, coaches) and the org metadata.
  5. If isPublished is false or publishedContent is null, the Astro page returns a 404.
  6. 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:

  1. Exact custom_domain match (case-insensitive).
  2. Exact subdomain match 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:

SectionSource dataHidden when
heroEditor (title, subtitle, background image, CTA)Never (always renders).
aboutEditor + org description + image galleryAll sources empty.
classesLive platformData.programs[]Org has no active programs.
scheduleLive platformData.upcomingSessions[]Org has no upcoming sessions.
pricingLive platformData.plans[]Org has no active plans.
coursesLive platformData.courses[]Org has no active courses.
trainersLive platformData.coaches[]Org has no active coaches.
contactEditor (email, phone, whatsapp, address, hours)Never.
galleryEditor (image list)Never (renders empty grid).
testimonialsEditorNever.
faqEditorNever.

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.content on every save (autosave).
  • “Publish” copies contentpublished_content and sets is_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_domain via the editor; the API persists it and instructs the owner to point a CNAME to cname.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=300 per 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

EndpointPurpose
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.