Project Structure & Theming

A directory tour, the request flow, and the CSS design-token theme system.

Directory tour

gravity-node/
├── src/
│   ├── content/
│   │   ├── content.ts             # Page/section content API — load/save/publish, block schemas
│   │   └── blocks/*.json          # Block-type schemas (hero, feature-grid, rich-text, cta, ...)
│   ├── docs-content/*.md          # This documentation site's markdown source (astro:content)
│   ├── components/blocks/         # Astro block components + the Sections list-renderer
│   ├── components/shared/         # EditFab.astro — the floating ✏️ button
│   ├── layouts/
│   │   ├── BaseLayout.astro       # Theme chrome: nav, footer, meta tags
│   │   └── DocsLayout.astro       # Sidebar + prose layout for /docs
│   ├── builder/                   # GrapesJS builder canvas client bundle
│   │   ├── main.ts                # GrapesJS init, load/save/publish wiring
│   │   ├── componentTypes.ts      # Schema-driven block/trait registration + JSON (de)serialization
│   │   ├── traits.ts              # Custom trait types: textarea, image upload, list repeater
│   │   └── render.ts              # Canvas-preview HTML, mirrors components/blocks/*.astro
│   ├── lib/
│   │   ├── theme.ts                # GRAVITY_TEMPLATE resolution
│   │   ├── auth.ts                 # Admin auth guard (dev bypass + Adhara token)
│   │   ├── portal.ts               # Customer-portal backend resolution
│   │   ├── adhara.ts               # Adhara HTTP API + the (optional, external) SDK client
│   │   ├── adharaCommerce.ts       # Commerce-specific SDK layer (shop)
│   │   ├── adharaMedia.ts          # Media-gallery-specific SDK layer (gallery)
│   │   ├── adharaForms.ts          # Form-provisioning SDK layer (contact)
│   │   ├── featureBackend.ts       # Shared local/adhara toggle resolver, reused by every feature
│   │   ├── sanitize.ts             # HTML allowlist sanitizer
│   │   └── editbar.ts              # Floating-button routing logic
│   ├── stores/                    # Pluggable ContentStore: local/GCS/S3/Azure/R2/Vercel Blob/Netlify Blobs
│   ├── services/
│   │   ├── blogStore.ts, eventStore.ts, linkStore.ts, shopStore.ts,
│   │   │   galleryStore.ts, scheduleStore.ts, podcastStore.ts,
│   │   │   contactFormStore.ts, contactStore.ts   # One store per feature — local ↔ Adhara
│   │   └── registrationStore.ts   # Free-event registration + attendee list, via Adhara
│   └── pages/                     # Astro routes — public pages, /docs, /admin/*, /api/*
│
├── public/
│   ├── themes/clarity/main.css    # The theme's entire CSS design system
│   ├── js/main.js                 # Nav scroll, mobile menu, scroll-reveal, tab switcher
│   ├── js/inline.js               # Inline editor client
│   └── js/shop-cart.js            # Client-side cart (localStorage)
│
├── data/content/                  # Local JSON content — pages, blog posts, events, ...
├── scripts/
│   ├── gravity-*.ts                # Seed/import CLIs (content, blog, events, courses, links, shop, contact)
│   ├── setup.sh / deploy_*.sh      # Deploy wizards (gcp/vercel/cloudflare/netlify)
│   ├── push_repo.sh                # Create + push to a GitHub/GitLab repo
│   ├── install_tools_*.sh/.ps1     # Idempotent per-OS dev-environment installer
│   └── lib/                        # Shared shell helpers + the storage-provisioning wizard
├── astro.config.mjs                # DEPLOY_TARGET-driven adapter selection
├── Dockerfile / docker-compose.yml / wrangler.jsonc / netlify.toml
└── .env.example

Request flow

  1. A request hits the Astro SSR server (Node, or the platform-native equivalent — a Vercel Function, a Cloudflare Worker, or a Netlify Function/edge function, depending on DEPLOY_TARGET).
  2. Public pages load content via src/content/content.ts (block-driven pages) or a feature-specific *Store.ts (blog/events/shop/etc.), which in turn resolves local vs. Adhara per the toggle rules and reads through getStore() for local data.
  3. BaseLayout.astro renders the shared nav/footer/theme chrome around whatever the page returns.
  4. Admin routes (/admin/*) check isAdmin() (src/lib/auth.ts) — either a real Adhara session or the GRAVITY_DEV_ADMIN=1 local bypass — before rendering.

Deploy targets

DEPLOY_TARGET picks the Astro adapter (and the edge-cache provider — see Deploying) at build time (astro.config.mjs):

Target Adapter Runtime
node (default) @astrojs/node Plain Node process — Docker, Google Cloud Run, self-host
vercel @astrojs/vercel A real Vercel Function
cloudflare @astrojs/cloudflare A real Cloudflare Worker — no Docker, no proxy layer
netlify @astrojs/netlify Netlify Functions + edge middleware

npm run build:<target> sets this explicitly, and an explicit value always wins. If it’s unset, astro.config.mjs auto-detects from the platform’s own build-environment signal (VERCEL, NETLIFY/NETLIFY_LOCAL, CF_PAGES — each platform’s own documented system env var) instead of just assuming node. This is what makes connecting this repo straight to Vercel’s or Netlify’s git integration (Import Project / “Deploy this repo,” no make deploy-* script involved, no env vars configured at all) build correctly out of the box — before this existed, that path silently built with the node adapter on whatever platform triggered it, which doesn’t produce output that platform can actually serve.

See Deploying for the full per-platform guide.

Theming

Design tokens are CSS custom properties — swap GRAVITY_TEMPLATE and every page’s fonts, colors, spacing, and components change without touching markup.

public/themes/<theme>/main.css   ← the whole design system for that theme
src/layouts/BaseLayout.astro     ← nav/footer chrome, links the active theme's CSS
src/components/blocks/*.astro    ← content-block components (hero, feature-grid, rich-text, cta)
src/lib/theme.ts                 ← GRAVITY_TEMPLATE → theme resolution, KNOWN_THEMES registry

Two themes ship. gravity — dark navy + gold, Playfair Display + Inter + JetBrains Mono — is the flagship theme and the default, matching the Python original. clarity — light, Notion-inspired, DM Serif Display + DM Sans + IBM Plex Mono — is the alternate. Both themes share the exact same class vocabulary (.hero, .card, .terminal, .feat-card, …), so every page and block component renders correctly under either without a fork — only the :root {} tokens and the Google Fonts link (themeFontsHref() in src/lib/theme.ts) differ per theme. Adding a third theme means:

  1. Copy public/themes/gravity/main.css to public/themes/<name>/main.css and edit its :root {} tokens (and its Google Fonts if the typefaces change).
  2. Add <name> to KNOWN_THEMES, and a matching entry in THEME_FONTS, in src/lib/theme.ts.
  3. Fork whichever layout/block components you want to look meaningfully different under the new theme — most themes won’t need this, since (1) already reskins everything that reads the shared tokens/classes.

There’s no per-theme template fallback mechanism — a theme is a CSS reskin of the same markup, not a different page structure. src/pages/index.astro’s dual-mode fallback content (the demo homepage shown before any block content is published) is theme-agnostic for the same reason: it only uses classes already in every theme’s main.css.

The CSS-dedup rule

Every reusable class lives in main.css, not copy-pasted across page-scoped <style> blocks — this is a hard rule, not a suggestion, for two concrete reasons: (1) copies drift (a hardcoded color in one duplicate silently overrode the theme-var version everywhere that didn’t have the same local copy); (2) a second theme’s main.css can only reskin what’s actually in main.css — a class baked into one page’s scoped <style> never sees a theme swap. See The Content Model for the full rule and how a page-scoped override on top of a shared base is still fine.