Skip to content

Commit de6c5e1

Browse files
authored
Toggle DatoCMS block labels on preview mode (#309)
# Changes Adds visual block labels in preview mode that allow users to quickly identify and edit blocks in DatoCMS. <img width="1125" height="839" alt="SCR-20260205-knbe" src="https://github.com/user-attachments/assets/cddd827d-8f1c-490f-9706-3c735dfb09a2" /> ## What changed - **Block labels**: Block names appear as overlays in preview mode (toggleable via "show blocks" button) - **Clickable labels**: Clicking a block label opens the block's focus field in DatoCMS editor - **Field path detection**: Automatically detects the primary editable field for each block type - **Nested block support**: Correctly handles nested blocks (e.g., blocks inside GroupingBlock items) ## Technical details - Added `block-debug-utils.ts` for field path building and API key conversion - Enhanced `download-item-types.ts` to detect focus fields (rich text, media, JSON, links, etc.) - Updated `Blocks.astro` to render debug labels with correct field paths - Updated `PreviewMode.client.ts` to handle label positioning, hover effects, and edit link generation - Added `hideDebugLabels` prop to skip labels in specific contexts (PagePartialBlock, TextBlock inline blocks) ## Usage Block labels appear automatically in preview mode when: 1. Preview mode is active 2. `PreviewModeSubscription` receives a `record` prop 3. "show blocks" button is toggled on Labels are hidden for: - PagePartialBlock children (wrong record context) - TextBlock inline blocks (embedded in rich text) ## Documentation Updated `docs/preview-mode.md` https://github.com/voorhoede/head-start/blob/feat/show-blocks/docs/preview-mode.md#block-field-path-detection # How to test - Click "show blocks" button in preview bar - Verify block labels appear as overlays - Hover over blocks to see highlight effect - Click a block label to verify it opens the correct field in DatoCMS - Test nested blocks (e.g., blocks inside GroupingBlock) - Verify labels are hidden for PagePartialBlock children and TextBlock inline blocks Test in other head-start based projects: <img width="1825" height="1216" alt="Screenshot 2026-02-05 at 11 38 35" src="https://github.com/user-attachments/assets/298589c1-3852-4bcb-8e71-3d669aa96838" /> # Checklist - [x] I have performed a self-review of my own code - [x] I have made sure that my PR is easy to review (not too big, includes comments) - [x] I have made updated relevant documentation files (in project README, docs/, etc) - ~~ I have added a decision log entry if the change affects the architecture or changes a significant technology~~ - [x] I have notified a reviewer <!-- Please strike through and check off all items that do not apply (rather than removing them) -->
1 parent aa18c42 commit de6c5e1

13 files changed

Lines changed: 646 additions & 123 deletions

File tree

config/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execSync } from 'node:child_process';
33
export const previewBranches = [
44
'preview',
55
'feat/pages-content-collection',
6-
'feat/edit-page'
6+
'feat/show-blocks'
77
];
88

99
function getGitBranch() {

docs/preview-mode.md

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const { page } = await datocmsRequest<PageQuery>({ query, variables });
7272
<h1>{page.title}</h1>
7373
```
7474

75-
The `record` prop is used to generate the "edit in CMS" link in the preview bar.
75+
The `record` prop is used to generate the "edit in CMS" link in the preview bar and to make block labels clickable (so they open the block’s field in DatoCMS).
7676

7777
## Preview mode bar
7878

@@ -117,10 +117,89 @@ query MyPage {
117117

118118
### Auto-generated files
119119

120-
- `src/lib/datocms/itemTypes.json``__typename` → item type id (generated, in `.gitignore`)
120+
- `src/lib/datocms/itemTypes.json``itemTypes[__typename]` with `id`, `name`, `focusField` (generated, in `.gitignore`)
121121

122122
When DatoCMS models change, regenerate:
123123

124124
```bash
125125
npm run prep:download-item-types
126126
```
127+
128+
## Block field path detection
129+
130+
When clicking on a block label in preview mode, the system automatically generates a field path to focus the correct field in DatoCMS. This uses the [DatoCMS content link](https://www.datocms.com/docs/content-link/how-to-use-content-link) feature, which allows linking directly to a specific field in the editor via a URL hash. Block labels are only clickable when `PreviewModeSubscription` receives a `record` prop (see above). This is done by detecting the "focus field" for each block type.
131+
132+
### How it works
133+
134+
The focus field is automatically detected based on field types:
135+
- **Rich/structured text fields** (primary content)
136+
- **Media fields** (file, video, image)
137+
- **JSON fields** (structured data like tables)
138+
- **Link fields** (with validators)
139+
- **URL text fields**
140+
141+
**Metadata fields are excluded** (e.g., `title`, `layout`, `style`, `slug`, `id`) since they're configuration rather than editable content.
142+
143+
The script finds the first field matching these criteria (excluding metadata) and uses it as the focus field. Results are written to `itemTypes.json` (id, name, focusField per block type). `Blocks.astro` reads that and builds the path; blocks that render nested `<Blocks />` (e.g. GroupingBlock) must pass an `editorFieldPath` that matches the schema path to the nested list.
144+
145+
### Field path format
146+
147+
Paths use **DatoCMS API keys** (snake_case), not GraphQL field names.
148+
149+
**How we build the path:**
150+
151+
1. **Script** ([`scripts/download-item-types.ts`](../scripts/download-item-types.ts)): For each block type, picks one focus field (or uses `focusFieldOverrides`) and writes it to `itemTypes.json`. Block types with no matching field get no `focusField`; the path then stops at the block index.
152+
2. **Blocks.astro**: Receives `editorFieldPath` (default `bodyBlocks`). Converts the first segment to API key with `toApiKey` (camelCase → snake_case, e.g. `bodyBlocks``body_blocks`). For each block at index `i`, `blockBasePath = apiKeyPath.i`; the label path is `blockBasePath` + optional `.focusField` from itemTypes. Passes `editorFieldPath={blockBasePath}` into the block (so nested blocks know their parent path).
153+
3. **Blocks with nested Blocks** (e.g. GroupingBlock): Receive `editorFieldPath` = parent's `blockBasePath` (e.g. `body_blocks.2`). They must append the schema path to the nested blocks array and pass that to `<Blocks />`. GroupingBlock does `editorFieldPath.items.{itemIndex}.blocks` (see `buildNestedFieldPath` in [`block-editor-utils.ts`](../src/blocks/block-editor-utils.ts)). Any other block that renders nested `<Blocks />` should follow the same idea so paths match the record structure in DatoCMS.
154+
155+
**Examples:**
156+
157+
| Context | Path (`data-editor-field-path`) |
158+
|--------|---------------------------|
159+
| First block on page, TableBlock (focus `table`) | `body_blocks.0.table` |
160+
| Second block, TextBlock (focus `body`) | `body_blocks.1.body` |
161+
| Block with no focus field | `body_blocks.0` |
162+
| Third block (e.g. TextBlock) inside first item of GroupingBlock at page index 2 | `body_blocks.2.items.0.blocks.2.body` |
163+
164+
The client injects the current locale (from `<html lang="...">`) after the root field when building the hash—e.g. `body_blocks.0.table``body_blocks.en.0.table`—so DatoCMS focuses the field in the correct locale.
165+
166+
**Specific cases:**
167+
168+
- **GroupingBlock**: Builds nested path as `{parentBasePath}.items.{itemIndex}.blocks`. Forwards `hideEditorLabels` to nested `<Blocks />`.
169+
170+
171+
### Hiding editor labels (hideEditorLabels)
172+
173+
Pass `hideEditorLabels` on `<Blocks />` (or on a block that forwards it, e.g. `GroupingBlock`) when you do not want to render block labels in that subtree.
174+
175+
- **PagePartialBlock children**: Blocks belong to the partial record, not the main page. Labels would open the wrong record in the editor.
176+
- **TextBlock inline blocks** (`src/blocks/TextBlock/nodes/Block.astro`): Blocks are embedded in rich text. Labels would open the wrong field or add clutter.
177+
178+
### Adding a new block
179+
180+
When you add a new block type, the focus field is automatically detected when you run:
181+
182+
```bash
183+
npm run prep:download-item-types
184+
```
185+
186+
The script will:
187+
1. Find the first field matching the criteria above
188+
2. Use it as the focus field for that block type
189+
3. Write the focus field to `itemTypes.json` (the locale and full path are resolved at runtime by `Blocks.astro` and the client)
190+
191+
### Manual override
192+
193+
If automatic detection picks the wrong field, add an override in [`scripts/download-item-types.ts`](../scripts/download-item-types.ts):
194+
195+
```typescript
196+
const focusFieldOverrides: Record<string, string> = {
197+
'card_block': 'item', // block API key -> field API key
198+
};
199+
```
200+
201+
**Finding the values:**
202+
- **Block API key**: Convert `__typename` to snake_case (`CardBlockRecord``card_block`) or check DatoCMS model settings
203+
- **Field API key**: Check DatoCMS field settings or `itemTypes[typename].focusField` in `itemTypes.json`
204+
205+
Then regenerate: `npm run prep:download-item-types`

package-lock.json

Lines changed: 57 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@rollup/plugin-graphql": "^2.0.3",
6464
"accept-language-parser": "^1.5.0",
6565
"astro": "^5.13.5",
66+
"async": "^3.2.6",
6667
"datocms-listen": "^0.1.15",
6768
"datocms-structured-text-utils": "^4.0.1",
6869
"get-video-id": "^3.6.5",
@@ -76,6 +77,7 @@
7677
"promise-all-props": "^3.0.0",
7778
"regexparam": "^3.0.0",
7879
"rosetta": "^1.1.0",
80+
"scule": "^1.3.0",
7981
"workbox-cacheable-response": "^7.3.0",
8082
"workbox-expiration": "^7.3.0",
8183
"workbox-routing": "^7.3.0",
@@ -92,6 +94,7 @@
9294
"@graphql-codegen/typescript-document-nodes": "^4.0.11",
9395
"@graphql-codegen/typescript-operations": "^4.0.1",
9496
"@types/accept-language-parser": "^1.5.6",
97+
"@types/async": "^3.2.25",
9598
"@types/dotenv-safe": "^8.1.4",
9699
"@types/eventsource": "^1.1.15",
97100
"@types/jsdom": "^21.1.7",

0 commit comments

Comments
 (0)