Visual Editor Annotations
This document describes the HTML data attributes required for the DCS portal's visual page editor to detect and enable editing of page sections and text content.
Overview
The visual page editor displays an interactive preview of customer site pages, allowing users to:
- View page sections with labeled boundaries
- Edit text content inline
- See changes reflected in real-time
For this to work, site HTML must include specific data-* attributes that the snapshot capture script detects.
Required Attributes
Section Container: data-section
Every editable section of a page must be wrapped in an element with data-section:
<section data-section="hero" data-section-label="Hero Banner">
<!-- Section content -->
</section>| Attribute | Required | Description |
|---|---|---|
data-section | ✅ | Unique identifier within the page (e.g., hero, features, cta) |
data-section-label | ✅ | Human-readable label shown in the editor sidebar |
data-section-type | ❌ | Optional type hint (e.g., header, footer, content) |
data-dynamic | ❌ | Flag for dynamically generated content (e.g., blog post lists) |
Text Content: data-text-key
Every editable text element must have data-text-key matching a key in .dcs/content.yaml:
<h1 data-text-key="hero.heading">{{ t('hero.heading') }}</h1>
<p data-text-key="hero.description">{{ t('hero.description') }}</p>
<button data-text-key="hero.cta">{{ t('hero.cta') }}</button>The data-text-key value must:
- Match a key in
content.yaml(either global or page-specific) - Follow dot notation:
section.elementorsection.item-N.property - Be unique within the page
Complete Example
Vue Component
<script setup lang="ts">
import { useTextContent } from '@duffcloudservices/cms'
const { t } = useTextContent({
pageSlug: 'home',
defaults: {
'hero.heading': 'Welcome to Our Site',
'hero.subheading': 'Build something amazing',
'hero.cta': 'Get Started',
'features.heading': 'Why Choose Us',
'features.item-1.title': 'Fast',
'features.item-1.description': 'Lightning fast performance',
'features.item-2.title': 'Secure',
'features.item-2.description': 'Enterprise-grade security',
}
})
</script>
<template>
<main>
<!-- Hero Section -->
<section data-section="hero" data-section-label="Hero Banner">
<h1 data-text-key="hero.heading">{{ t('hero.heading') }}</h1>
<p data-text-key="hero.subheading">{{ t('hero.subheading') }}</p>
<button data-text-key="hero.cta">{{ t('hero.cta') }}</button>
</section>
<!-- Features Section -->
<section data-section="features" data-section-label="Features">
<h2 data-text-key="features.heading">{{ t('features.heading') }}</h2>
<div class="feature-grid">
<div class="feature">
<h3 data-text-key="features.item-1.title">{{ t('features.item-1.title') }}</h3>
<p data-text-key="features.item-1.description">{{ t('features.item-1.description') }}</p>
</div>
<div class="feature">
<h3 data-text-key="features.item-2.title">{{ t('features.item-2.title') }}</h3>
<p data-text-key="features.item-2.description">{{ t('features.item-2.description') }}</p>
</div>
</div>
</section>
</main>
</template>Matching content.yaml
# .dcs/content.yaml
version: 1
lastUpdated: "2026-01-08T00:00:00Z"
global:
nav.home: Home
nav.about: About
footer.copyright: © 2026 Company. All rights reserved.
pages:
home:
hero.heading: Welcome to Our Site
hero.subheading: Build something amazing
hero.cta: Get Started
features.heading: Why Choose Us
features.item-1.title: Fast
features.item-1.description: Lightning fast performance
features.item-2.title: Secure
features.item-2.description: Enterprise-grade securityHow Section Detection Works
During deployment, the snapshot capture script:
- Navigates to each page defined in
.dcs/pages.yaml - Queries the DOM for
[data-section]elements - For each section, extracts:
data-section→ Section IDdata-section-label→ Display label- Bounding box coordinates
- Scans for
[data-text-key]elements within each section - Records each text key with its current value
- Captures a screenshot of the section
The resulting snapshot is stored in blob storage:
/{siteSlug}/site-snapshots/
├── manifest.json # Page list and capture metadata
└── {pageSlug}/
├── snapshot.json # Page data including sections and text keys
├── full-page.png # Full page screenshot
└── sections/
├── hero.png # Individual section screenshots
└── features.pngFramework-Specific Patterns
VitePress
VitePress uses Markdown with frontmatter, requiring custom components for editable sections:
<!-- .vitepress/theme/components/EditableSection.vue -->
<script setup lang="ts">
defineProps<{
sectionId: string
label: string
}>()
</script>
<template>
<section :data-section="sectionId" :data-section-label="label">
<slot />
</section>
</template><!-- docs/index.md -->
<EditableSection section-id="hero" label="Hero Banner">
# Welcome to CloudSync
<span data-text-key="home.hero.tagline">Cloud sync made simple</span>
</EditableSection>React
// src/components/Section.tsx
interface SectionProps {
id: string
label: string
children: React.ReactNode
}
export function Section({ id, label, children }: SectionProps) {
return (
<section data-section={id} data-section-label={label}>
{children}
</section>
)
}
// src/pages/Home.tsx
import { useTextContent } from '@duffcloudservices/cms-react'
import { Section } from '../components/Section'
export function Home() {
const { t } = useTextContent({ pageSlug: 'home', defaults: {...} })
return (
<Section id="hero" label="Hero Banner">
<h1 data-text-key="hero.heading">{t('hero.heading')}</h1>
<p data-text-key="hero.description">{t('hero.description')}</p>
</Section>
)
}Angular
// src/app/components/section.component.ts
@Component({
selector: 'app-section',
template: `
<section [attr.data-section]="sectionId" [attr.data-section-label]="label">
<ng-content></ng-content>
</section>
`
})
export class SectionComponent {
@Input() sectionId!: string
@Input() label!: string
}Astro
---
// src/components/Section.astro
interface Props {
id: string
label: string
}
const { id, label } = Astro.props
---
<section data-section={id} data-section-label={label}>
<slot />
</section>Dynamic Content
For dynamically generated content (e.g., blog post lists), use data-dynamic to indicate the section contains generated content:
<section data-section="blog-posts" data-section-label="Blog Posts" data-dynamic>
<h2 data-text-key="posts.heading">{{ t('posts.heading') }}</h2>
<!-- Blog posts are rendered dynamically, not directly editable -->
<div v-for="post in posts" :key="post.id">
<h3>{{ post.title }}</h3>
</div>
</section>Dynamic sections will appear in the editor but individual items won't have editable text keys.
Troubleshooting
"No sections detected"
If the page editor shows "No sections detected":
- Check HTML attributes - Ensure
data-sectionanddata-section-labelare present - Re-run snapshot capture - Snapshots may be outdated
- Verify deployment - Ensure the latest build with annotations is deployed
Text keys not appearing
If sections appear but text keys are missing:
- Check
data-text-keyattributes - Must be on the text element itself - Verify content.yaml - Key must exist in content.yaml
- Check snapshot.json - Verify text keys were captured
Section boundaries wrong
If section boundaries don't match the visual layout:
- Check element structure -
data-sectionshould be on the outermost container - Check CSS - Ensure no overflow/clip issues affecting bounding box
- Re-capture - Bounding boxes are calculated at capture time
Best Practices
- Use semantic section IDs -
hero,features,testimonialsnotsection-1,section-2 - Provide clear labels - "Hero Banner" is better than "Hero"
- Keep text keys consistent - Use
section.elementnaming pattern - Include defaults - Always provide fallback values in composables
- Test locally - Run snapshot capture locally before deploying
- Document custom patterns - Note any non-standard implementations
