Astro Integration
Astro is the recommended framework for building sites with Aether’s visual headless CMS. This guide covers everything from setup to deployment.
Why Astro?
Section titled “Why Astro?”- Build-Time Content Fetching - Content fetched during build for maximum performance
- Auto-Middleware Injection - SDK automatically configures CSP headers
- Type-Safe Development - Full TypeScript autocomplete for all content fields
- Islands Architecture - SDK only loads when needed (in editor mode)
- Framework Agnostic - Use React, Vue, Svelte, or vanilla JS for interactive parts
- Static Site Generation - Fast, SEO-friendly sites with optional SSR
Quick Start
Section titled “Quick Start”1. Create an Astro Project
Section titled “1. Create an Astro Project”npm create astro@latest my-aether-sitecd my-aether-site2. Install the SDK and CLI
Section titled “2. Install the SDK and CLI”npm install @aether-official/sites-sdknpm install -D @aether-official/cli3. Run Interactive Setup
Section titled “3. 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 in
src/aether-types.d.ts
4. Add Astro Integration
Section titled “4. Add Astro Integration”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 || 'http://localhost:3000', enabled: true, }) ]});5. Create a Page with Type-Safe Content
Section titled “5. Create a Page with Type-Safe Content”---import { fetch, aether } from '@aether-official/sites-sdk/astro';import { SECTIONS } from '../aether-types'; // Auto-generated!import { getConfig } from '@aether-official/sites-sdk/astro';
// SECURITY: Use process.env (server-only) to keep API token secure// Never use import.meta.env as it can expose secrets to the browserconst config = getConfig(process.env as Record<string, string | undefined>);
// Type-safe with full autocomplete!const hero = await fetch.section(SECTIONS.HERO, config);const helper = aether.section(SECTIONS.HERO);---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>{hero.data.title}</title></head><body> <section {...helper.section()}> <h1 {...helper.field('title')}> {hero.data.title} </h1> <p {...helper.field('subtitle')}> {hero.data.subtitle} </p> </section></body></html>That’s it! The Astro integration automatically:
- Injects middleware for CSP headers
- Initializes the SDK for visual editing
- No manual script tags needed!
Security Best Practices
Section titled “Security Best Practices”API Token Security
Section titled “API Token Security”CRITICAL: Never expose your AETHER_API_TOKEN to the browser!
---// ✅ CORRECT: Use process.env (server-side only)const config = getConfig(process.env as Record<string, string | undefined>);
// ❌ WRONG: Never use import.meta.env for API tokens// const config = getConfig(import.meta.env); // This can expose tokens to browser!---Why this matters:
process.envis only available server-side during SSR/SSG buildsimport.meta.envvariables (especiallyPUBLIC_*) are bundled into browser code- API tokens should never be accessible from the browser
- Vercel and other platforms expose
process.envduring build time
Environment Variables:
# Vercel/Netlify Dashboard - Server-side only (build-time)AETHER_API_TOKEN=your_token_hereAETHER_API_URL=https://your-platform.com
# Public variables (safe for browser)PUBLIC_AETHER_SITE_ID=your_site_idPUBLIC_AETHER_ORGANIZATION_ID=your_org_idPUBLIC_AETHER_EDITOR_ORIGIN=https://your-platform.comContent Fetching Patterns
Section titled “Content Fetching Patterns”Single Section with Type Safety
Section titled “Single Section with Type Safety”---import { fetch, aether } from '@aether-official/sites-sdk/astro';import { SECTIONS } from '../aether-types'; // Auto-generated types!import { getConfig } from '@aether-official/sites-sdk/astro';
// SECURITY: Use process.env to keep API tokens server-side onlyconst config = getConfig(process.env as Record<string, string | undefined>);
// Full autocomplete for section ID and all fields!const hero = await fetch.section(SECTIONS.HERO, config);const helper = aether.section(SECTIONS.HERO);---
<section {...helper.section()}> <h1 {...helper.field('title')}> {hero.data.title} </h1></section>Multiple Sections in Parallel
Section titled “Multiple Sections in Parallel”---import { fetch, aether } from '@aether-official/sites-sdk/astro';import { SECTIONS } from '../aether-types';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);
// Fetch multiple sections in parallel for better performanceconst [hero, features, cta] = await Promise.all([ fetch.section(SECTIONS.HERO, config), fetch.section(SECTIONS.FEATURES, config), fetch.section(SECTIONS.CTA, config),]);
const heroHelper = aether.section(SECTIONS.HERO);const featuresHelper = aether.section(SECTIONS.FEATURES);---
<section {...heroHelper.section()}> <h1 {...heroHelper.field('title')}> {hero.data.title} </h1></section>
<section {...featuresHelper.section()}> <h2 {...featuresHelper.field('heading')}> {features.data.heading} </h2></section>Keeping Types in Sync
Section titled “Keeping Types in Sync”When you add or modify sections in the CMS:
# Sync latest schema and regenerate typesnpx aether-sdk sync && npx aether-sdk types
# Restart dev servernpm run devYour IDE instantly knows about:
- New sections you created
- New fields you added
- Changed field types
Component Patterns
Section titled “Component Patterns”Layout Component
Section titled “Layout Component”---interface Props { title?: string;}
const { title = 'My Site' } = Astro.props;---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title></head><body> <slot />
<!-- No SDK initialization needed! --> <!-- The Astro integration handles it automatically --></body></html>Reusable Section Component with Types
Section titled “Reusable Section Component with Types”---import { fetch, aether } from '@aether-official/sites-sdk/astro';import { SECTIONS } from '../aether-types';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(import.meta.env);const hero = await fetch.section(SECTIONS.HERO, config);const helper = aether.section(SECTIONS.HERO);---
<section class="hero" {...helper.section()}> <h1 {...helper.field('title')}> {hero.data.title} </h1> <p {...helper.field('subtitle')}> {hero.data.subtitle} </p></section>
<style> .hero { padding: 4rem 2rem; text-align: center; }</style>Repeater Fields
Section titled “Repeater Fields”Team Members Example with Helper API
Section titled “Team Members Example with Helper API”---import { fetch, aether } from '@aether-official/sites-sdk/astro';import { SECTIONS } from '../aether-types';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(import.meta.env);const team = await fetch.section(SECTIONS.TEAM, config);const helper = aether.section(SECTIONS.TEAM);---
<section {...helper.section()}> <h2 {...helper.field('heading')}> {team.data.heading} </h2>
<div class="team-grid" {...helper.repeater('members', team.data.members)}> {team.data.members?.map((member, i) => ( <div class="team-member" {...helper.repeaterItem(i)}> <h3 {...helper.field(`members.${i}.name`)}> {member.name} </h3> <p {...helper.field(`members.${i}.role`)}> {member.role} </p> </div> ))} </div></section>
<style> .team-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; }</style>Global Site Settings
Section titled “Global Site Settings”Global settings allow you to manage site-wide information like contact details, business hours, social media links, SEO defaults, and analytics configuration. These settings are managed in the visual editor and can be accessed in any Astro component.
Available Global Settings
Section titled “Available Global Settings”- Contact Information: Phone, email, address, toll-free, fax
- Business Hours: Timezone, weekly schedule, special hours
- Social Media: Facebook, Twitter, LinkedIn, Instagram, YouTube, TikTok, Pinterest, GitHub
- SEO Defaults: Default title, description, keywords, OG image
- Analytics: Google Analytics, Tag Manager, Facebook Pixel, Hotjar, Plausible
Fetching Global Settings
Section titled “Fetching Global Settings”---import { getGlobalSettings } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
// SECURITY: Use process.env to keep API token secureconst config = getConfig(process.env as Record<string, string | undefined>);
// Fetch global settings for the siteconst globalSettings = await getGlobalSettings( config.apiToken, config.editorOrigin);---
{globalSettings && ( <footer> <p>Contact: {globalSettings.contact?.email}</p> <p>{globalSettings.contact?.phone}</p> </footer>)}Contact Information Example
Section titled “Contact Information Example”Display contact information in your site footer or contact page:
---import { getGlobalSettings, formatAddress } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);const contact = settings?.contact;---
<footer class="site-footer"> <div class="contact-info"> <h3>Contact Us</h3>
{contact?.email && ( <p> <strong>Email:</strong> <a href={`mailto:${contact.email}`}>{contact.email}</a> </p> )}
{contact?.phone && ( <p> <strong>Phone:</strong> <a href={`tel:${contact.phone}`}>{contact.phone}</a> </p> )}
{contact?.tollFree && ( <p> <strong>Toll Free:</strong> <a href={`tel:${contact.tollFree}`}>{contact.tollFree}</a> </p> )}
{contact?.address && ( <p> <strong>Address:</strong><br /> {formatAddress(contact.address)} </p> )} </div></footer>
<style> .site-footer { background: #f5f5f5; padding: 2rem; margin-top: 4rem; }
.contact-info a { color: #0066cc; text-decoration: none; }</style>Business Hours Widget
Section titled “Business Hours Widget”Create a dynamic business hours widget that shows current status:
---import { getGlobalSettings, formatBusinessHours, getCurrentStatus, getNextOpening} from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);const businessHours = settings?.businessHours;
const isOpen = businessHours ? getCurrentStatus(businessHours) === 'open' : false;const nextOpening = businessHours && !isOpen ? getNextOpening(businessHours) : null;---
{businessHours && ( <div class="business-hours"> <div class={`status ${isOpen ? 'open' : 'closed'}`}> {isOpen ? '🟢 Open Now' : '🔴 Closed'} </div>
{!isOpen && nextOpening && ( <p class="next-opening">{nextOpening}</p> )}
<h4>Hours of Operation</h4> <ul class="schedule"> {(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const).map(day => ( <li> <span class="day">{day.charAt(0).toUpperCase() + day.slice(1)}:</span> <span class="hours"> {formatBusinessHours(businessHours.schedule, day)} </span> </li> ))} </ul>
{businessHours.specialHours && businessHours.specialHours.length > 0 && ( <div class="special-hours"> <h4>Special Hours</h4> <ul> {businessHours.specialHours.map(special => ( <li> <strong>{special.date}:</strong> {special.note} {special.open && special.close && ` (${special.open} - ${special.close})`} </li> ))} </ul> </div> )} </div>)}
<style> .business-hours { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.status { font-weight: bold; font-size: 1.25rem; margin-bottom: 0.5rem; }
.status.open { color: #22c55e; } .status.closed { color: #ef4444; }
.next-opening { color: #64748b; font-size: 0.875rem; margin-bottom: 1rem; }
.schedule { list-style: none; padding: 0; }
.schedule li { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #e5e7eb; }
.day { font-weight: 500; }
.hours { color: #64748b; }</style>Social Media Links
Section titled “Social Media Links”Display social media icons with links from global settings:
---import { getGlobalSettings } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);const social = settings?.socialMedia;
// Social media platforms with icons (using emoji or your icon library)const platforms = [ { key: 'facebook', label: 'Facebook', icon: '📘' }, { key: 'twitter', label: 'Twitter/X', icon: '🐦' }, { key: 'linkedin', label: 'LinkedIn', icon: '💼' }, { key: 'instagram', label: 'Instagram', icon: '📸' }, { key: 'youtube', label: 'YouTube', icon: '🎥' }, { key: 'tiktok', label: 'TikTok', icon: '🎵' }, { key: 'pinterest', label: 'Pinterest', icon: '📌' }, { key: 'github', label: 'GitHub', icon: '⚙️' },] as const;---
{social && ( <div class="social-links"> <h3>Follow Us</h3> <div class="social-icons"> {platforms.map(platform => { const url = social[platform.key]; return url && ( <a href={url} target="_blank" rel="noopener noreferrer" aria-label={platform.label} class="social-icon" > <span class="icon">{platform.icon}</span> <span class="label">{platform.label}</span> </a> ); })}
{social.custom?.map(custom => ( <a href={custom.url} target="_blank" rel="noopener noreferrer" class="social-icon" > {custom.icon && <span class="icon">{custom.icon}</span>} <span class="label">{custom.name}</span> </a> ))} </div> </div>)}
<style> .social-links { margin: 2rem 0; }
.social-icons { display: flex; gap: 1rem; flex-wrap: wrap; }
.social-icon { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background: #f5f5f5; border-radius: 8px; text-decoration: none; color: #333; transition: background 0.2s; }
.social-icon:hover { background: #e5e5e5; }
.icon { font-size: 1.5rem; }
.label { font-size: 0.875rem; }</style>SEO Meta Tags
Section titled “SEO Meta Tags”Use global SEO settings as defaults for your pages:
---import { getGlobalSettings } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
interface Props { title?: string; description?: string; keywords?: string[]; ogImage?: string;}
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);const seo = settings?.seo;
// Merge page-specific props with global defaultsconst { title = seo?.defaultTitle || 'My Site', description = seo?.defaultDescription || '', keywords = seo?.defaultKeywords || [], ogImage = seo?.ogImage} = Astro.props;---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO Meta Tags --> <title>{title}</title> {description && <meta name="description" content={description} />} {keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
<!-- Open Graph --> <meta property="og:title" content={title} /> {description && <meta property="og:description" content={description} />} {ogImage && <meta property="og:image" content={ogImage} />}
<!-- Twitter Card --> {seo?.twitterCard && <meta name="twitter:card" content={seo.twitterCard} />} <meta name="twitter:title" content={title} /> {description && <meta name="twitter:description" content={description} />}
<!-- Canonical URL --> {seo?.canonicalUrl && <link rel="canonical" href={seo.canonicalUrl} />}</head><body> <slot /></body></html>Analytics Integration
Section titled “Analytics Integration”Inject analytics scripts from global settings:
---import { getGlobalSettings } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);const analytics = settings?.analytics;---
{analytics?.googleAnalyticsId && ( <> <script async src={`https://www.googletagmanager.com/gtag/js?id=${analytics.googleAnalyticsId}`}></script> <script is:inline define:vars={{ gaId: analytics.googleAnalyticsId }}> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', gaId); </script> </>)}
{analytics?.googleTagManagerId && ( <> <script is:inline define:vars={{ gtmId: analytics.googleTagManagerId }}> (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer',gtmId); </script> <noscript> <iframe src={`https://www.googletagmanager.com/ns.html?id=${analytics.googleTagManagerId}`} height="0" width="0" style="display:none;visibility:hidden" ></iframe> </noscript> </>)}
{analytics?.facebookPixelId && ( <script is:inline define:vars={{ pixelId: analytics.facebookPixelId }}> !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', pixelId); fbq('track', 'PageView'); </script>)}
{analytics?.plausibleDomain && ( <script defer data-domain={analytics.plausibleDomain} src="https://plausible.io/js/script.js" ></script>)}
{analytics?.custom?.map(custom => ( custom.scriptUrl && ( <script async src={custom.scriptUrl} data-tracking-id={custom.trackingId} ></script> )))}Complete Footer Example
Section titled “Complete Footer Example”Combine contact, business hours, and social media in a comprehensive footer:
---import { getGlobalSettings, formatAddress, formatBusinessHours, getCurrentStatus} from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);
const isOpen = settings?.businessHours ? getCurrentStatus(settings.businessHours) === 'open' : false;---
<footer class="site-footer"> <div class="footer-grid"> <!-- Contact Section --> {settings?.contact && ( <div class="footer-section"> <h3>Contact</h3> {settings.contact.email && ( <p><a href={`mailto:${settings.contact.email}`}>{settings.contact.email}</a></p> )} {settings.contact.phone && ( <p><a href={`tel:${settings.contact.phone}`}>{settings.contact.phone}</a></p> )} {settings.contact.address && ( <address>{formatAddress(settings.contact.address)}</address> )} </div> )}
<!-- Business Hours Section --> {settings?.businessHours && ( <div class="footer-section"> <h3>Hours</h3> <p class={`status ${isOpen ? 'open' : 'closed'}`}> {isOpen ? 'Open Now' : 'Closed'} </p> <p> Today: {formatBusinessHours( settings.businessHours.schedule, new Date().toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase() as any )} </p> </div> )}
<!-- Social Media Section --> {settings?.socialMedia && ( <div class="footer-section"> <h3>Follow Us</h3> <div class="social-links"> {settings.socialMedia.facebook && ( <a href={settings.socialMedia.facebook} target="_blank">Facebook</a> )} {settings.socialMedia.twitter && ( <a href={settings.socialMedia.twitter} target="_blank">Twitter</a> )} {settings.socialMedia.linkedin && ( <a href={settings.socialMedia.linkedin} target="_blank">LinkedIn</a> )} {settings.socialMedia.instagram && ( <a href={settings.socialMedia.instagram} target="_blank">Instagram</a> )} </div> </div> )} </div>
<div class="footer-bottom"> <p>© {new Date().getFullYear()} All rights reserved.</p> </div></footer>
<style> .site-footer { background: #1f2937; color: #f9fafb; padding: 3rem 2rem 1rem; margin-top: 4rem; }
.footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; max-width: 1200px; margin: 0 auto 2rem; }
.footer-section h3 { color: #fff; margin-bottom: 1rem; }
.footer-section a { color: #93c5fd; text-decoration: none; }
.footer-section a:hover { color: #60a5fa; }
.status.open { color: #34d399; } .status.closed { color: #f87171; }
.social-links { display: flex; flex-direction: column; gap: 0.5rem; }
.footer-bottom { text-align: center; padding-top: 2rem; border-top: 1px solid #374151; color: #9ca3af; }</style>TypeScript Support
Section titled “TypeScript Support”Global settings are fully typed. Import types for custom components:
import type { ContactSettings, GlobalSettings } from '@aether-official/sites-sdk/global-settings';
interface Props { contact: ContactSettings;}
const { contact } = Astro.props;// TypeScript knows all available fields: phone, email, address, etc.Handling Missing Settings
Section titled “Handling Missing Settings”Always check if global settings exist before using them:
---import { getGlobalSettings } from '@aether-official/sites-sdk/global-settings';import { getConfig } from '@aether-official/sites-sdk/astro';
const config = getConfig(process.env as Record<string, string | undefined>);const settings = await getGlobalSettings(config.apiToken, config.editorOrigin);
// Safe access with optional chainingconst email = settings?.contact?.email;const phone = settings?.contact?.phone;---
{settings ? ( <footer> {email && <a href={`mailto:${email}`}>{email}</a>} {phone && <a href={`tel:${phone}`}>{phone}</a>} </footer>) : ( <footer> <p>Contact information will be displayed here.</p> </footer>)}Dynamic Routes
Section titled “Dynamic Routes”Content-Driven Pages
Section titled “Content-Driven Pages”---import { getAetherPage, getConfig } from '@aether-official/sites-sdk/astro';
export async function getStaticPaths() { const config = getConfig(import.meta.env);
// Fetch all blog posts from Aether // This would typically use a custom endpoint const posts = await fetch(`${config.apiUrl}/blog/posts`).then(r => r.json());
return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const config = getConfig(import.meta.env);const content = await getAetherSection(post.sectionId, config);---
<article data-aether-section={content.id}> <h1 data-aether-field="title">{content.data.title}</h1> <div data-aether-field="body"> {content.data.body} </div></article>Build Configuration
Section titled “Build Configuration”Optimize for Static Sites
Section titled “Optimize for Static Sites”import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'static', // or 'hybrid' for SSR build: { inlineStylesheets: 'auto', }, vite: { optimizeDeps: { exclude: ['@aether-official/sites-sdk'], // Let Astro handle bundling }, },});Deployment
Section titled “Deployment”Vercel
Section titled “Vercel”{ "buildCommand": "npm run build", "outputDirectory": "dist", "framework": "astro", "env": { "AETHER_API_URL": "@aether-api-url", "AETHER_API_TOKEN": "@aether-api-token" }}Environment variables in Vercel dashboard:
PUBLIC_AETHER_SITE_IDPUBLIC_AETHER_ORG_IDPUBLIC_AETHER_EDITOR_URLAETHER_API_URLAETHER_API_TOKEN
Netlify
Section titled “Netlify”[build] command = "npm run build" publish = "dist"
[[plugins]] package = "@netlify/plugin-sitemap"GitHub Pages
Section titled “GitHub Pages”name: Deploy to GitHub Pages
on: push: branches: [main] workflow_dispatch:
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run build env: AETHER_API_URL: ${{ secrets.AETHER_API_URL }} AETHER_API_TOKEN: ${{ secrets.AETHER_API_TOKEN }} - uses: actions/upload-pages-artifact@v2 with: path: ./dist
deploy: needs: build runs-on: ubuntu-latest permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/deploy-pages@v2 id: deploymentTesting
Section titled “Testing”Local Development
Section titled “Local Development”# Start dev servernpm run dev
# Test in visual editor# 1. Run dev server (localhost:4321)# 2. Open Aether app# 3. Navigate to Sites → Your Site → Open Editor# 4. Editor loads http://localhost:4321?aether-editor=truePreview Builds
Section titled “Preview Builds”# Build and previewnpm run buildnpm run previewTroubleshooting
Section titled “Troubleshooting”SDK Not Loading
Section titled “SDK Not Loading”Problem: SDK doesn’t initialize in editor mode
Solution:
- Check URL has
?aether-editor=true - Verify SDK script is in
<script>tag (not in frontmatter) - Look for console errors
- Ensure PUBLIC_ env vars are set
Content Not Fetching
Section titled “Content Not Fetching”Problem: getAetherSection() fails
Solution:
- Check
AETHER_API_URLandAETHER_API_TOKENenv vars - Verify section ID exists in database
- Check network tab for API errors
- Use try/catch for better error messages
Build Errors
Section titled “Build Errors”Problem: Build fails with SDK import errors
Solution:
export default defineConfig({ vite: { ssr: { noExternal: ['@aether-official/sites-sdk'], }, },});Next Steps
Section titled “Next Steps”- Visual Editor - Enable inline editing
- API Reference - Complete API docs
- Example Project - Full Astro starter