Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3bc8cea
feat(seo): core SEO model, resolver, JSON-LD, robots generation
DavidBabinec Jun 12, 2026
9ea3cd6
feat(seo)!: replace flat seoTitle/seoDescription with structured seo …
DavidBabinec Jun 12, 2026
8b24cf5
feat(seo): publisher metadata, JSON-LD, robots.txt and sitemap.xml
DavidBabinec Jun 12, 2026
7eb1590
feat(seo): admin SEO workspace, Tools navigation, seo API and capabil…
DavidBabinec Jun 12, 2026
b47c8fc
feat(seo): AI metadata suggestions endpoint and sparkle bubbles
DavidBabinec Jun 12, 2026
464be3c
docs(seo): feature documentation and lint cleanup
DavidBabinec Jun 12, 2026
7fbbdee
feat(seo): redesign Meta workbench — sticky preview rail, grouped index
DavidBabinec Jun 12, 2026
8e491b2
feat(seo): index v3, gradient meters, SERP favicon, unified preview r…
DavidBabinec Jun 12, 2026
70c61e4
fix(seo): say 'goes live on publish' in the Meta tab save status
DavidBabinec Jun 12, 2026
48a332e
feat(seo): toolbar publish and scored control-center Meta tab
DavidBabinec Jun 12, 2026
ad13023
refactor(seo): rebuild SeoImageField on the shared media-picker pattern
DavidBabinec Jun 12, 2026
4840110
fix(seo): render AI sparkle errors and bubbles under the input, not i…
DavidBabinec Jun 12, 2026
f2f2b62
style(seo): inline index rows — title + route on one baseline, no acc…
DavidBabinec Jun 12, 2026
2438e26
refactor(seo): shared SeoFormRow + two-column Robots/Sitemap workbench
DavidBabinec Jun 12, 2026
92e990d
style(seo,ui): high-contrast switches, bigger row text, roomier cards
DavidBabinec Jun 12, 2026
34e3be7
refactor(seo): move the site score into the index sidebar, drop the t…
DavidBabinec Jun 12, 2026
4713f74
refactor(seo): flip Meta tab into one flat three-column grid
DavidBabinec Jun 12, 2026
c7687ea
style(seo): lift subtle text to muted across the workspace
DavidBabinec Jun 12, 2026
51dbd26
fix(seo): exclude layout templates from targets, name entry-template …
DavidBabinec Jun 12, 2026
60ea8ba
feat(nav): Tools dropdown — chevron, hover-open, current-section style
DavidBabinec Jun 12, 2026
d28bdac
chore(seo): de-export internal SITE_DEFAULTS_ID, drop stale spec qual…
DavidBabinec Jun 13, 2026
d2143f3
feat(seo): robots.txt — custom rules, escape hatch + URL tester, env …
DavidBabinec Jun 13, 2026
2875535
refactor(seo): robots.txt as a directly-edited document with an assis…
DavidBabinec Jun 13, 2026
d19931b
style(seo): deslop the Robots tab — quiet rows, color only as state
DavidBabinec Jun 14, 2026
e27378c
style(seo): redesign the Sitemap tab to match the Robots tab
DavidBabinec Jun 14, 2026
dc1194a
docs(seo): drop stale 'two toggles' reference in aiCrawlers row
DavidBabinec Jun 14, 2026
9249121
Merge remote-tracking branch 'origin/main' into feat/seo-aeo-workspace
DavidBabinec Jun 15, 2026
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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Three categories, three voices:
|------------------------------------------------------------------|----------------------------------------------------------------------|
| [features/plugin-system.md](features/plugin-system.md) | The plugin system end-to-end: package shape, lifecycle, sandbox, SDK, permissions, CLI |
| [features/publisher.md](features/publisher.md) | The page-tree-to-HTML/CSS renderer + server-side publishing wrappers |
| [features/seo.md](features/seo.md) | SEO & AEO: metadata model, JSON-LD, robots/sitemap, the SEO workspace |
| [features/visual-components.md](features/visual-components.md) | VCs, slots, params, instantiation, recursion guard |
| [features/content-storage.md](features/content-storage.md) | `data_tables` + `data_rows` — the universal content store |
| [features/content-workspace.md](features/content-workspace.md) | Content workspace UI: collections, entries, body editor, settings panel |
Expand Down
4 changes: 2 additions & 2 deletions docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ src/admin/
### Cross-page primitives

- **`SpotlightRoot`** — Cmd+K command palette. Owns its own command registry (`spotlight/commands/`), provider runner (`providers/`), scopes, keybindings, recents, telemetry. Available from every workspace.
- **`AdminSectionNavigation`** — top-of-screen workspace switcher.
- **`AdminSectionNavigation`** — top-of-screen workspace switcher. Ends with the **Tools** dropdown: first-party utility screens (SEO at `/admin/tools/seo`) plus plugin admin pages (their `/admin/plugins/:pluginId/:pageId` routes are unchanged — only the nav grouping moved).
- **`AccountMenuButton`** — top-right avatar / account menu.
- **`Panel`, `PanelHeader`, `SidebarResizeHandle`** — generic floating-panel chrome reused across the editor, content, and data workspaces.
- **`StepUp`** — re-auth dialog gating sensitive actions.
Expand Down Expand Up @@ -582,7 +582,7 @@ The sidebar shell expands/collapses by animating `--*-panel-width`. The panel sl

| Section | What it contains |
|---------------|------------------------------------------------------------------------------|
| General | Site name, meta title, meta description, language, favicon |
| General | Site name, language, favicon (site-wide SEO copy moved to `/admin/tools/seo`) |
| Shortcuts | Auto-rendered keyboard shortcut reference from the keybindings registry |
| Publishing | Self-hosted runtime info + framework CSS tree-shaking toggle |
| Preferences | Catalog-driven editor preferences (auto-rendered from `PREFERENCE_CATALOG`) |
Expand Down
9 changes: 5 additions & 4 deletions docs/features/content-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Used to render the **currently-published** page (vs. the in-progress draft on th

| `kind` | Authored in | Built-in fields | Workflow | Notes |
|--------------|-----------------------------------|-----------------|----------|------------------------------------------------|
| `postType` | Content workspace (`/admin/content`) | `title`, `slug`, `body` (text), `featuredMedia`, `seoTitle`, `seoDescription` | `draft / published / unpublished / scheduled` + versions | Built-in fields cannot be renamed or deleted, only enabled / disabled. |
| `postType` | Content workspace (`/admin/content`) | `title`, `slug`, `body` (text), `featuredMedia`, `seo` (structured `SeoMetadata`) | `draft / published / unpublished / scheduled` + versions | Built-in fields cannot be renamed or deleted, only enabled / disabled. |
| `data` | Data workspace grid (`/admin/data`) | none | none | Pure user-defined fields. Like a database table.|
| `page` | Site workspace (`/admin/site`) | `title`, `slug`, `body` (pageTree) | same as `postType` | Each row is a CMS page. `body` cell holds the `NodeTree<PageNode>`. |
| `component` | Site workspace, VC mode | `name`, `tree` (pageTree), `params` (fieldSchema), `description` | none | Each row is a Visual Component. See [docs/features/visual-components.md](visual-components.md). |
Expand Down Expand Up @@ -252,7 +252,7 @@ The field appears in the postType's edit form and is queryable from loops.

1. Open the Data workspace.
2. Create a new `data_table` with `kind: 'postType'`.
3. The system seeds the built-in fields (`title`, `slug`, `body`, `featuredMedia`, `seoTitle`, `seoDescription`) and a default entry template.
3. The system seeds the built-in fields (`title`, `slug`, `body`, `featuredMedia`, `seo`) and a default entry template.
4. Add custom fields as needed.
5. Add posts via the Content workspace.

Expand Down Expand Up @@ -322,8 +322,9 @@ There's also one filter — `content.entry.cells` — that runs over the cell ba
api.cms.hooks.filter('content.entry.cells', (cells, { tableSlug, entryId, actor }) => {
if (tableSlug !== 'pages') return cells
if (actor.kind === 'plugin' && actor.pluginId === api.plugin.id) return cells
if (!cells.metaDescription && typeof cells.body === 'string') {
return { ...cells, metaDescription: cells.body.slice(0, 160) }
const seo = (cells.seo ?? {}) as { description?: string }
if (!seo.description && typeof cells.body === 'string') {
return { ...cells, seo: { ...seo, description: cells.body.slice(0, 160) } }
}
return cells
})
Expand Down
2 changes: 1 addition & 1 deletion docs/features/content-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ The mode switch is client-only. The markdown body is the source of truth in both
| Hook | Source | Owns |
|------|--------|------|
| `useContentWorkspace` | `hooks/useContentWorkspace.ts` | Collection list, entry list, selection, CRUD operations, error state |
| `useContentEntryDraft` | `hooks/useContentEntryDraft.ts` | In-memory field state (`title`, `body`, `slug`, `featuredMediaId`, `seoTitle`, `seoDescription`), save / publish / status-change handlers |
| `useContentEntryDraft` | `hooks/useContentEntryDraft.ts` | In-memory field state (`title`, `body`, `slug`, `featuredMediaId`, `seoTitle`/`seoDescription` — merged into the structured `seo` cell on save), save / publish / status-change handlers |
| `useContentMediaPicker` | `hooks/useContentMediaPicker.ts` | Media picker modal open/close, featured media asset hydration, body media insert |

---
Expand Down
7 changes: 3 additions & 4 deletions docs/features/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ src/admin/pages/dashboard/
│ ├── DashboardGrid.module.css — the 1px-gap pattern + customize-mode transitions
│ ├── BlockLibrary.tsx — bottom-docked dock of unused widgets in customize mode
│ ├── BlockLibrary.module.css
│ ├── OnboardingPanel.tsx — first-run setup checklist
│ ├── OnboardingPanel.module.css
│ ├── LiquidProgressRing.tsx — animated liquid-filled ring (onboarding completion)
│ └── LiquidProgressRing.module.css
│ ├── OnboardingPanel.tsx — first-run setup checklist (completion ring:
│ │ the shared @ui LiquidProgressRing primitive)
│ └── OnboardingPanel.module.css
├── hooks/
│ ├── useDashboardLayout.ts — layout state (positions / sizes) + DnD + resize math
│ ├── useDashboardStats.ts — fetches /admin/api/cms/dashboard
Expand Down
2 changes: 1 addition & 1 deletion docs/features/data-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Three tiers for postType tables, enforced by the guard functions:
| Tier | Field IDs | Edit affordance | Delete affordance |
|------|-----------|-----------------|-------------------|
| Mandatory built-in | `title`, `slug` | None — locked row, no edit/delete buttons | Blocked |
| Optional built-in | `body`, `featuredMedia`, `seoTitle`, `seoDescription` | Description + required only; label locked | Allowed |
| Optional built-in | `body`, `featuredMedia`, `seo` | Description + required only; label locked | Allowed |
| Custom | all others | Fully editable | Allowed if not the primary field |

```ts
Expand Down
7 changes: 4 additions & 3 deletions docs/features/plugin-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ const pagesTable = await api.cms.content.tables.get('pages')
const pages = api.cms.content.table('pages')
const result = await pages.list({ status: 'published', limit: 50 })
const entry = await pages.get(entryId)
await pages.update(entryId, { cells: { seoTitle: 'New title' } })
await pages.update(entryId, { cells: { seo: { title: 'New title' } } })
await pages.publish(entryId)
await pages.delete(entryId)

Expand Down Expand Up @@ -585,8 +585,9 @@ Filter that runs before persistence — validate, normalize, auto-fill:
```js
api.cms.hooks.filter('content.entry.cells', (cells, { tableSlug, entryId, actor }) => {
if (tableSlug !== 'pages') return cells
if (!cells.metaDescription && typeof cells.body === 'string') {
return { ...cells, metaDescription: cells.body.slice(0, 160) }
const seo = (cells.seo ?? {}) as { description?: string }
if (!seo.description && typeof cells.body === 'string') {
return { ...cells, seo: { ...seo, description: cells.body.slice(0, 160) } }
}
return cells
})
Expand Down
20 changes: 12 additions & 8 deletions docs/features/publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,18 @@ The publisher emits `<head>` in this order:

1. `<meta charset="utf-8">`
2. `<meta name="viewport" content="width=device-width, initial-scale=1">`
3. `<title>` from `page.title`
4. `<meta name="description">` if present in page settings
5. `<link rel="icon">` if a favicon is configured
6. `<script type="importmap">` mapping bare specifiers (e.g. `three`) to `/_instatic/runtime/cache/<hash>/...` URLs
7. Runtime asset `<script>` tags (`scriptTagsForRuntimeAssets`)
8. `<link rel="stylesheet" href="/_instatic/css/<bundle>-<hash>.css">` per bundle
9. **`head` placement** plugin-injected tags (after the publisher's own head, before custom user head content)
10. `<meta http-equiv="Content-Security-Policy" content="...">` — assembled based on what's actually in the page
3. The resolved SEO block (`src/core/publisher/seoHead.ts`): `<title>`,
description, canonical, robots, Open Graph + X card tags, and one
`<script type="application/ld+json">` per JSON-LD entity. Values come from
the shared `@core/seo` resolver — the server pre-resolves page/row SEO
(incl. the configured public origin); previews/exports use `publishPage`'s
internal fallback. See [docs/features/seo.md](seo.md).
4. `<link rel="icon">` if a favicon is configured
5. `<script type="importmap">` mapping bare specifiers (e.g. `three`) to `/_instatic/runtime/cache/<hash>/...` URLs
6. Runtime asset `<script>` tags (`scriptTagsForRuntimeAssets`)
7. `<link rel="stylesheet" href="/_instatic/css/<bundle>-<hash>.css">` per bundle
8. **`head` placement** plugin-injected tags (after the publisher's own head, before custom user head content)
9. `<meta http-equiv="Content-Security-Policy" content="...">` — assembled based on what's actually in the page

Installed fonts are emitted through the CSS bundle, not external `<link>` tags. The font CSS includes self-hosted `@font-face` rules for `site.settings.fonts.items` plus `:root` declarations for editable tokens such as `--font-primary`. A page rule can therefore keep `font-family: var(--font-primary)` while the token assignment changes site-wide.

Expand Down
Loading