Editable Framework

repository·main·Indexed 23 days ago

https://github.com/michael/editable

A Svelte-based framework for building websites with integrated live editing, eliminating the need for a separate CMS. It features a typed content model using nodes, properties, and a specialized set of primitives like TextProperty, MediaProperty, and NodeArrayProperty to define editing boundaries and content structures directly within the codebase.

Tokens
25.4K
Snippets
39
Records
123
Agent score
83%

What's inside Editable

  1. Understand the Editable Content Model

    main

    Editable uses a typed vocabulary to define pages and shared site content. Documents are structured as graphs of nodes identified by id. Each node has a type and type-specific properties.

    Key Naming Conventions

    • content: The string payload of text properties.
    • body: Holds authored nested content.
    • items: Holds repeated structured children.
    • label, title, description, meta: Text properties with semantic meaning.

    Data Structures

    • Text Property: Contains a content string, plus marks and annotations arrays that reference separate nodes by ID.
    • Node Array: Contains an array of nodes (ordered child node IDs), plus marks and annotations arrays.
  2. Automatic internal link rewriting

    main

    When a page's active slug is changed, the system automatically rewrites all internal href links that reference that page to prevent broken links.

    Scope of Rewriting:

    • The system inspects all persisted documents (e.g., page documents, nav documents, footer documents) for any property named href.
    • It targets patterns like /${old_slug} and /${old_slug}#fragment.
    • It preserves URL fragments (e.g., #section).
    • It does not rewrite external URLs, same-page anchors (like #section), or the root / path.

    Note on Identity: The system resolves href values to document_id before deciding whether to rewrite them, ensuring that even if multiple aliases exist, the correct underlying document is identified.

  3. Understand page visibility and sitemap inclusion

    main

    In this system, all pages are public by default and discoverable via their unique URL slug. There is no distinction between 'private drafts' and 'public pages'. Instead, visibility is managed through reachability:

    • Public/Unlisted: Every page has a slug and is accessible via direct URL. If a page is not linked from the home page, it is considered unlisted.
    • Sitemap Inclusion: Only pages that are reachable from the home page via the site's link graph are included in sitemap.xml.

    This allows you to host parallel page hierarchies that are routable but not advertised to search engines.

  4. Rules for internal page references and document_refs

    main

    Internal page references are route-based and deterministic. To maintain a clean relationship graph, follow these rules for document_refs:

    • Slug-based routing: Use /${slug} for internal links.
    • Ignore Fragments: Links containing fragments (e.g., /${slug}#section) are treated as links to the base page. The #section part is ignored for reachability and sitemap purposes.
    • Ignore Same-Page Anchors: Anchor links that point to the current page (e.g., /#section on the home page) are not considered page references and must not create a document_refs edge.
    • Ignore External URLs: Links to external domains are ignored.
    • Normalization: document_refs should track relationships using the normalized target page ID rather than the full href string.
  5. How the page browser forest projection works

    main

    The page browser renders a forest projection of all pages rather than a full graph or a simple list. This ensures a deterministic, editor-friendly view. The forest is built using these rules:

    1. No Duplicates: The "first occurrence wins" rule is applied to prevent the same page from appearing multiple times.
    2. Home Subtree Priority: The canonical home subtree is built first and placed last in the forest.
    3. Home Subtree Ordering: The home subtree follows a specific hierarchy:
      • Shared navigation links
      • Home page body links
      • Shared footer links
    4. Recursive Ordering: For child pages within a subtree, only links found in the page body are used to find children.
    5. Non-home Roots: After the home subtree is placed, any remaining unassigned pages are grouped into additional subtrees. These non-home roots are appended to the forest before the home root.
  6. How slug-based routing and URLs work

    main

    The system uses human-readable slugs for public routing instead of raw IDs.

    Routing Rules

    • Home Page: The canonical route is always /. It does not use a slug.
    • Non-Home Pages: Use /:slug for public routes.
    • Historical Aliases: If a page's slug is changed, the old slug remains as a historical alias and issues a 301 redirect to the new active slug.

    Slug Lifecycle

    1. Generation: On the first save of a new page, a slug is automatically generated from the page title using slugify(title, { lower: true, strict: true, trim: true }). If the title is empty, it uses the document_id. If the slug is taken, a suffix (e.g., -2, -3) is added.
    2. Stability: Once generated, the slug remains stable. Changing the page title does not automatically update the slug.
    3. Manual Changes: Users can manually edit the URL in the Page Browser.

    Slug Validation & Conflicts

    • Unused Slugs: Can be claimed freely.
    • Historical Aliases: If you claim a slug that is currently a historical alias of another page, the system automatically reclaims it for you without a confirmation step.
    • Active Slugs: You cannot claim a slug that is currently the active slug of another page.
  7. Understanding the Editable mental model

    main

    Editable is a Svelte-based project where the website and the editor are the same thing. Unlike traditional CMS setups that separate content modeling from the frontend, Editable uses Svelte components as both the public website and the editing surface.

    Key characteristics:

    • Unified Surface: Editing happens directly on the actual page within the layout you built, rather than in a separate form-based CMS.
    • Schema-Driven: The content schema defines what data is allowed, while Svelte components define how that data is rendered.
    • Ownership: The site is a standard Svelte project. Content is stored locally (using SQLite), making the entire system versionable, deployable, and independent of proprietary platforms.
    • Extensibility: You can start with existing content types and layouts, or extend the project by writing custom Svelte components and CSS.
  8. Implement safer node type cycling

    main

    The CycleNodeTypeCommand is designed to prevent accidental data loss during node type switches. The behavior depends on whether the selected node subtree is considered 'empty'.

    Emptiness Check

    A node subtree is considered empty only when every property is empty or equal to its schema/default value (including all child nodes via node and node_array properties). The layout property is ignored during this check.

    Cycling Rules

    • For Empty Nodes: Users can cycle to any other type allowed by the containing node_array. This uses the existing inserter-based replacement behavior.
    • For Non-Empty Nodes: Users can only cycle to types whose property schema is exactly equivalent to the current node type's property schema. This performs a non-destructive replacement: the node root is replaced with a new ID, property values are carried over, and referenced child nodes are reused.

    Implementation Details

    • get_cycle_node_state(session) in src/routes/app_utils.js returns { node, node_array_path, node_index, available_types } or null.
    • CycleNodeTypeCommand.is_enabled() requires a cycle node state with at least one available type.
    • available_types are ordered relative to the current type so that next uses the first available type and previous uses the last.
  9. Generate a Table of Contents from Markdown

    main

    By setting toc: true in your MARKDOWN_SOURCES configuration, a table of contents is automatically generated.

    Behavior:

    • It targets headings exactly one level below the file's first heading (e.g., if the file starts with #, it uses ## headings).
    • It is inserted as a linked two-column listing before the first chapter.
    • Each row contains a link to the chapter and a description (the first sentence of the chapter's first paragraph).
    • Files with fewer than two chapter headings will not display a table of contents.
    • Headings use GitHub's anchor algorithm for stable IDs (e.g., ## Getting started becomes #getting-started). Note that on the website, anchors starting with a digit are prefixed with h- (e.g., #1-intro becomes #h-1-intro).
  10. How data safety and rollbacks work

    main

    The pnpm data:push command is designed to prevent production corruption through several safeguards:

    1. Pre-flight Validation: The local database is checked for integrity and all referenced assets are verified to exist before transmission.
    2. Automatic Backup: Before applying new data, the current remote database is snapshotted on the server and mirrored to your local data-backups/ folder.
    3. Atomic Swap: The new database is swapped in during a period with no active connections to prevent mid-write corruption.
    4. Post-flight Validation: The new remote database is re-validated. If it fails, the exact pnpm data:restore command needed to revert is printed.

    Rolling Back:

    If a push fails or a content error is discovered, use the snapshots created during the push:

    1. pnpm data:backups to find the correct snapshot name.
    2. pnpm data:restore <name> to revert the database.

    Note: A rollback restores the database but re-points to the existing immutable asset pool. To safely roll back assets, ensure your ASSET_GRACE_PERIOD_DAYS covers the necessary window.

  11. Server file layout and persistence

    main

    The deployment script uses a specific layout on the VPS to separate persistent data from disposable application code.

    Persistent Data:

    • /data: The site's persistent data. This is the only directory that must be backed up. It is bind-mounted into the container at /data.

    Disposable Application Files (under /srv/editable/):

    • docker-compose.yml: Uploaded on every deploy.
    • .env: Created on first run; contains ADMIN_PASSWORD, ORIGIN, and backup credentials. It is never overwritten by subsequent deploys.
    • .deploy_env: A marker file containing IMAGE_TAG, CONTAINER_NAME, and HOST_DATA_DIR used to coordinate deployments and the data toolbox.

    Caddy Configuration:

    • /etc/caddy/sites/editable.caddy: The vhost configuration for the site, imported into the main Caddyfile.
  12. Understand Admin Session and Cookie Behavior

    main

    Admin sessions are managed via a sliding window mechanism using an opaque session ID stored in a cookie.

    Session Lifecycle:

    • Lifetime: Sessions last for 2 weeks.
    • Sliding Window: Every meaningful authenticated request (including get_auth_status() or protected mutations) extends the expires timestamp to now + 2 weeks.
    • Cleanup: Expired sessions are deleted during session lookup in src/hooks.server.js.

    Cookie Security Settings:

    • httpOnly: True
    • sameSite: 'lax'
    • secure: True (in production)
    • path: /