Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This step does not include:

Editable is a SvelteKit application that lets site owners edit content directly in the browser. The editor (Svedit) works with a graph-based document model — a flat map of nodes with references between them. The backend stores these documents in SQLite and serves them to the frontend, stitching together shared content (nav, footer) with page-specific content into a single document that Svedit can edit locally.

The production architecture is database-backed and supports multiple pages, but the project must also continue to support static preview/local development mode (for example `VERCEL=1`) where the app falls back to the demo document. In that mode, only the `/` route needs to work, multi-page features are disabled, authentication is disabled, and code paths must avoid hard dependencies on server-only runtime features that would break static deployments.
The production architecture is database-backed and supports multiple pages, but the project must also continue to support static preview/local development mode (for example `VERCEL=1`) where the app falls back to the default site document. In that mode, only the `/` route needs to work, multi-page features are disabled, authentication is disabled, and code paths must avoid hard dependencies on server-only runtime features that would break static deployments.

The content model includes reusable `list` and `button_group` blocks for richer text flows. `prose.content`, `accordion_item.body`, and `feature.body` may contain `text`, `list`, `supporting_media`, and `button_group` nodes. Each `list` owns a `list_items` node array of `list_item` nodes, and each `list_item` owns a single-line annotated text `content` property. Each `button_group` owns a `buttons` node array of `button` nodes and is styled like the existing button rows. A `button` has `primary`, `secondary`, and `link` layouts; `link` keeps the same height as the other layouts but drops the fill, the border, and the horizontal padding in favour of an underline that spans exactly the label width. The paragraph and heading text scale includes `paragraph`, `paragraph_sm`, `paragraph_lg`, `paragraph_xl`, `heading_1_xl`, `heading_1`, `heading_2`, `heading_3`, and `heading_4` nodes with `regular` and `muted` layouts. The `list.layout` string id controls marker style so the existing layout-cycling flow can switch between unordered and ordered variants without changing node type.

Expand All @@ -71,7 +71,7 @@ Guidelines:

- **Non-strict for now.** `tsconfig.json` keeps `strict: false`; implicit `any` is allowed. The goal of the conversion is consistency and schema-derived autocomplete, not exhaustive type coverage. Strictness can be ratcheted up later.
- **Readable surface, ceremonial internals.** Customizable node components (e.g. `Paragraph.svelte`, `Figure.svelte`) must stay light: typed props plus a typed `node`, nothing more. Internal modules (server code, `api.remote.ts`, `PagesDrawer`, overlays, commands) may carry heavier type ceremony where it stabilizes things.
- **Schema-derived node types.** `src/lib/document_schema.ts` exports `type Nodes = NodeMap<typeof document_schema>` (from svedit). Node components access the session through the typed `get_svedit_context()` helper in `src/routes/svedit_context.ts` and annotate their node as `let node: Nodes['paragraph'] = $derived(svedit.session.get(path));`, which gives property autocomplete and catches misspelled properties in `pnpm check`.
- **Schema-derived node types.** `src/app/document_schema.ts` exports `type Nodes = NodeMap<typeof document_schema>` (from svedit). Node components access the session through the typed `get_svedit_context()` helper in `src/app/svedit_context.ts` and annotate their node as `let node: Nodes['paragraph'] = $derived(svedit.session.get(path));`, which gives property autocomplete and catches misspelled properties in `pnpm check`.
- **Import specifiers keep the `.js` extension** even when the target file is `.ts` (TypeScript's `bundler` resolution and Vite both resolve `./foo.js` → `./foo.ts`). This matches svedit's own convention.
- **`pnpm check` must stay at 0 errors** and is the required validation step after schema or component changes.

Expand Down Expand Up @@ -139,7 +139,7 @@ Editable must preserve a lightweight static-compatible mode for preview deployme
**Requirements:**

- Only the `/` route must support static/Vercel mode.
- In static/Vercel mode, `/` renders from the existing demo document instead of the database.
- In static/Vercel mode, `/` renders from the default site document instead of the database.
- multi-page features are disabled in this mode at the **UI / integration** level:
- no pages drawer
- no links or flows that send the user to `/new`
Expand All @@ -153,7 +153,7 @@ Editable must preserve a lightweight static-compatible mode for preview deployme
This means the app effectively has two operating modes:

1. **Full runtime mode** — database-backed, multi-page, shared nav/footer, admin-authenticated editing
2. **Static/Vercel mode** — single-page demo-doc fallback on `/`, while multi-page routes may still exist but are not surfaced or used
2. **Static/Vercel mode** — single-page default-site fallback on `/`, while multi-page routes may still exist but are not surfaced or used

The static/Vercel mode is a compatibility constraint on all future multi-page work.

Expand All @@ -179,7 +179,9 @@ data/
│ ├── w640.webp
│ ├── w1024.webp
│ └── w1536.webp
├── e7a3f1bc...abcd.mp4 # video passthrough
├── e7a3f1bc...abcd.mp4 # converted video
├── e7a3f1bc...abcd/
│ └── poster.webp # first decoded video frame
├── f9c2d4ae...cdef.gif # animated gif passthrough
└── ...
```
Expand Down Expand Up @@ -222,7 +224,7 @@ CREATE TABLE sessions (

**`documents`**

- `document_id` — a persistent identifier (nanoid with a custom alphabet — letters only, no numbers, no `_` or `-` — so ids are safe to use as HTML ids; see `src/routes/nanoid.ts`)
- `document_id` — a persistent identifier (nanoid with a custom alphabet — letters only, no numbers, no `_` or `-` — so ids are safe to use as HTML ids; see `src/app/nanoid.ts`)
- `type` — categorizes the document, e.g. `page`, `nav`, or `footer`
- `data` — the full Svedit document serialized as JSON (`{ document_id, nodes }`)

Expand Down Expand Up @@ -741,7 +743,7 @@ The `/new` route uses an **ephemeral client-created document**. When the user op

This id is stable from the beginning, even before the document is persisted. The page remains ephemeral only in the sense that it is not stored in the database until the first save.

The transient `/new` document must be composed from the **current shared nav and footer documents in the database**, not from the static demo document. This ensures that if shared nav or footer content has been edited elsewhere, the new page starts from that latest shared state.
The transient `/new` document must be composed from the **current shared nav and footer documents in the database**, not from the static default site document. This ensures that if shared nav or footer content has been edited elsewhere, the new page starts from that latest shared state.

On first save:

Expand Down Expand Up @@ -839,7 +841,7 @@ Both media types fill their container the same way visually. Images and videos u
- `object-fit`, `object-position`, and `transform: scale(...)` applied identically to how `Image.svelte` does it.
- `width` and `height` attributes set from node properties.
- `contenteditable="false"` to prevent the browser from trying to edit the video element.
- No `srcset` or variants — videos are served as-is.
- No `srcset` or video variants — videos are served as-is. Every saved video has one derived `poster.webp`, stored in the video's companion asset directory and assigned to the native `poster` attribute while the video loads. The poster is not stored in the document node.
- The `alt` property is rendered as `aria-label` on the `<video>` element.
- **Autoplay handling:** uses `$effect` with multiple retry strategies — checks `readyState >= 2`, listens for `canplay`/`loadeddata`, and falls back to `setTimeout`. This handles late hydration where readiness events may have already fired.
- **Click-to-fullscreen (published view only):** clicking the video enters native fullscreen with controls enabled and audio unmuted. On fullscreen exit (including iOS Safari's `webkitendfullscreen`), the video restores to muted inline autoplay. iOS sometimes re-pauses ~400ms after play succeeds, so a `setTimeout(500)` retry is needed. Inline-playing videos show `cursor: zoom-in`.
Expand Down Expand Up @@ -1133,7 +1135,7 @@ The page browser uses a bottom drawer. Its resize handle is rendered outside the

A page is **home-reachable** (and appears in `sitemap.xml`) if it can be reached by following links starting from the home page, nav, or footer. This is a transitive check. A page may still exist publicly by URL even if it is not home-reachable.

This reachability logic only applies in the full database-backed multi-page runtime. In static/Vercel compatibility mode there is no live multi-page graph, no sitemap drawer, and no home-reachable vs unlisted distinction — the app simply serves the demo document at `/`.
This reachability logic only applies in the full database-backed multi-page runtime. In static/Vercel compatibility mode there is no live multi-page graph, no sitemap drawer, and no home-reachable vs unlisted distinction — the app simply serves the default site document at `/`.

The traversal starts from three roots:

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ Content is defined through schemas that specify:

When adding new properties to a node type:

1. Add to schema in `src/lib/document_schema.ts` (`document_schema`)
2. Add to inserter in `src/routes/create_session.ts` (`inserters`)
1. Add to schema in `src/app/document_schema.ts` (`document_schema`)
2. Add to inserter in `src/app/session.ts` (`inserters`)

## Available MCP Tools

Expand Down
62 changes: 51 additions & 11 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Implementation plan

## Product page

- Keep the compact product and audience story as the repository-managed `PRODUCT.md` Markdown document, served read-only at `/product`.
- Lead with `Why Editable?`: a direct explanation that Editable is a Svelte website with on-page editing, without a separate CMS.
- Address each audience in a distinct section: For developers, For designers, For creators, For agencies, and For artists.
- End with one clear prompt to install Editable through the existing `/manual` quickstart.
- Keep this text-first page in code until the same content is represented in the page builder.

## Video posters

- Generate one `poster.webp` from the first decoded frame of every processed or explicitly pre-optimized video.
- Generate the poster from the final stored MP4 after conversion/remuxing; pre-optimized MP4 uploads generate it from their original bytes.
- Store each poster as `assets/<video-hash>/poster.webp`, derived from its content-addressed video asset id; do not add a poster property to video document nodes.
- Upload, serve, mirror, and delete the poster with its associated video asset.
- Render the derived URL through the native `<video poster>` attribute for saved videos. Do not create responsive poster variants because that attribute has no native `srcset` support.

## External link preview labels

- Remove the `https://` scheme from external link preview labels for readability while preserving the complete URL in the link target.
- Leave other URL schemes and link behavior unchanged.

## Android end-of-page hold gesture

- Schedule the end-of-page hold check immediately when a qualifying touch starts at the page end, rather than relying only on a scroll event.
- Recheck during touch movement as a fallback for browsers that do not emit the same overscroll scroll events as iOS.
- Preserve the existing one-second hold, eligibility checks, and iOS behavior.

## Save progress indicator

- Keep the save phase text stable while saving and encode the current media progress in a compact circular loading indicator.
- Label the stable media phases as processing and uploading so the user can distinguish them from document saving.
- Pass progress separately from the status message so percentage updates do not cause text layout shifts.
- Use a small circular progress indicator and preserve the existing text-only completion pill.

## Internal link preview loading

- Hide the complete internal link preview surface, including actions, while its page data is resolving.
- Render the existing preview only after resolution succeeds, or render the existing no-match/error state once resolution settles.
- Keep external link previews and the create-link state unchanged.

## SvelteKit 3 prerelease migration

- Upgrade to `@sveltejs/kit@3.0.0-next.12`, TypeScript 6, and SvelteKit-3-compatible prerelease versions of the Node and Vercel adapters while retaining compatible Svelte, Vite, and Vite plugin ranges.
Expand All @@ -16,7 +56,7 @@
Convert the whole codebase from JS+JSDoc to TypeScript per the "Language: TypeScript" section in [ARCHITECTURE.md](ARCHITECTURE.md). Non-strict; svelte-check must stay at 0 errors.

1. Replace `jsconfig.json` with `tsconfig.json` (same options, `allowJs`/`checkJs` kept during transition) and point the `check` script at it.
2. Convert `src/lib/document_schema.js` → `.ts`, export `type Nodes = NodeMap<typeof document_schema>`, and add a typed `get_svedit_context()` in `src/routes/svedit_context.ts` (mirrors the svedit demo app).
2. Convert the document schema to `src/app/document_schema.ts`, export `type Nodes = NodeMap<typeof document_schema>`, and add a typed `get_svedit_context()` in `src/app/svedit_context.ts` (mirrors the svedit demo app).
3. Convert `src/lib` modules (root, `client/`, `server/`, `server/markdown/`, `server/migrations/`), translating existing JSDoc annotations to TS syntax.
4. Convert `src/routes` modules (`hooks.server`, load functions, API endpoints, `app_utils`, `commands.svelte`, `create_session`, helpers).
5. Convert all Svelte components to `<script lang="ts">`. Node components use the typed-node pattern; internal components get explicit prop types.
Expand Down Expand Up @@ -77,7 +117,7 @@ Limit `CycleNodeTypeCommand` so it only offers destructive type switches while t
### Scope

- Implement this only in `editable-website`.
- Add `get_cycle_node_state(session)` in `src/routes/app_utils.js`, returning `{ node, node_array_path, node_index, available_types }` or `null`.
- Add `get_cycle_node_state(session)` in `src/app/app_utils.ts`, returning `{ node, node_array_path, node_index, available_types }` or `null`.
- Keep the existing closest-switchable-node search behavior, but compute `available_types` from the containing `node_array` schema.
- Treat a node subtree as empty only when every property is empty or equal to its schema/default value, including all child nodes reached through `node` and `node_array` properties, except `layout`, which is ignored for the emptiness check.
- For empty nodes, allow cycling to all other types in the containing `node_array`.
Expand Down Expand Up @@ -1028,12 +1068,12 @@ This step must preserve the current strengths of the app:
- existing save flow including asset processing/upload/replacement
- current document splitting and asset reference tracking
- editable-in-place page editing with the same session and toolbar behavior
- static/Vercel compatibility for the `/` route using the demo document fallback
- static/Vercel compatibility for the `/` route using the default site document fallback

In addition, the multi-page work must preserve the current static preview / local single-page mode:

- when running in static/Vercel-style mode (for example `VERCEL=1`), only `/` needs to work
- `/` should continue to render from the demo document in that mode
- `/` should continue to render from the default site document in that mode
- multi-page features are disabled in that mode from the `/` route's point of view:
- no pages drawer
- no linking into `/new`
Expand Down Expand Up @@ -1167,7 +1207,7 @@ This is a good use for Svelte async patterns and keeps the main editor lightweig

- `/` continues to resolve `home_page_id` from site settings in the full runtime
- it loads that page using the same dynamic page loader used by `/:page_id`
- however, `/` must also retain a static/Vercel fallback mode that renders the demo document without requiring the database or multi-page runtime
- however, `/` must also retain a static/Vercel fallback mode that renders the default site document without requiring the database or multi-page runtime

This avoids duplicating page rendering logic while preserving a clean homepage URL and keeping preview/static deployments viable.

Expand Down Expand Up @@ -1484,7 +1524,7 @@ Current `src/routes/+page.svelte` mixes:

Extract the reusable editor page shell into something like:

- `src/routes/components/PageEditor.svelte`
- `src/app/components/PageEditor.svelte`

Inputs:

Expand Down Expand Up @@ -1524,7 +1564,7 @@ Implemented:
Current behavior:

- in full runtime mode, `/` loads the configured home page and renders it through the shared editor shell
- in static/Vercel mode, `/` still falls back to `demo_doc`
- in static/Vercel mode, `/` still falls back to `default_site_document`

### 2.4 Add `/new`

Expand Down Expand Up @@ -1708,15 +1748,15 @@ These constraints must be respected during implementation:
- the drawer/page-browser UI should not appear in static/Vercel mode
- authentication should remain effectively off in static/Vercel mode
- route/component structure should avoid forcing server-only imports for `/`
- if needed, keep the current pattern where `/` conditionally loads the demo document in static mode and only uses the runtime database path in full mode
- if needed, keep the current pattern where `/` conditionally loads the default site document in static mode and only uses the runtime database path in full mode

## Suggested file changes summary

### New or extracted files

- `src/routes/[page_id]/+page.svelte`
- `src/routes/new/+page.svelte`
- `src/routes/components/PageEditor.svelte`
- `src/app/components/PageEditor.svelte`
- maybe `src/lib/new_page.js`
- maybe `src/lib/server/page_browser.js`
- maybe `src/lib/server/page_summary.js`
Expand All @@ -1725,8 +1765,8 @@ These constraints must be respected during implementation:

- `src/routes/+page.svelte`
- `src/lib/api.remote.js`
- `src/routes/components/PagesDrawer.svelte`
- `src/routes/components/Overlays.svelte`
- `src/app/components/PagesDrawer.svelte`
- `src/app/components/Overlays.svelte`
- possibly `src/lib/server/migrations.js` if additional seed/settings support is needed

## Recommended implementation order
Expand Down
Loading