Source Maps
Source maps are the key mechanism that enables visual editing. They trace every rendered HTML element back to its exact origin in the database.
What is a Source Map?
Section titled “What is a Source Map?”A source map is an immutable value object that answers: “Where did this piece of content come from?”
graph LR
A[Database Record] -->|ContentResolver| B[BlockData]
B -->|getSourceMap| C[CmsSourceMap]
C -->|toJson| D[data-cms-source]
D -->|parsed by| E[VisualEditor]
E -->|loads| F[InlineEditForm]
The Source Map Chain
Section titled “The Source Map Chain”graph TD
subgraph "Database"
Page["Page: id=5, slug='about'"]
Section["Section: id=12, slug='hero'"]
Content["Content JSON: [{type:'hero', data:{...}}]"]
end
subgraph "Resolution"
CR["ContentResolver"]
SC["SectionContent"]
BD["BlockData"]
end
subgraph "Frontend"
LC["Livewire Component"]
HTML["HTML element with data-cms-source"]
end
Page -->|has| Section
Section -->|stores| Content
Content -->|resolved by| CR
CR -->|produces| SC
SC -->|wraps| BD
BD -->|carries| SM["Source Map"]
BD -->|rendered by| LC
LC -->|outputs| HTML
CmsSourceMap Structure
Section titled “CmsSourceMap Structure”JFA\VeFilamentCMSLivewire\CmsSourceMap is a final readonly class with these properties:
| Property | Type | Description |
|---|---|---|
sectionId | ?int | The section’s database ID |
sectionSlug | ?string | The section’s slug |
blockType | ?string | Block type (e.g., ‘hero’) |
blockIndex | ?int | Index in the content array |
pageId | ?int | The page’s database ID |
field | ?string | Specific field name (added by trait) |
fieldType | ?string | Field type: text, textarea, rich_text, image, repeater |
itemIndex | ?int | For repeater items |
parentBlockIndex | ?int | Parent block for nested repeaters |
How Source Maps Are Created
Section titled “How Source Maps Are Created”1. During Content Resolution
Section titled “1. During Content Resolution”// In ContentResolverforeach ($content as $index => $block) { $blockData = new BlockData($block['data']);
$sourceMap = new CmsSourceMap( sectionId: $section->id, sectionSlug: $section->slug, blockType: $block['type'], blockIndex: $index, pageId: $pageId, );
$blockData = $blockData->withSourceMap($sourceMap);}2. In the Frontend Component
Section titled “2. In the Frontend Component”class Hero extends SectionComponent{ public array $cmsSourceMap = [];
protected function hydrateFromContent(SectionContent $content): void { $this->label = $content->hero->label ?? ''; }
protected function initializeVisualEditing(): void { $sourceMap = $this->sectionContent->hero->getSourceMap(); if ($sourceMap !== null) { $this->cmsSourceMap = $sourceMap; } }
protected function getCmsSourceMap(): array { return $this->cmsSourceMap; }
protected function resolveFieldValue(string $field): mixed { return match ($field) { 'label' => $this->label, default => null, }; }}3. When Rendering Fields
Section titled “3. When Rendering Fields”renderField() is provided by the InteractsWithVisualEditing trait in ve-filament-cms-livewire. It works automatically:
// In your Blade view:<h1>{!! $this->renderField('headline', 'text') !!}</h1>
// When visual editing is active, this outputs:// <span data-cms-source='{"sectionId":12,"field":"headline","fieldType":"text",...}'>Build Amazing Sites</span>
// When visual editing is inactive, it outputs plain text:// Build Amazing SitesThe method handles source map building, field type injection, and label resolution automatically.
Source Map JSON Example
Section titled “Source Map JSON Example”{ "sectionId": 12, "sectionSlug": "hero", "blockType": "hero", "blockIndex": 0, "pageId": 5, "field": "headline", "fieldType": "text", "fieldLabel": "Headline"}This JSON is embedded in HTML:
<span data-cms-source='{"sectionId":12,"sectionSlug":"hero",...}'> Build Amazing Sites</span>How Visual Editing Uses Source Maps
Section titled “How Visual Editing Uses Source Maps”1. DOM Instrumentation
Section titled “1. DOM Instrumentation”When editing mode is active, JavaScript scans for data-cms-source attributes:
// Pseudocodeconst editableElements = document.querySelectorAll('[data-cms-source]');editableElements.forEach(el => { el.addEventListener('click', () => { const sourceMap = JSON.parse(el.dataset.cmsSource); Livewire.dispatch('openEditor', sourceMap); });});2. Loading the Editor
Section titled “2. Loading the Editor”// VisualEditor::openEditor()public function openEditor(array $sourceMap): void{ $this->activeSourceMap = $sourceMap; $this->isModalOpen = true;
// Open Filament slide-over modal $this->dispatch('open-modal', id: 'edit-content');}3. Loading Content
Section titled “3. Loading Content”// InlineEditForm::loadFromSourceMap()public function loadFromSourceMap(): void{ $map = CmsSourceMap::fromJson(json_encode($this->sourceMap));
$this->section = Section::find($map->sectionId); $block = $this->section->content[$map->blockIndex]; $this->value = $block['data'][$map->field];}4. Saving Changes
Section titled “4. Saving Changes”// InlineEditForm::save()public function save(): void{ $map = CmsSourceMap::fromJson(json_encode($this->sourceMap));
// Update the specific field in the content array $content = $this->section->content; $content[$map->blockIndex]['data'][$map->field] = $this->value; $this->section->content = $content; $this->section->save();
// Notify frontend to refresh $this->dispatch('contentUpdated', sectionId: $map->sectionId);}Why Source Maps Matter
Section titled “Why Source Maps Matter”Source maps solve a critical problem: how do we know which database field a rendered HTML element corresponds to?
Without source maps, visual editing would require:
- Complex DOM traversal heuristics
- Fragile CSS selector matching
- Assumptions about HTML structure
With source maps:
- Every element carries its own provenance metadata
- No guessing or heuristics needed
- Works with any HTML structure
- Supports nested repeaters and complex layouts
Immutable Design
Section titled “Immutable Design”CmsSourceMap is immutable. You create scoped variants without mutating the original:
$baseMap = new CmsSourceMap( sectionId: 12, sectionSlug: 'hero', blockType: 'hero', blockIndex: 0, pageId: 5,);
// Scope to a specific field$fieldMap = $baseMap->forField('headline', 'text');// Adds: field='headline', fieldType='text'
// Scope to a repeater item$itemMap = $baseMap->forRepeaterItem(2);// Adds: itemIndex=2, parentBlockIndex=0Best Practices
Section titled “Best Practices”- Always pass source maps through
BlockData::withSourceMap() - Store the source map array in
$this->cmsSourceMap - Use
renderField()instead of raw{{ }}for editable content - Include
field_typeto help the editor render the right form control - Add
field_labelviaHasFieldLabelsfor better UX