Skip to content

Visual Editor

The visual editor allows content editors to click and edit content directly on your site, with changes saving to Aether’s database in real-time.

sequenceDiagram
participant Editor as Visual Editor
participant SDK as Aether SDK
participant Site as Your Site
participant API as Aether API
Editor->>Site: Load in iframe (?aether-editor=true)
Site->>SDK: Initialize SDK
SDK->>SDK: Scan for data-aether-field
SDK->>Site: Highlight editable fields
Editor->>SDK: User clicks field
SDK->>Editor: Send field metadata
Editor->>Editor: Show edit panel
Editor->>API: Save changes
API->>SDK: Content updated
SDK->>Site: Update DOM

The SDK must be initialized when your site loads in editor mode:

<script>
import { AetherSDK } from '@aether-official/sites-sdk';
// Check if in editor mode
const params = new URLSearchParams(window.location.search);
if (params.get('aether-editor') === 'true') {
const sdk = new AetherSDK({
siteId: 'YOUR_SITE_ID',
organizationId: 'YOUR_ORG_ID',
editorOrigin: 'https://app.aether.com',
debug: true, // Enable logging
});
sdk.on('ready', () => {
console.log('SDK ready for editing!');
});
}
</script>

The helper API automatically adds all required data-aether-* attributes:

---
import { fetch, aether, getConfig } from '@aether-official/sites-sdk/astro';
import { SECTIONS } from '../aether-types';
const config = getConfig(import.meta.env);
const section = await fetch.section(SECTIONS.HERO, config);
const content = section.data;
// Create helper
const helper = aether.section(SECTIONS.HERO);
---
<!-- Helper automatically adds data-aether-section -->
<section {...helper.section()}>
<!-- Helper adds data-aether-field + section ID -->
<h1 {...helper.field('title')}>
{content.title}
</h1>
<p {...helper.field('subtitle')}>
{content.subtitle}
</p>
</section>

Benefits:

  • ✅ ~60% less boilerplate
  • ✅ Type-safe field names
  • ✅ Automatic attribute generation
  • ✅ Consistent naming

You can also manually add attributes if needed:

<section data-aether-section="hero-section-123">
<h1 data-aether-field="hero.title">
Welcome to Our Site
</h1>
</section>

Every editable field must be within a section:

<div data-aether-section="SECTION_ID">
<!-- Editable fields go here -->
</div>

The SECTION_ID must match a PageSection.id in your Aether database.

<h1 data-aether-field="hero.title">
Content here
</h1>
<p
data-aether-field="hero.description"
data-aether-field-type="textarea"
>
Multi-line content here
</p>
<img
src="/path/to/image.jpg"
data-aether-field="hero.image"
data-aether-field-type="image"
alt="Hero image"
/>

Use dot notation to structure your content:

<!-- Top-level field -->
<h1 data-aether-field="title">Main Title</h1>
<!-- Nested object -->
<h2 data-aether-field="hero.subtitle">Hero Subtitle</h2>
<!-- Deep nesting -->
<a
href="#"
data-aether-field="hero.cta.url"
>
<span data-aether-field="hero.cta.text">Click Here</span>
</a>

Resulting JSON structure:

{
"title": "Main Title",
"hero": {
"subtitle": "Hero Subtitle",
"cta": {
"url": "#",
"text": "Click Here"
}
}
}

Repeaters allow editing arrays of content (team members, features, etc.):

<div
data-aether-section="team-section"
data-aether-repeater="team.members"
>
<!-- Each item -->
<div data-aether-repeater-item="0">
<img
src="/alice.jpg"
data-aether-field="team.members.0.photo"
/>
<h3 data-aether-field="team.members.0.name">
Alice Johnson
</h3>
<p data-aether-field="team.members.0.role">
Lead Developer
</p>
</div>
<div data-aether-repeater-item="1">
<img src="/bob.jpg" data-aether-field="team.members.1.photo" />
<h3 data-aether-field="team.members.1.name">Bob Smith</h3>
<p data-aether-field="team.members.1.role">Designer</p>
</div>
</div>
---
const members = section.data.team.members || [];
---
<div data-aether-repeater="team.members">
{members.map((member, i) => (
<div data-aether-repeater-item={i}>
<h3 data-aether-field={`team.members.${i}.name`}>
{member.name}
</h3>
</div>
))}
</div>

Listen to SDK events for custom behavior:

const sdk = new AetherSDK(config);
// SDK initialized
sdk.on('ready', () => {
console.log('Editor ready!');
});
// Content updated
sdk.on('content:updated', (event) => {
console.log('Content changed:', event);
// event = { sectionId, fieldPath, newValue }
});
// Error occurred
sdk.on('error', (event) => {
console.error('SDK error:', event.error);
});
  1. Start your site: npm run dev (usually localhost:4321)
  2. Open Aether app: Navigate to Sites
  3. Configure site URL: Set to your local URL
  4. Open editor: Click “Open Visual Editor”
  5. Editor loads: Your site loads with ?aether-editor=true
  1. Deploy your site: Push to Vercel, Netlify, etc.
  2. Update site URL: In Aether Sites settings
  3. Open editor: Loads your production URL
  4. Test editing: Changes save to database

The SDK validates postMessage origins for security:

// SDK only accepts messages from editorOrigin
const sdk = new AetherSDK({
editorOrigin: 'https://app.aether.com', // Must match exactly
// ...
});

Your site must allow iframe embedding:

next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'ALLOW-FROM https://app.aether.com',
},
],
},
];
},
};

By default, the SDK adds inline styles to editable fields. Customize with CSS:

/* Override SDK highlights */
[data-aether-field] {
outline: 2px dashed #3b82f6 !important;
outline-offset: 4px !important;
cursor: pointer !important;
transition: outline 0.2s;
}
[data-aether-field]:hover {
outline-style: solid !important;
outline-color: #2563eb !important;
}

Apply styles only in editor mode:

/* Only in editor */
body[data-aether-editor="true"] [data-aether-field] {
outline: 2px dashed blue;
}

Set the body attribute in your HTML:

<script>
const params = new URLSearchParams(window.location.search);
if (params.get('aether-editor') === 'true') {
document.body.setAttribute('data-aether-editor', 'true');
}
</script>

Only make certain fields editable:

---
const isEditable = Astro.url.searchParams.get('aether-editor') === 'true';
---
<h1
data-aether-field={isEditable ? 'hero.title' : undefined}
>
{title}
</h1>
sdk.on('content:updated', (event) => {
const { fieldPath, newValue } = event;
// Validate email
if (fieldPath.includes('email')) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newValue)) {
console.error('Invalid email format');
// Revert or show error
}
}
// Max length
if (fieldPath.includes('title') && newValue.length > 100) {
console.warn('Title too long');
}
});

Show draft content before publishing:

---
const isDraft = Astro.url.searchParams.get('draft') === 'true';
const section = await getAetherSection(
'hero-123',
config,
{ draft: isDraft }
);
---

Check:

  1. ✅ SDK initialized (sdk:ready in console)
  2. data-aether-field attribute present
  3. ✅ Parent has data-aether-section
  4. ✅ Section ID exists in database
  5. ✅ URL has ?aether-editor=true

Check:

  1. ✅ PostMessage communication (check console)
  2. ✅ Origin validation (editorOrigin matches)
  3. ✅ No JavaScript errors blocking SDK
  4. ✅ Element is not behind another element (z-index)

Check:

  1. ✅ User authenticated in Aether
  2. ✅ Section ID matches database record
  3. ✅ Network tab shows API call
  4. ✅ No 401/403 errors in console