# Gravity Node — full documentation Generated from https://www.edmecosystem.com/docs. Each section below corresponds to one page there. --- Gravity Node is a boilerplate for going from **zero to a deployed, editable website in minutes**. Clone it, run one command, and you get a themed, server-rendered site (Astro, SSR) with a working blog, events, forms, a customer portal, an e-commerce storefront, a media gallery, an admin dashboard, and **two** visual editors for page content — all wired up already. It's the Node/Astro sibling of [Gravity](https://thegravityframework.com), the original Python/Flask framework — same architecture, same content model, same "local JSON ↔ Adhara" story, ported to a stack most platforms support natively. ## The core idea Everything in this project follows one rule: **content is stored as local JSON files by default, and switches to a real headless CMS ([Adhara](https://adharaweb.com)) when you connect one — feature by feature, not all-or-nothing.** - **Out of the box**: blog posts, events, page content, links, shop items — all plain JSON under `data/content/`. No database, no account, no API key. Clone it and it works. - **Connect Adhara** by setting a couple of env vars, and individual features switch over independently. Run events on Adhara while the portal stays local. See [Adhara Integration](/docs/overview) for exactly how this works per feature. - **Deploy anywhere** — Vercel, Cloudflare, Google Cloud Run, or Netlify, with one command each. See [Deploying](/docs/deploying). ## Who this is for - **A site owner** who wants a real website with a blog, events, a shop, and a client portal, without committing to a specific CMS on day one. - **A developer** extending the template — every architectural rule that keeps the two visual editors working is documented in [Architecture](/docs/content-model), not just implied by the code. - **An AI coding agent** building on top of this template. This documentation is written to be exactly as useful read raw as it is rendered — see the machine-readable index at [`/llms.txt`](/llms.txt) and the full corpus at [`/llms-full.txt`](/llms-full.txt). ## Where to start | I want to... | Go to | |---|---| | Get a copy running locally | [Quick Start](/docs/quick-start) | | Understand how content editing works | [The Content Model](/docs/content-model) | | Add a new block type, or a new page section | [The Content Model](/docs/content-model) | | Understand the pluggable storage backends | [Storage](/docs/storage) | | Connect a real CMS backend | [Adhara Integration](/docs/overview) | | See what's built already | [Features](/docs/features) | | Ship it | [Deploying](/docs/deploying) | | Look up an env var or CLI command | [Environment Variables](/docs/environment-variables), [CLI Reference](/docs/cli-reference) | ## How this differs from the Python original This is a **core-first v1**, not a 1:1 port of every feature in the Python repo — see the comparison table in the [README](https://gitlab.com/eim_opensource/gravity-node#readme) for the full breakdown. The short version: everything content-related (blog, events, portal, scheduling, links, forms) is ported; a handful of things new to this port didn't exist in Python at all (a real e-commerce storefront via the Adhara Commerce SDK, a podcast page, a media gallery, Netlify as a fifth deploy target, an idempotent "install your tools" script); and a couple of things (paid event/membership checkout, real Adhara-backed admin login) are deliberately deferred — see [FAQ & Roadmap](/docs/faq). --- ## Prerequisites Node.js **18.20+** and npm. That's it — unlike the Python original, there's no separate Python/pip toolchain to install. Starting from a completely bare machine? [Install Your Tools](/install-your-tools) covers Git, the GitHub CLI, Node.js, a text editor, and an AI coding agent with one idempotent command per OS. ## Clone, install, configure, run ```bash # 1. Clone and install git clone https://gitlab.com/eim_opensource/gravity-node.git cd gravity-node npm install # 2. Configure cp .env.example .env # generate a SECRET_KEY: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # paste the result into SECRET_KEY= in .env # 3. Run it npm run dev ``` Open `http://localhost:3001`. That's a fully working site — home, about, features, blog, events, forms, links, podcast page (unconfigured), shop, gallery, and a customer portal — all reading from `data/content/`, no external services required. ## Prefer an AI coding agent to do this for you? Open [`/get-started`](/get-started) in the running app (or read it as plain source at `src/pages/get-started.astro`) — it's a copy-pasteable prompt for Claude Code, Claude Desktop, or Codex that clones the repo, installs it, runs it locally, and — once you confirm it looks right — deploys it to Vercel and hands you back a live URL. ## Unlock the admin dashboard and editors Local admin auth normally requires a real Adhara-backed session (not yet implemented — see [FAQ & Roadmap](/docs/faq)). For local development, set the dev bypass: ```bash # in .env GRAVITY_DEV_ADMIN=1 ``` Restart the dev server, then: - Visit `/admin` — the dashboard, with a live "Feature backends" panel showing which features are local vs. Adhara right now. - Look for the **✏️ floating button** in the bottom-right corner of the home, about, or features pages — that's the inline click-to-edit editor. - Visit `/admin/editor/build/home` for the full drag-and-drop **builder canvas** (built on GrapesJS). **Never set `GRAVITY_DEV_ADMIN=1` in production** — it bypasses login entirely. ## Seed sample content ```bash npm run content -- import docs/gravity/examples/content.sample.json npm run blog -- import docs/gravity/examples/blog.sample.json npm run events -- import docs/gravity/examples/events.sample.json npm run links -- import docs/gravity/examples/links.sample.json npm run shop -- import docs/gravity/examples/shop.sample.json npm run courses -- import docs/gravity/examples/portal/courses/building-with-ai.json ``` Or, if you're running the Docker path: `make editor` builds the container, seeds all of the above, and starts with the admin/portal dev bypass on — see the printed URLs when it's done. ## Your first edit 1. Open `/admin` (with the dev bypass on). 2. Click **Inline edit** on the Home page, or **Open in Builder**. 3. Change some text, save/publish. 4. Refresh the live page — the change is there. It's now sitting in `data/content/pages/home.json` (or wherever `GRAVITY_STORE_BACKEND` points), not in a database. That's the whole content loop: every editor — inline, builder canvas, or the `npm run content` CLI — reads and writes the exact same `Section[]` data through `src/content/content.ts`. See [The Content Model](/docs/content-model) for how that works under the hood, and why it matters if you're extending the template rather than just using it. ## What's next - [The Content Model](/docs/content-model) — how the block/schema system works, and the one rule that keeps both visual editors working. - [Adhara Integration](/docs/overview) — connect a real CMS backend, feature by feature. - [Deploying](/docs/deploying) — ship it to Vercel, Cloudflare, Google Cloud Run, or Netlify. --- ## The rule that matters most **Anything a site owner should be able to edit after launch — without a developer and without touching code — must be a content block, not hand-written HTML on a page.** This site has three interchangeable editing surfaces that all operate on the exact same data: 1. A **floating inline click-to-edit button** on the live page. 2. A **full drag-and-drop builder canvas** (`/admin/editor/build/`), built on [GrapesJS](https://grapesjs.com). 3. A **JSON/CLI path** (`npm run content`). All three work by reading and writing `Section[]` — `{ id, type, fields }` objects — through one module, `src/content/content.ts`. A block type only exists to those editors if it has a schema under `src/content/blocks/*.json`. If you write a new marketing section directly into `index.astro` as literal markup instead of as a block, it renders correctly on the live site and is then **completely invisible** to both editors — not broken-looking, just silently unreachable. That's the failure mode this page exists to prevent. **Heuristic:** if a human product owner would plausibly want to change this text, image, or list themselves later without asking a developer, it belongs in a block's `fields`. If it's structural (page layout, nav, footer, deploy config, an entire new route) or truly one-off, it's just code — edit it directly, no block needed. See [hand-written pages](#hand-written-pages-not-part-of-the-block-system) below for the list of pages that are deliberately exempt. ## Adding a new block type This is a fixed, mechanical checklist — the builder canvas is schema-driven, so you do **not** need to write any GrapesJS-specific registration code: 1. **`src/content/blocks/.json`** — the schema: `type`, `label`, `description`, and a `fields` array, each `{ key, type, label, default }`. A `list` field also needs an `item` array of the same shape, describing one row. 2. **`src/components/blocks/.astro`** — the Astro render component. Read every field via the `blockField(section, key)` helper from `src/content/content.ts` (it falls back to the schema default automatically). Reuse existing theme classes — every theme's `main.css` (`public/themes/gravity/main.css`, `public/themes/clarity/main.css`) shares the same class vocabulary — see [Hard rules](#hard-rules) below. 3. **Register it** in `src/components/blocks/Sections.astro`'s `BLOCK_COMPONENTS` map (one line). 4. **`src/builder/render.ts`** — add a `render(fields)` function that mirrors step 2's markup as a plain HTML string. This is the builder canvas's live-preview renderer — a deliberate small duplication of the Astro component, not a shared abstraction, because GrapesJS renders HTML strings, not Astro components. Add the block's outer `
` tag/class to the `BLOCK_WRAPPER` map in the same file. That's the whole checklist. `src/builder/componentTypes.ts` fetches every schema from `/api/admin/editor/block-schemas` at runtime and generates the GrapesJS block, component type, and trait panel from it automatically — don't hand-write a new `editor.Components.addType(...)` call for a new block type; if you find yourself doing that, the schema-driven factory is missing something, and *that's* the bug to fix. ## Modifying an existing block type A block type's field list exists in three places that must all agree, or the builder canvas's preview will silently diverge from the real site (and saved data may not round-trip): - `src/content/blocks/.json` — the schema, source of truth for field keys/types/defaults. - `src/components/blocks/.astro` — the live-site renderer. - `src/builder/render.ts`'s matching `render()` — the canvas-preview renderer. Add, rename, or remove a field in the schema and update the other two in the same change. Trait UI (the settings panel) needs no separate update — it's generated from the schema. ## Field types Only six exist, and each maps to exactly one editor UI (see `src/builder/traits.ts`): | Type | Editor UI | Notes | |---|---|---| | `text` | Plain input | Single-line string | | `textarea` | Multi-line input | Plain string, no formatting | | `richtext` | RTE (inline editor) / textarea (builder) | Stored as raw HTML — no markdown | | `link` | URL input | A URL string | | `image` | Upload button | Wired to `/api/admin/editor/upload`, goes through the pluggable `ContentStore` — see [Storage](/docs/storage) | | `list` | Repeater | Needs an `item` sub-schema describing one row | If you think you need a seventh type, you also need to: add a case to `traitTypeFor()` in `src/builder/componentTypes.ts`, and a matching custom trait type in `src/builder/traits.ts` (follow the pattern of `gv-textarea`/`gv-image`/`gv-list` — read that file's own docstring first, it explains why every custom trait manages its own DOM instead of GrapesJS's default input-value wiring). Don't invent a new field `type` string without doing both, or it'll silently fall through to a plain text input. ## Hand-written pages, not part of the block system These pages are ordinary code, not editor-managed content — neither visual editor touches them: theme CSS, site chrome (nav/footer in `BaseLayout.astro`), and standalone pages — `/get-started`, `/install-your-tools`, `/forms`, `/blog`, `/events`, `/scheduling`, `/schedule/*`, `/links`, `/podcast`, `/shop/*`, `/gallery`, `/contact`, `/admin/*`, `/portal/*`, and this documentation site itself. When in doubt, ask: "does this text/image currently come from a `Section`'s `fields`?" If not, it's just code — edit it directly. Three pages are **dual-mode**: `index.astro`, `about.astro`, and `features.astro` render `` when block content has been published for that page, and fall back to a hardcoded default layout only when it hasn't. Once a page has published blocks, add a new block/section instead of editing the fallback markup — the fallback branch is dead once real content exists. ## Hard rules - **Don't hand-write new sections into `index.astro`/`about.astro`/`features.astro`'s markup** once real content exists there — see dual-mode pages above. - **Don't add inline `style=` attributes or one-off CSS classes inside block markup.** Use only the existing utility classes and CSS custom properties already defined in every theme's `main.css` (`.card`, `.grid-3`, `var(--space-*)`, etc. — see `public/themes/gravity/main.css`). The builder canvas deliberately has no style manager — block markup is the *only* thing that determines how a block looks, precisely so a site owner can never drag it off-theme. - **A new shared class goes into every theme's `main.css`, not just one.** Each theme is a self-contained design system with the full class vocabulary, not a shared base plus per-theme diffs — a class added to only one theme breaks under the other. - **Before adding a class to a hand-written page's scoped `