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] -->|Section::resolve| 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"
SR["Section::resolve()"]
BCol["BlockCollection"]
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| SR
SR -->|produces| BCol
BCol -->|contains| 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 Section Resolution
Section titled “1. During Section Resolution”// In Section::resolve()foreach ($content as $index => $block) { $blockData = new GenericBlockData($block['data']);
$sourceMap = new CmsSourceMap( sectionId: $this->id, sectionSlug: $this->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{ use InteractsWithVisualEditing;
public string $label = ''; public string $headline = ''; public array $cmsSourceMap = [];
// autoHydrate() handles $label and $headline
protected function initializeVisualEditing(): void { $block = $this->block?->first(); if ($block !== null) { $this->cmsSourceMap = $block->getSourceMap(); } }
protected function getCmsSourceMap(): array { return $this->cmsSourceMap; }
// No resolveFieldValue() needed — base class provides default}3. When Rendering Fields
Section titled “3. When Rendering Fields”renderField() is provided by the InteractsWithVisualEditing trait. 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 Sites --}}The 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->getRawContent(); $content[$map->blockIndex]['data'][$map->field] = $this->value; $this->section->update(['content' => $content]);
// Notify frontend to refresh $this->dispatch('contentUpdated', sectionId: $map->sectionId);}5. Refreshing the Frontend
Section titled “5. Refreshing the Frontend”// InteractsWithVisualEditing traitpublic function refreshFromCMS(mixed $sectionId): void{ if (! $this->isVisualEditingActive()) { return; }
$sourceMapSectionId = $this->getCmsSourceMap()['section_id'] ?? null; if ($sourceMapSectionId != $sectionId) { return; }
$section = Section::find($sectionId); $this->block = $section->resolve(); $this->blockData = $this->block?->toArray() ?? []; $this->autoHydrate();}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=0Key Normalization
Section titled “Key Normalization”InteractsWithVisualEditing includes normalizeSourceMapKeys() which converts camelCase keys from buildSourceMap() to snake_case for InlineEditForm:
// buildSourceMap() generates:['sectionId' => 12, 'blockType' => 'hero', 'pageId' => 5]
// normalizeSourceMapKeys() converts to:['section_id' => 12, 'block_type' => 'hero', 'page_id' => 5]This happens automatically — no manual normalization needed.
Best Practices
Section titled “Best Practices”- Use
renderField()instead of raw{{ }}for editable content - Initialize
$this->cmsSourceMapininitializeVisualEditing() - Use
wire:keywith source map hash onInlineEditFormto prevent schema caching - Source maps are lightweight — no performance concern