@aether-official/sites-sdk
v1.3.4
Published
Type-safe headless CMS SDK for Astro with visual editing
Maintainers
Readme
@aether-official/sites-sdk
Official JavaScript SDK for integrating Aether's Visual Headless CMS into your static sites.
✨ What's New in v1.3
- 🎯 TypeScript Type Generation - Auto-generate types from your CMS schema with the CLI tool
- 📝 Type-Safe Content Fetching - Full autocomplete and type checking for all content fields
- 🔧 Unified Fetch API -
fetchnamespace for cleaner, more intuitive API - 🛠️ CLI Tool - Interactive setup wizard and schema synchronization
- 🌐 Global Settings - Access site-wide settings (contact, hours, social)
- 🔍 SEO Metadata - Complete page-level SEO management
See Changelog | Developer Guide
Features
- 🎯 Type Generation - Auto-generate TypeScript types from CMS schema
- 📝 Type-Safe API - Full autocomplete and type checking for content
- 🔧 CLI Tool - Interactive setup and schema synchronization
- 🎨 Helper API - Reduce boilerplate with intuitive helper functions
- 🖼️ Visual Editor - Enable inline content editing with postMessage communication
- 📦 Content Fetching - Server-side and build-time content retrieval
- 🚀 Astro First - First-class Astro integration with auto-configuration
- ⚡ Optimized - Minified bundle with tree-shaking support
Installation
# Install the SDK
npm install @aether-official/sites-sdk
# Install the CLI (for type generation)
npm install -D @aether-official/cliRequirements: Astro 3.5.0+ (for auto-middleware injection)
Quick Start (5 Minutes)
1. Install Packages
npm install @aether-official/sites-sdk
npm install -D @aether-official/cli2. Run Interactive Setup
npx aether-sdk initThis will:
- Prompt for your credentials (Site ID, Org ID, API Token)
- Test the connection
- Create
.envfile - Fetch your schema
- Generate TypeScript types
3. Add Astro Integration
// astro.config.mjs
import { defineConfig } from 'astro/config';
import aetherSites from '@aether-official/sites-sdk/astro';
export default defineConfig({
integrations: [
aetherSites({
siteId: process.env.AETHER_SITE_ID,
organizationId: process.env.AETHER_ORG_ID,
editorOrigin: process.env.PUBLIC_AETHER_EDITOR_ORIGIN || 'https://myaether.cloud',
enabled: true,
})
]
});4. Use Type-Safe Fetching
---
import { fetch, aether, getConfig } from '@aether-official/sites-sdk/astro';
import { SECTIONS } from '../aether-types'; // Auto-generated!
import type { HeroData } from '../aether-types';
const config = getConfig(import.meta.env);
// Fetch section content
const section = await fetch.section(SECTIONS.HERO, config);
const content = section.data as HeroData;
// Create helper for visual editing
const helper = aether.section(SECTIONS.HERO);
---
<section {...helper.section()}>
<h1 {...helper.field('title', content.title)}>
{content.title}
</h1>
<p {...helper.field('subtitle', content.subtitle)}>
{content.subtitle}
</p>
</section>That's it! You now have:
- ✅ Full TypeScript autocomplete for all content fields
- ✅ Visual inline editing with
?aether-editor=true - ✅ Auto-generated types that stay in sync with your CMS
What Just Happened? 🎉
1. Auto-Middleware Injection ✨
The SDK automatically:
- Injects middleware for CSP header configuration
- Enables visual editing with
?aether-editor=truequery parameter - No manual middleware file creation required!
2. Helper API Reduces Boilerplate 🎨
Before (v1.0.x - verbose):
<section data-aether-section="hero-123">
<h1
data-aether-field="title"
data-aether-section="hero-123"
data-aether-value={content.title}
>
{content.title}
</h1>
</section>After (v1.1.0+ - concise):
<section {...helper.section()}>
<h1 {...helper.field('title')}>
{content.title}
</h1>
</section>~60% reduction in attribute boilerplate! 🚀
Note: helper.field() only requires the field name - the value is displayed separately in your template.
3. Type Generation Keeps Types in Sync 🎯
When you update fields in the CMS:
# Sync latest schema and regenerate types
npx aether-sdk sync && npx aether-sdk typesYour IDE instantly knows about:
- New sections you created
- New fields you added
- Changed field types
- Renamed fields
No manual type definitions needed! The CLI automatically generates TypeScript interfaces from your CMS schema.
CLI Tool Reference
The Aether CLI provides commands to manage your SDK integration:
aether-sdk init
Interactive setup wizard that configures your project:
npx aether-sdk initWhat it does:
- Prompts for Site ID, Organization ID, and API Token
- Tests your connection to Aether CMS
- Creates
.envfile with credentials - Fetches your site's schema
- Generates TypeScript types
aether-sdk sync
Fetch the latest schema from your CMS:
npx aether-sdk syncUse when:
- You created new sections in the CMS
- You added/modified fields
- You want to update your local schema cache
aether-sdk types
Generate TypeScript types from cached schema:
npx aether-sdk typesGenerates:
src/aether-types.d.ts- Section IDs and data interfaces- Full autocomplete for all content fields
- Type-safe constants for section IDs
Workflow Example
# Initial setup
npx aether-sdk init
# After creating sections in CMS
npx aether-sdk sync && npx aether-sdk types
# Use in your code with full type safetyHelper API Reference
Creating a Helper
import { aether } from '@aether-official/sites-sdk/astro';
const helper = aether.section('section-id');Helper Methods
helper.section()
Returns attributes for section wrapper:
<section {...helper.section()}>
<!-- Content -->
</section>helper.field(name, value?)
Returns attributes for editable fields. Pass the value for the editor to track changes:
<h1 {...helper.field('title', content.title)}>
{content.title}
</h1>
<!-- For images -->
{content.hero_image && (
<img
src={content.hero_image.url}
alt={content.hero_image.alt || 'Alt text'}
{...helper.field('hero_image', content.hero_image)}
/>
)}Note: Image fields are returned as ImageField objects with {url, alt, width, height} properties.
helper.repeater(name, items)
Returns attributes for repeater fields:
<div {...helper.repeater('items', content.items)}>
{content.items.map((item, index) => (
<div {...helper.repeaterItem(index)}>
<h3 {...helper.field('name', item.name)}>{item.name}</h3>
</div>
))}
</div>helper.repeaterItem(index)
Returns attributes for repeater items:
<div {...helper.repeaterItem(0)}>
<!-- Repeater item content -->
</div>Check Editor Mode
import { aether } from '@aether-official/sites-sdk/astro';
if (aether.isEditorMode()) {
// Currently in visual editor
}Configuration Options
interface AetherAstroConfig {
siteId: string; // Required: Site ID from Aether dashboard
organizationId: string; // Required: Organization ID
editorOrigin?: string; // Optional: Editor URL (default: https://myaether.cloud)
enabled?: boolean; // Optional: Enable SDK (default: true)
debug?: boolean; // Optional: Enable debug logging (default: false)
editableSelector?: string; // Optional: Custom selector (default: '[data-aether-editable]')
autoMiddleware?: boolean; // Optional: Auto-inject middleware (default: true)
}Working with Components
Component instances are reusable content blocks that can be attached to sections. Here's how to fetch and render them:
Fetching Components
---
import { fetch, aether, getConfig } from '@aether-official/sites-sdk/astro';
import { SECTIONS } from '../aether-types';
import type { FoundationOfExcellenceData } from '../aether-types';
const config = getConfig(import.meta.env);
// Fetch section content
const section = await fetch.section(SECTIONS.FOUNDATION_OF_EXCELLENCE, config);
const content = section.data as FoundationOfExcellenceData;
// Fetch component instances attached to this section
const components = await fetch.components(SECTIONS.FOUNDATION_OF_EXCELLENCE, config);
// Filter components by type/slug
const statComponents = components.filter(c => c.component.slug === 'stat');
const buttonComponents = components.filter(c => c.component.slug === 'button');
const helper = aether.section(SECTIONS.FOUNDATION_OF_EXCELLENCE);
---Rendering Components with React/Vue
For components that need client-side interactivity, create a React/Vue component:
// src/components/StatCard.tsx
import React from 'react';
interface StatCardProps {
number: string;
label: string;
'data-aether-component'?: string;
'data-aether-section'?: string;
}
export const StatCard: React.FC<StatCardProps> = ({
number,
label,
'data-aether-component': dataAetherComponent,
'data-aether-section': dataAetherSection,
}) => {
return (
<>
<div
data-aether-component={dataAetherComponent}
data-aether-section={dataAetherSection}
data-aether-field="numeric_stat"
>
{number}
</div>
<div
data-aether-component={dataAetherComponent}
data-aether-section={dataAetherSection}
data-aether-field="stat_desc"
>
{label}
</div>
</>
);
};Using Components in Astro
---
import { StatCard } from '../components/StatCard.tsx';
// ... component fetching code from above
---
<section {...helper.section()}>
<!-- Render component instances -->
{statComponents.map(comp => (
<StatCard
number={comp.data.numeric_stat}
label={comp.data.stat_desc}
data-aether-component={comp.id}
data-aether-section={SECTIONS.FOUNDATION_OF_EXCELLENCE}
client:load
/>
))}
</section>Key Points:
- Use
data-aether-component={comp.id}to enable component editing - Use
data-aether-field="fieldName"on individual field elements - Include
data-aether-sectionfor proper editor context - Use
client:load(orclient:visible, etc.) for hydration
Advanced Usage
Disable Auto-Middleware
If you need custom middleware or want to disable auto-injection:
// astro.config.mjs
export default defineConfig({
integrations: [
aetherSites({
autoMiddleware: false,
// ... other config
})
]
});Then manually create src/middleware.ts:
import { onRequest as aetherMiddleware } from '@aether-official/sites-sdk/middleware';
export const onRequest = aetherMiddleware;Custom Middleware Sequence
Combine Aether middleware with your own:
import { sequence } from 'astro:middleware';
import { onRequest as aetherMiddleware } from '@aether-official/sites-sdk/middleware';
import { myCustomMiddleware } from './my-middleware';
export const onRequest = sequence(
aetherMiddleware, // Aether runs first
myCustomMiddleware, // Your middleware runs second
);Content Fetching
import { getAetherSection, getAetherPage } from '@aether-official/sites-sdk/astro';
// Fetch single section
const hero = await getAetherSection('section-id', config);
// Fetch entire page with all sections
const page = await getAetherPage('page-slug', config);Common Patterns
Handling Image Fields
Image fields are returned as ImageField objects with url, alt, width, and height properties:
---
const section = await fetch.section(SECTIONS.HERO, config);
const content = section.data as HeroData;
---
<!-- Image fields are objects -->
{content.hero_image && (
<img
src={content.hero_image.url}
alt={content.hero_image.alt || 'Hero image'}
width={content.hero_image.width}
height={content.hero_image.height}
{...helper.field('hero_image')}
/>
)}Conditional Content Rendering
<!-- Only render if field has content -->
{content.optional_text && (
<p {...helper.field('optional_text')}>
{content.optional_text}
</p>
)}
<!-- With fallback content -->
<h1 {...helper.field('title')}>
{content.title || 'Default Title'}
</h1>Filtering Component Types
---
const components = await fetch.components(SECTIONS.MY_SECTION, config);
// Get specific component types
const buttons = components.filter(c => c.component.slug === 'button');
const cards = components.filter(c => c.component.slug === 'card');
const stats = components.filter(c => c.component.slug === 'stat');
// Check if components exist
const hasButtons = buttons.length > 0;
---
{hasButtons && (
<div class="button-group">
{buttons.map(btn => (
<a href={btn.data.url} {...helper.field('url')}>
{btn.data.label}
</a>
))}
</div>
)}Complete Section Example
---
import { fetch, aether, getConfig } from '@aether-official/sites-sdk/astro';
import { SECTIONS } from '../aether-types';
import type { AboutSectionData } from '../aether-types';
import { StatCard } from '../components/StatCard.tsx';
const config = getConfig(import.meta.env);
let section;
let content: AboutSectionData;
let components: any[] = [];
try {
section = await fetch.section(SECTIONS.ABOUT, config);
content = section.data as AboutSectionData;
components = await fetch.components(SECTIONS.ABOUT, config);
} catch (e) {
console.error('Failed to fetch section:', e);
// Fallback content
content = {
headline: 'Default Headline',
subheadline: 'Default subheadline',
};
}
const helper = aether.section(SECTIONS.ABOUT);
const statComponents = components.filter(c => c.component.slug === 'stat');
---
<section {...helper.section()}>
<h2 {...helper.field('headline')}>{content.headline}</h2>
<p {...helper.field('subheadline')}>{content.subheadline}</p>
{statComponents.length > 0 && (
<div class="stats">
{statComponents.map(comp => (
<StatCard
number={comp.data.value}
label={comp.data.label}
data-aether-component={comp.id}
data-aether-section={SECTIONS.ABOUT}
client:load
/>
))}
</div>
)}
</section>Troubleshooting
Types Not Updating
Problem: Generated types don't reflect changes made in Aether CMS
Solution:
# Clear cache and regenerate
rm -rf .aether
npx aether-sdk sync && npx aether-sdk types
# Restart dev server
npm run devConnection Failed
Problem: Connection failed: Unauthorized during aether-sdk init
Solution:
- Verify API token in Aether dashboard → Sites → Your Site → API
- Check API URL is correct (
http://localhost:3000for development) - Ensure API token hasn't expired
Visual Editor Not Loading
Verify Astro version: Requires Astro 3.5.0+ for auto-middleware
npm list astroCheck browser console: Look for CSP errors
Verify query parameter: URL must include
?aether-editor=trueTest manually: Try adding middleware manually to verify it's a middleware issue
Migration from v1.0.x
See detailed Migration Guide for step-by-step upgrade instructions.
Links
- 📖 Setup Guide - Detailed setup instructions
- 🔄 Migration Guide - Upgrade from v1.0.x
- 📝 Changelog - Version history
- 🐛 Issues
- 📚 Documentation
License
MIT © Aether Team
