Skip to content

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.

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]
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

JFA\VeFilamentCMSLivewire\CmsSourceMap is a final readonly class with these properties:

PropertyTypeDescription
sectionId?intThe section’s database ID
sectionSlug?stringThe section’s slug
blockType?stringBlock type (e.g., ‘hero’)
blockIndex?intIndex in the content array
pageId?intThe page’s database ID
field?stringSpecific field name (added by trait)
fieldType?stringField type: text, textarea, rich_text, image, repeater
itemIndex?intFor repeater items
parentBlockIndex?intParent block for nested repeaters
// In ContentResolver
foreach ($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);
}
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,
};
}
}

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 Sites

The method handles source map building, field type injection, and label resolution automatically.

{
"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>

When editing mode is active, JavaScript scans for data-cms-source attributes:

// Pseudocode
const editableElements = document.querySelectorAll('[data-cms-source]');
editableElements.forEach(el => {
el.addEventListener('click', () => {
const sourceMap = JSON.parse(el.dataset.cmsSource);
Livewire.dispatch('openEditor', sourceMap);
});
});
// 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');
}
// 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];
}
// 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);
}

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

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=0
  • 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_type to help the editor render the right form control
  • Add field_label via HasFieldLabels for better UX