Back to Guides

Developer Guide

A framework-like CMS that makes content management effortless for your clients.

šŸŽØ Visual Editor (NEW!)

Edit content directly on the live website! Click any element to edit it

✨ Click-to-Edit

Hover and click elements directly on your website

⚔ Instant Preview

See changes as you type, no reload needed

šŸš€ Zero Setup

Works automatically with existing SDK

šŸŽÆ Default View

Opens automatically when accessing a project

šŸš€ Quick Start

1

Get Your Credentials

Create a project in the dashboard to get your API Key and Project ID.

2

Add SDK to Your HTML (Optimized)

<!DOCTYPE html>
<html>
  <head>
    <title>My Site</title>
    <!-- āœ… Load SDK in head with defer for best performance -->
    <script src="https://spaceblock.vercel.app/spaceblock-sdk.js" defer></script>
  </head>
  <body>
    <div id="root"></div>
    
    <!-- Your app scripts -->
    <script type="module" src="/src/main.tsx"></script>
    
    <!-- Initialize SDK -->
    <script>
      window.addEventListener('DOMContentLoaded', function() {
        if (window.SpaceBlock) {
          SpaceBlock.init({
            apiKey: 'YOUR_API_KEY',
            projectId: 'YOUR_PROJECT_ID',
            autoLoad: true,  // Automatic timing!
            debug: true      // Enable for development
          });
        }
      });
    </script>
  </body>
</html>
3

Mark Your Content

<h1 data-cms-id="hero-title">Welcome to My Site</h1>
<p data-cms-id="hero-subtitle">Editable content</p>

That's it! Your client can now edit this content from the dashboard using the Visual Editor!

4

Enable Visual Editor

Go to Project Settings and add your website domain:

http://localhost:3000  (for development)
or
https://your-site.com  (for production)

Done! The Visual Editor will now load your site and make all elements clickable.

Visual Editor - How It Works

Automatic Detection

The SDK automatically detects when it's running inside the CMS iframe and enables visual editing features. No configuration needed!

React Compatible

Uses MutationObserver to handle React re-renders. Elements stay clickable even after your app updates.

Performance Optimized

Visual Editor loads in 1-2 seconds with these optimizations:

  • SDK loads with defer attribute
  • Images use loading="lazy"
  • Routes use code splitting with React.lazy()
  • Preconnect hints for faster CMS communication

Client Site Performance Tips

āœ… Load SDK in <head> with defer

āœ… Add loading="lazy" to all images

āœ… Use React 18 createRoot

āœ… Implement code splitting for routes

āœ… Add preconnect links for CMS domain

šŸ—‚ļø Automatic Template Detection

The Problem

The CMS SDK detects insertable templates by scanning the current page's DOM for elements with data-cms-insertable attributes. Templates are only detected when their components are rendered on the page being viewed.

The Solution: TemplateRegistry

Add a hidden TemplateRegistry component to your Layout that registers all available templates on every page:

// components/TemplateRegistry.jsx
function TemplateRegistry() {
  // Only render when inside CMS iframe (visual editor)
  const isInCmsIframe = typeof window !== 'undefined' && window.self !== window.top;

  if (!isInCmsIframe) return null;

  return (
    <div
      aria-hidden="true"
      style={{
        position: 'absolute',
        width: 1,
        height: 1,
        padding: 0,
        margin: -1,
        overflow: 'hidden',
        clip: 'rect(0, 0, 0, 0)',
        whiteSpace: 'nowrap',
        border: 0,
      }}
    >
      {/* Register all insertable templates here */}
      <div data-cms-insertable="hero-section" />
      <div data-cms-insertable="features-section" />
      <div data-cms-insertable="contact-section" />
      <div data-cms-insertable="collection-block" />
    </div>
  );
}

Add to Your Layout

// components/Layout.jsx
import TemplateRegistry from './TemplateRegistry';

export function Layout({ children }) {
  return (
    <div>
      <Header />
      <main>{children}</main>
      <Footer />
      <TemplateRegistry />
    </div>
  );
}

How It Works

  • āœ… Conditional Rendering: Only renders inside CMS iframe - regular users never see it
  • āœ… Visually Hidden: Uses CSS to hide while keeping in DOM for SDK detection
  • āœ… Screen Reader Safe: aria-hidden="true" ensures accessibility

Adding New Templates

  1. 1. Create component with data-cms-insertable="template-name"
  2. 2. Add matching <div data-cms-insertable="template-name" /> to TemplateRegistry
  3. 3. Add rendering logic in your dynamic page handler

šŸ·ļø Attribute System

data-cms-id(Required)

Unique identifier for the content block.

<h1 data-cms-id="hero-title">My Title</h1>

data-cms-page(Optional)

Groups content by page. Default: "home"

<!-- Home page -->
<h1 data-cms-id="hero-title" data-cms-page="home">Home</h1>

<!-- About page -->
<h1 data-cms-id="hero-title" data-cms-page="about">About</h1>

data-cms-section(Optional)

Groups content into sections. Auto-inferred from cms-id if not specified.

<div data-cms-section="hero">
  <h1 data-cms-id="hero-title">Title</h1>
  <p data-cms-id="hero-subtitle">Subtitle</p>
</div>

data-cms-component(Optional)

Groups related elements into components.

<div data-cms-component="testimonial-card-1">
  <p data-cms-id="testimonial-1-text" data-cms-component="testimonial-card-1">
    Great product!
  </p>
  <h4 data-cms-id="testimonial-1-author" data-cms-component="testimonial-card-1">
    John Doe
  </h4>
</div>

data-cms-order(Optional)

Manually control display order. Default: auto-ordered by DOM position.

<h1 data-cms-id="hero-title" data-cms-order="1">Title</h1>
<footer data-cms-id="footer-text" data-cms-order="999">Footer</footer>

🌳 Tree Structure

Content is organized in a hierarchical tree view:

šŸ“ Home (Page)
ā”œā”€ šŸ“‚ Hero (Section)
│ ā”œā”€ šŸ“¦ Header (Component)
│ │ ā”œā”€ šŸ“„ hero-title
│ │ └─ šŸ“„ hero-subtitle
│ └─ šŸ“¦ CTA (Component)
│ └─ šŸ“„ cta-button
ā”œā”€ šŸ“‚ Features (Section)
│ ā”œā”€ šŸ“„ features-title
│ ā”œā”€ šŸ“„ feature-1
│ └─ šŸ“„ feature-2
└─ šŸ“‚ Footer (Section)
└─ šŸ“„ footer-copyright
šŸ“ About (Page)
└─ šŸ“‚ Team (Section)
└─ šŸ“„ team-title

šŸ“‹ Complete Example

<!DOCTYPE html>
<html lang="en">
<head>
  <title>My Landing Page</title>
</head>
<body>
  
  <!-- Hero Section -->
  <header data-cms-section="hero">
    <h1 data-cms-id="hero-title" data-cms-section="hero">
      Welcome to Our Product
    </h1>
    <p data-cms-id="hero-subtitle" data-cms-section="hero">
      The best solution for your needs
    </p>
    <a href="#" data-cms-id="hero-cta" data-cms-section="hero">
      Get Started
    </a>
  </header>

  <!-- Features Section -->
  <section data-cms-section="features">
    <h2 data-cms-id="features-title">Why Choose Us?</h2>
    
    <div data-cms-component="feature-1">
      <h3 data-cms-id="feature-1-title" data-cms-component="feature-1">
        Fast Performance
      </h3>
      <p data-cms-id="feature-1-description" data-cms-component="feature-1">
        Lightning-fast load times
      </p>
    </div>

    <div data-cms-component="feature-2">
      <h3 data-cms-id="feature-2-title" data-cms-component="feature-2">
        Easy to Use
      </h3>
      <p data-cms-id="feature-2-description" data-cms-component="feature-2">
        Intuitive interface
      </p>
    </div>
  </section>

  <!-- Footer -->
  <footer data-cms-section="footer">
    <p data-cms-id="footer-copyright">Ā© 2025 My Company</p>
  </footer>

  <!-- SpaceBlock SDK -->
  <script src="https://spaceblock.vercel.app/spaceblock-sdk.js"></script>
  <script>
    SpaceBlock.init({
      apiKey: 'YOUR_API_KEY',
      projectId: 'YOUR_PROJECT_ID',
      apiUrl: 'https://spaceblock.vercel.app/api/public/content',
      autoDetect: true
    });
    
    window.addEventListener('load', () => {
      SpaceBlock.load();
    });
  </script>
</body>
</html>

✨ Best Practices

āœ“

Use semantic HTML structure

Organize your HTML in logical reading order (top to bottom)

āœ“

Consistent naming patterns

Use kebab-case: feature-1-title, not feat1 or Feature1Title

āœ“

Group related content with components

Use data-cms-component for cards, testimonials, etc.

āœ“

Provide meaningful default content

Avoid placeholders like [Title Goes Here]

āœ“

Use images and links

Just add data-cms-id to <img> and <a> tags - auto-detected!

āœ“

Built-in optimization

Smart caching and zero layout shift - no flickering!

⚔ Performance & Optimization

SpaceBlock offers three levels of optimization for different deployment scenarios.

Level 1

Runtime Caching (Automatic)

Perfect for development and quick deployments. Content is cached in localStorage after first load.

// Enabled by default - zero config!
SpaceBlock.init({
  apiKey: 'YOUR_API_KEY',
  projectId: 'YOUR_PROJECT_ID',
  autoDetect: true,
  cacheEnabled: true,      // Default: true
  hideUntilLoaded: true    // Default: true (prevents flash)
});
First visit: ~450msReturn visit: ~155ms āœ…
Level 2 - RECOMMENDED

Build-Time Inlining (Production)

3x faster first load! Content is embedded in HTML at build time. Zero network latency, zero flash.

Step 1: Download the build script

curl -O https://spaceblock.vercel.app/spaceblock-build.js

Step 2: Run before deploying

node spaceblock-build.js \
  --apiKey YOUR_API_KEY \
  --input index.html \
  --output dist/index.html

Step 3: Integrate with your build process

// package.json
{
  "scripts": {
    "prebuild": "node spaceblock-build.js --apiKey $API_KEY --input src/index.html --output dist/index.html",
    "build": "your-build-command"
  }
}

How it works:

  • • Fetches content from CMS API at build time
  • • Embeds JSON in HTML as <script type="application/json">
  • • SDK detects inline data and applies instantly (0ms)
  • • Still fetches fresh content in background for cache update
First visit: ~155ms āœ…āœ…āœ…Return visit: ~155ms āœ…āœ…āœ…
Level 3

Hybrid (Best of Both Worlds)

Use build-time inlining + set up webhooks to trigger rebuilds when content changes. Perfect for high-traffic sites.

Setup: Same as Level 2, but configure your CI/CD (Vercel, Netlify, GitHub Actions) to rebuild when content changes. Content stays fresh + loads instantly!

šŸ“Š Quick Comparison

ApproachFirst VisitReturn VisitBest For
Runtime Caching450ms155ms āœ…Development, Testing
Build-Time Inline155ms āœ…āœ…āœ…155ms āœ…āœ…āœ…Production Sites
Hybrid155ms āœ…āœ…āœ…155ms āœ…āœ…āœ…High-Traffic Sites

šŸ”§ Development Tools

Clear Cache:

SpaceBlock.clearCache();
// or
localStorage.removeItem('spaceblock_cache_YOUR_PROJECT_ID');

Test Inline Content:

// In browser console
const inline = document.getElementById('spaceblock-data');
console.log(inline ? 'āœ… Inline data found!' : 'āŒ No inline data');

Blog Posts API

SpaceBlock includes a full-featured blog system. Your clients can create, edit, and publish blog posts from the dashboard. You can fetch these posts via the public API.

List All Published Posts

GET /api/public/blog?apiKey=YOUR_API_KEY

// Optional query parameters:
// ?page=1&limit=10  - Pagination
// ?tag=news         - Filter by tag

Response:

{
  "posts": [
    {
      "id": "abc123",
      "title": "My First Post",
      "slug": "my-first-post",
      "excerpt": "A short summary...",
      "featuredImage": "https://...",
      "author": "John Doe",
      "tags": ["news", "updates"],
      "publishedAt": "2025-01-15T10:00:00Z",
      "createdAt": "2025-01-14T08:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 25,
    "totalPages": 3
  }
}

Get Single Post by Slug

GET /api/public/blog/my-first-post?apiKey=YOUR_API_KEY

Response (Block-Based Content):

{
  "post": {
    "id": "abc123",
    "title": "My First Post",
    "slug": "my-first-post",
    "excerpt": "A short summary...",
    "featuredImage": "https://...",
    "author": "John Doe",
    "tags": ["news", "updates"],
    "content": [
      { "id": "b1", "type": "text", "content": "Intro paragraph..." },
      { "id": "b2", "type": "heading", "content": "Section Title", "level": 2 },
      { "id": "b3", "type": "image", "content": "https://...", "size": "large" },
      { "id": "b4", "type": "quote", "content": "A highlighted quote" },
      { "id": "b5", "type": "subblog", "content": "Related", "articles": [...] }
    ],
    "publishedAt": "2025-01-15T10:00:00Z"
  }
}

Content Block Types

text

Plain paragraph text

heading

Section heading with level: 2 (H2) or 3 (H3)

image

Image with size: full (100%), large (75%), medium (50%), small (25%)

quote

Highlighted quote or callout

subblog NEW

Carousel of related articles with articles array

Each article: id, title, excerpt, image, link, variant (default/minimal/card/overlay/featured)

React Integration Example

// pages/blog/[slug].tsx - Render block-based content
const sizeWidths = { full: '100%', large: '75%', medium: '50%', small: '25%' };

function renderBlock(block) {
  switch (block.type) {
    case 'text': return <p key={block.id}>{block.content}</p>;
    case 'heading':
      return block.level === 3
        ? <h3 key={block.id}>{block.content}</h3>
        : <h2 key={block.id}>{block.content}</h2>;
    case 'image':
      return <img key={block.id} src={block.content}
        style={{ width: sizeWidths[block.size || 'full'] }} />;
    case 'quote':
      return <blockquote key={block.id}>{block.content}</blockquote>;
    case 'subblog':
      return (
        <div key={block.id} className="carousel">
          {block.content && <h3>{block.content}</h3>}
          <div className="carousel-items">
            {block.articles?.map(a => (
              <div key={a.id} className={`card card--${a.variant}`}>
                {a.image && <img src={a.image} alt={a.title} />}
                <h4>{a.title}</h4>
                <p>{a.excerpt}</p>
                {a.link && <a href={a.link}>Read more</a>}
              </div>
            ))}
          </div>
        </div>
      );
    default: return null;
  }
}

export default function BlogPostPage({ params }) {
  const [post, setPost] = useState(null);

  useEffect(() => {
    fetch(`/api/public/blog/${params.slug}?apiKey=YOUR_API_KEY`)
      .then(res => res.json())
      .then(data => setPost(data.post));
  }, [params.slug]);

  if (!post) return <div>Loading...</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {post.featuredImage && <img src={post.featuredImage} />}
      <div className="content">
        {post.content.map(block => renderBlock(block))}
      </div>
    </article>
  );
}

API Key Authentication

Pass your API key in one of two ways:

  • • Query parameter: ?apiKey=YOUR_KEY
  • • Header: x-api-key: YOUR_KEY

Collections NEW

Collections let you manage repeatable content like team members, products, portfolio items, or any structured data. Create collections in your dashboard, add items with images and detailed content, then manage everything from a clean interface.

How to Use Collections

  1. 1.Go to your project and click the Collections tab
  2. 2.Click New Collection and enter a name (e.g., "Team Members")
  3. 3.Click Add Item to create collection items with:
  4. Title: Name of the item (e.g., "Sarah Chen")
    Image URL: Featured image for the item
    Description: Brief subtitle (e.g., "CEO & Founder")
    Full Content: Detailed long-form content
  5. 4.Click on any item to view its detail page and edit inline

Common Use Cases

šŸ‘„Team Members

Photos, names, roles, detailed bios

šŸ“¦Products

Images, names, descriptions, features

šŸ’¼Portfolio Items

Screenshots, project names, details

šŸ’¬Testimonials

Avatars, names, companies, quotes

Key Features

  • āœ… Clean Interface: Simple, focused content management
  • āœ… Auto-generated slugs: URL-friendly slugs created from titles
  • āœ… Inline editing: Edit items directly on detail pages
  • āœ… Built-in ordering: Items maintain order with drag handles
  • āœ… Clickable items: Navigate to detailed view for each item
  • āœ… Image previews: See images while editing

Collections API

Fetch your collection data programmatically using the public API. Perfect for rendering dynamic content like team members, products, or portfolio items on your website.

List All Items in a Collection

GET /api/public/project/{projectId}/collections/{collectionName}?apiKey=YOUR_API_KEY

// collectionName can be the slug or name (case-insensitive)

Response:

{
  "collection": {
    "id": "coll123",
    "name": "Team Members",
    "slug": "team-members",
    "description": "Our amazing team",
    "schema": [...]
  },
  "items": [
    {
      "id": "item1",
      "title": "Sarah Chen",
      "slug": "sarah-chen",
      "image": "https://...",
      "description": "CEO & Founder",
      "tags": ["leadership", "founder"],
      "data": { "contentBlocks": [...] },
      "order": 0,
      "updatedAt": "2025-01-15T10:00:00Z"
    }
  ]
}

Get Single Collection Item

GET /api/public/project/{projectId}/collections/{collectionName}/{itemSlug}?apiKey=YOUR_API_KEY

Response (with Content Blocks):

{
  "id": "item1",
  "title": "Sarah Chen",
  "slug": "sarah-chen",
  "image": "https://...",
  "description": "CEO & Founder",
  "tags": ["leadership", "founder"],
  "contentBlocks": [
    { "id": "b1", "type": "text", "content": "Sarah founded the company in 2020..." },
    { "id": "b2", "type": "heading", "content": "Background", "level": 2 },
    { "id": "b3", "type": "text", "content": "With over 15 years of experience..." },
    { "id": "b4", "type": "image", "url": "https://...", "alt": "Team photo", "size": "large" },
    { "id": "b5", "type": "quote", "content": "Innovation starts with people", "author": "Sarah Chen" },
    { "id": "b6", "type": "divider" }
  ],
  "data": { ... },
  "order": 0,
  "updatedAt": "2025-01-15T10:00:00Z",
  "collection": {
    "id": "coll123",
    "name": "Team Members",
    "slug": "team-members",
    "description": "Our amazing team"
  }
}

Content Block Types

text

Plain paragraph text. Properties: content

heading

Section heading with level: 2 (H2) or 3 (H3)

image

Image with url, alt, and size: full, large, medium, small

quote

Highlighted quote with optional author attribution

divider

Horizontal separator line

Item Tags

Collection items support tags for categorization and filtering. Tags are returned as an array of strings.

// Filter items by tag in your frontend
const items = data.items;
const leadershipItems = items.filter(item =>
  item.tags?.includes("leadership")
);

// Render tags as badges
{item.tags?.map(tag => (
  <span key={tag} className="tag">{tag}</span>
))}

Tags can be added/edited in the CMS collection item editor.

React Integration Example

// components/CollectionItemDetail.tsx
const sizeWidths = { full: '100%', large: '75%', medium: '50%', small: '25%' };

function renderContentBlock(block) {
  switch (block.type) {
    case 'text':
      return <p key={block.id}>{block.content}</p>;
    case 'heading':
      return block.level === 3
        ? <h3 key={block.id}>{block.content}</h3>
        : <h2 key={block.id}>{block.content}</h2>;
    case 'image':
      return (
        <img
          key={block.id}
          src={block.url}
          alt={block.alt || ''}
          style={{ width: sizeWidths[block.size || 'full'] }}
        />
      );
    case 'quote':
      return (
        <blockquote key={block.id}>
          <p>{block.content}</p>
          {block.author && <cite>— {block.author}</cite>}
        </blockquote>
      );
    case 'divider':
      return <hr key={block.id} />;
    default:
      return null;
  }
}

export default function CollectionItemPage({ projectId, collectionName, itemSlug }) {
  const [item, setItem] = useState(null);

  useEffect(() => {
    fetch(`/api/public/project/${projectId}/collections/${collectionName}/${itemSlug}?apiKey=YOUR_API_KEY`)
      .then(res => res.json())
      .then(data => setItem(data));
  }, [projectId, collectionName, itemSlug]);

  if (!item) return <div>Loading...</div>;

  return (
    <article>
      <h1>{item.title}</h1>
      {item.image && <img src={item.image} alt={item.title} />}
      <p className="subtitle">{item.description}</p>
      <div className="content">
        {item.contentBlocks?.map(block => renderContentBlock(block))}
      </div>
    </article>
  );
}

API Key Authentication

Pass your API key in one of two ways:

  • • Query parameter: ?apiKey=YOUR_KEY
  • • Header: x-api-key: YOUR_KEY

Find your project ID and API key in your project dashboard settings.

Pages API NEW

Fetch pages created via the Page Builder. Use this API to build dynamic navigation, render custom pages, and manage page content.

List All Pages (with API Key)

GET /api/public/pages/{projectId}?apiKey=YOUR_API_KEY

Response:

{
  "pages": [
    {
      "id": "page-123",
      "name": "About Us",
      "slug": "about-us",
      "path": "/about-us",
      "published": true,
      "createdAt": "2026-01-15T10:00:00Z"
    },
    {
      "id": "page-456",
      "name": "Contact",
      "slug": "contact",
      "path": "/contact",
      "published": true,
      "createdAt": "2026-01-16T14:30:00Z"
    }
  ]
}

Returns all pages (published and unpublished). Requires API key validation.

List Published Pages (Public)

GET /api/public/pages/{projectId}/list

Response:

{
  "pages": [
    {
      "id": "page-123",
      "name": "About Us",
      "slug": "about-us",
      "path": "/about-us",
      "title": "About Our Company",
      "description": "Learn more about us",
      "createdAt": "2026-01-15T10:00:00Z"
    }
  ]
}

Returns only published pages. No API key required.

Get Page with Elements

GET /api/public/pages/{projectId}/{pageSlug}

Response:

{
  "page": {
    "id": "page-123",
    "name": "About Us",
    "slug": "about-us",
    "path": "/about-us",
    "title": "About Our Company",
    "published": true
  },
  "elements": [
    {
      "id": "el-1",
      "elementId": "hero-title",
      "type": "heading",
      "content": { "text": "Welcome" },
      "zone": "main",
      "order": 0
    }
  ]
}

React Integration Example

// Build dynamic navigation from CMS pages
function Navigation() {
  const [pages, setPages] = useState([])

  useEffect(() => {
    fetch(`/api/public/pages/${projectId}?apiKey=YOUR_API_KEY`)
      .then(res => res.json())
      .then(data => setPages(data.pages.filter(p => p.published)))
  }, [])

  return (
    <nav>
      <Link href="/">Home</Link>
      {pages.map(page => (
        <Link key={page.id} href={page.path}>{page.name}</Link>
      ))}
    </nav>
  )
}

API Key Authentication

Pass your API key in one of two ways:

  • • Query parameter: ?apiKey=YOUR_KEY
  • • Header: x-api-key: YOUR_KEY

šŸ–¼ļø Images & Links

Images (Auto-Detected)

Add data-cms-id to <img> tags:

<img
  data-cms-id="hero-image"
  src="hero.jpg"
  alt="Hero"
  style="object-fit: cover;"
/>

Dashboard features: Edit URL, Alt text, and Fit mode (cover, contain, fill, none, scale-down)

Links (Auto-Detected)

Add data-cms-id to <a> tags:

<a data-cms-id="cta-button" href="/signup">
  Get Started
</a>

<nav>
  <a data-cms-id="nav-home" href="/">Home</a>
  <a data-cms-id="nav-about" href="/about">About</a>
</nav>

Dashboard features: Edit URL (href attribute)