Skip to content

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:

html
<section data-section="hero" data-section-label="Hero Banner">
  <!-- Section content -->
</section>
AttributeRequiredDescription
data-sectionUnique identifier within the page (e.g., hero, features, cta)
data-section-labelHuman-readable label shown in the editor sidebar
data-section-typeOptional type hint (e.g., header, footer, content)
data-dynamicFlag 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:

html
<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.element or section.item-N.property
  • Be unique within the page

Complete Example

Vue Component

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

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 security

How Section Detection Works

During deployment, the snapshot capture script:

  1. Navigates to each page defined in .dcs/pages.yaml
  2. Queries the DOM for [data-section] elements
  3. For each section, extracts:
    • data-section → Section ID
    • data-section-label → Display label
    • Bounding box coordinates
  4. Scans for [data-text-key] elements within each section
  5. Records each text key with its current value
  6. 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.png

Framework-Specific Patterns

VitePress

VitePress uses Markdown with frontmatter, requiring custom components for editable sections:

vue
<!-- .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>
md
<!-- 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

tsx
// 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

typescript
// 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

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:

html
<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":

  1. Check HTML attributes - Ensure data-section and data-section-label are present
  2. Re-run snapshot capture - Snapshots may be outdated
  3. Verify deployment - Ensure the latest build with annotations is deployed

Text keys not appearing

If sections appear but text keys are missing:

  1. Check data-text-key attributes - Must be on the text element itself
  2. Verify content.yaml - Key must exist in content.yaml
  3. Check snapshot.json - Verify text keys were captured

Section boundaries wrong

If section boundaries don't match the visual layout:

  1. Check element structure - data-section should be on the outermost container
  2. Check CSS - Ensure no overflow/clip issues affecting bounding box
  3. Re-capture - Bounding boxes are calculated at capture time

Best Practices

  1. Use semantic section IDs - hero, features, testimonials not section-1, section-2
  2. Provide clear labels - "Hero Banner" is better than "Hero"
  3. Keep text keys consistent - Use section.element naming pattern
  4. Include defaults - Always provide fallback values in composables
  5. Test locally - Run snapshot capture locally before deploying
  6. Document custom patterns - Note any non-standard implementations

Duff Cloud Services Documentation