Everything you need to embed, author, and publish reusable web content blocks.
Cellpy is a CDN-powered block platform. A developer places one embed tag on a page once. After that, the marketing or design team assigns, swaps, and themes blocks from the dashboard — no code changes, no redeploys.
Install the package for your framework and drop the embed tag once.
Quick Start →Author blocks with Claude or VS Code, save them to Workspace, publish.
Marketplace →Add Cellpy to any site in under five minutes.
Cellpy uses the <cellpy-block> HTML custom element. A developer places it once; the block assigned to it can be swapped or updated from the dashboard without touching code.
Go to Containers → New container. Give it a slug (e.g. homepage-hero). A container is the permanent embed slot on your page — the block assigned to it can change any time.
Pick your framework below for a full guide, or use the CDN script for plain HTML:
# React / Vue / Svelte — install the framework wrapper
npm install @cellpy/react # or @cellpy/vue / @cellpy/angular / @cellpy/runtime (Svelte)
# Plain HTML — no install needed, just add a script tag<!-- Place this wherever the block should render.
The slug matches the container slug from your dashboard. -->
<cellpy-block slug="homepage-hero"></cellpy-block>In the dashboard, assign a saved block to your container and click Publish. The embed tag on your live page starts rendering it immediately — no redeploy needed.
Use the official React wrapper or the web component directly in JSX.
npm install @cellpy/reactThe CellpyBlock component renders your block inside an isolated Shadow DOM. Pass the container slug you set in the dashboard.
import { CellpyBlock } from '@cellpy/react'
export function MyPage() {
return (
<main>
<CellpyBlock slug="homepage-hero" />
{/* Override CSS variables for theming */}
<CellpyBlock
slug="cta-banner"
style={{ '--cellpy-primary': '#e11d48' } as React.CSSProperties}
/>
</main>
)
}React 19 handles custom elements natively. You can use the raw tag if you prefer:
// React 19+ — custom elements work without a wrapper
<cellpy-block slug="homepage-hero" />
// React 18 — declare the element type to avoid TS errors:
declare global {
namespace JSX {
interface IntrinsicElements {
'cellpy-block': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & { slug: string },
HTMLElement
>
}
}
}_document.tsx or layout.tsx.Use the official Vue plugin or the web component directly in templates.
npm install @cellpy/vue// main.ts
import { createApp } from 'vue'
import { CellpyPlugin } from '@cellpy/vue'
import App from './App.vue'
createApp(App).use(CellpyPlugin).mount('#app')<template>
<main>
<CellpyBlock slug="homepage-hero" />
<!-- Theme override via style binding -->
<CellpyBlock slug="cta-banner" :style="{ '--cellpy-primary': '#e11d48' }" />
</main>
</template>cellpy-block custom element without warnings.Embed blocks using the custom element with schema support.
npm install @cellpy/angular// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { CellpyModule } from '@cellpy/angular'
@NgModule({
imports: [CellpyModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// ── standalone components ──────────────────────
// app.config.ts
import { ApplicationConfig } from '@angular/core'
import { provideCellpy } from '@cellpy/angular'
export const appConfig: ApplicationConfig = {
providers: [provideCellpy()],
}<cellpy-block slug="homepage-hero"></cellpy-block>
<!-- Bind a dynamic slug -->
<cellpy-block [attr.slug]="blockSlug"></cellpy-block>Svelte handles web components natively — minimal setup required.
npm install @cellpy/runtime// src/routes/+layout.ts (SvelteKit)
import '@cellpy/runtime'<script>
let slug = 'homepage-hero'
</script>
<cellpy-block {slug} />
<!-- Reactive + themed -->
<cellpy-block slug={slug} style="--cellpy-primary: #e11d48" />cellpy-block.Drop a script tag and the custom element anywhere — no build step required.
Add this once in the <head> of your page or global template.
<script src="https://cdn.cellpy.com/runtime.js" defer></script><cellpy-block slug="homepage-hero"></cellpy-block>
<!-- Theme via CSS variables on the element or any ancestor -->
<cellpy-block slug="cta-banner" style="--cellpy-primary: #e11d48;"></cellpy-block>The runtime registers the <cellpy-block> custom element, fetches the latest published version from the CDN, and renders it inside an isolated Shadow DOM — no styles bleed in or out.
slug attribute matches your container slug from the dashboard — not the block slug. The container is the permanent embed slot; the assigned block can change any time without touching your HTML.Browse, preview, and install ready-made blocks from the Cellpy community.
The Marketplace is a library of pre-built blocks — hero sections, pricing cards, testimonial carousels, CTAs, and more. Install any block into your Workspace, customise it, and publish it to your containers.
Navigate to Marketplace in the dashboard sidebar. Filter by category or search by keyword. Click any card to preview the block live.
Click Add to Workspace on any listing. The block appears in your Workspace where you can review the HTML and CSS before saving.
Every Marketplace block exposes CSS custom properties for theming. Override them from your page without editing the block source:
<cellpy-block
slug="pricing-cards"
style="
--cellpy-primary: #6366f1;
--cellpy-font-heading: 'Inter', sans-serif;
--cellpy-radius: 12px;
"
></cellpy-block>Click Save Block in the Workspace. Then open your container, assign the block, and publish — the live page updates immediately.
Describe a block in plain language — Claude designs it and pushes it to your Workspace.
Once the MCP server is connected, you can create production-ready blocks in a conversation. Claude generates HTML and CSS — isolated in Shadow DOM — and pushes it straight to your Workspace for review.
Open Workspace in the dashboard. The block appears with status pending. Click it to preview with live dimensions and inspect the generated HTML and CSS.
Reply to Claude: "Make the card borders rounder and add a sparkle icon before each plan name." Claude pushes a revised version — the previous one stays in Workspace history.
When satisfied, click Save Block. Assign it to a container — the embed tag on your live site updates immediately.
--cellpy-primary, --cellpy-font) so your team can theme the block per-site without editing the source.Visual block authoring with live preview, then push to your Workspace.
Claude Design is a visual authoring environment built on top of Claude. It lets you create and edit Cellpy blocks with a live preview alongside the conversation — tweak layout, colors, and copy without leaving the tool.
Claude Design renders a live preview as it generates. Click elements in the preview and ask Claude to change specific parts — "make this heading larger", "softer background" — or edit the HTML and CSS directly in the code panel.
Claude Design cannot access your files. When a block needs an image, Claude inserts a stub placeholder instead of the real one.
Ask Claude to push it: "Push this block to my Cellpy Workspace." Swap stub images, click Save Block, then assign to a container to go live.
Every block is validated before it reaches your Workspace. Here are all the restrictions, requirements, and what each trust tier allows.
Cellpy validates every block pushed via the MCP server, VS Code Extension, or the Workspace UI against a set of safety rules called principles. Rules marked error block the push entirely; rules marked warning let the block through but surface a notice in Workspace. Your account's trust level determines which principles apply.
Community-submitted blocks. All 9 principles apply.
Established creators. P7 Marketplace content rules are skipped.
Internal / white-label use. P3–P7 are all relaxed.
| # | Principle | Enforcement | Applies to |
|---|---|---|---|
| P1 | Self-Contained CSS No body, html, *, :root, or :host-context selectors | error | All levels |
| P2 | No Script Execution No <script>, event handlers, or javascript: URIs | error | All levels |
| P3 | No Layout Escaping No position:fixed, viewport units, or top:0 + left:0 together | error | Public + Verified |
| P4 | Z-Index Limit z-index must not exceed 100 | error | Public + Verified |
| P5 | Resource Budgets HTML ≤1MB, CSS ≤50KB, images ≤20, per-image ≤1MB, DOM nodes ≤500 | error | Public + Verified |
| P6 | Animation Limits Infinite animations on full-size elements are rejected | warning | Public + Verified |
| P7 | Marketplace Content No deceptive inputs; phishing phrases trigger human review | error | Public only |
| P8 | Version Immutability Published versions can never be overwritten | error | All levels |
| P9 | Explicit Data Inputs {{token}} placeholders must be declared in block.properties | warning | All levels |
CSS must not use selectors that affect the host page. Blocks render inside a Shadow DOM — all styles must be scoped to the block itself using :host or class selectors.
body { color: red; }
html { font-size: 16px; }
* { box-sizing: border-box; }
:root { --x: 1; }
:host-context(.dark) { color: white; }:host { display: block; }
.hero { padding: 40px; }
h2 { font-size: 2rem; }
.card:hover { opacity: 0.9; }Blocks are static HTML + CSS — no JavaScript execution is permitted. All of the following are rejected:
<script> <iframe> <object> <embed> <form> <base> <meta http-equiv> <link rel="import">onclick, onmouseover, onerror, and any other on* attributejavascript: and data:text/htmlsrcdoc<!-- Forbidden -->
<script>alert(1)</script>
<iframe src="https://evil.com"></iframe>
<button onclick="steal()">Click</button>
<a href="javascript:void(0)">Nope</a>
<!-- Allowed -->
<section class="hero">
<h1>Hello world</h1>
<a href="https://example.com" target="_blank" rel="noopener">Learn more</a>
<button type="button">CTA</button>
</section>Blocks must stay within their container boundaries and cannot break out of the host page's layout.
position: fixed — always forbiddenposition: sticky — only permitted for block types sticky-header and sticky-cta on public/verified accountswidth: 100vw, height: 100vh, width: 100dvw, height: 100dvh — forbidden; use 100% or relative units insteadtop: 0 and left: 0 together in the same CSS rule — forbidden without explicit positioning context.overlay {
position: fixed;
width: 100vw;
height: 100vh;
}.hero {
position: relative;
width: 100%;
max-width: 1200px;
}Z-index values must not exceed 100. This prevents blocks from overlapping the host page's navigation, modals, or other UI elements. z-index: auto and z-index: inherit are always permitted.
z-index: 9999;
z-index: 1000;
z-index: 101;z-index: 10;
z-index: 100; /* maximum */
z-index: auto;Blocks must stay within resource limits to ensure fast load times on any host page.
| Resource | Limit |
|---|---|
| HTML size | 1 MB |
| CSS size | 50 KB |
| Total number of images | 20 |
| Single image size (decoded) | 1 MB |
| Estimated DOM nodes | 500 |
Infinite CSS animations can drain CPU on host pages. Two tiers of enforcement apply:
animation-iteration-count: infinite on an element that also declares width: 100% or height: 100%animation-iteration-count: infinite anywhere else — the block is accepted but Workspace shows a noticeanimation-iteration-count: 3) or respect user preferences with @media (prefers-reduced-motion).Public Marketplace blocks must not contain deceptive content. Two categories of checks run:
type="password" or type="credit-card"name="cc", name="card", or name="ssn"Once a block version is published it can never be overwritten. Every save creates a new immutable version. This guarantees that any site embedding a specific version always renders exactly what was reviewed and approved.
Any {{token}} placeholder used in the block HTML must be declared in block.properties. Undeclared placeholders produce a non-blocking warning — the block is accepted but the runtime will render the raw placeholder string instead of a value.
<!-- {{userName}} used but missing
from block.properties -->
<h1>Hello, {{userName}}</h1>{
"html": "<h1>Hello, {{name}}</h1>",
"properties": {
"name": {
"type": "string",
"default": "World"
}
}
}Your account's trust level is set by Cellpy. It controls which principles apply. Higher trust unlocks fewer restrictions for advanced use cases — contact support to request a trust-level upgrade.
| Principle | Public | Verified | Private |
|---|---|---|---|
| P1 — CSS Scoping | ✓ | ✓ | ✓ |
| P2 — No Script Execution | ✓ | ✓ | ✓ |
| P3 — No Layout Escaping | ✓ | ✓ | — |
| P4 — Z-Index Limit | ✓ | ✓ | — |
| P5 — Resource Budgets | ✓ | ✓ | — |
| P6 — Animation Limits | ✓ | ✓ | — |
| P7 — Marketplace Content | ✓ | — | — |
| P8 — Immutability | ✓ | ✓ | ✓ |
| P9 — Data Inputs | ✓ | ✓ | ✓ |
Serve your block files directly from your own cloud storage — AWS S3, Azure Blob Storage, or Cloudflare R2.
By default, Cellpy stores and serves your block content from its own CDN infrastructure. With a Pro plan or above, you can point Cellpy to your own storage bucket instead. When configured, block HTML and CSS are uploaded exclusively to your bucket — Cellpy only stores a URL pointer and never holds your content.
Block content is uploaded to your bucket at assign time. Cellpy holds only a URL pointer — no content copy on Cellpy infrastructure.
The runtime fetches a lightweight container JSON from Cellpy's CDN, then fetches the block file directly from your storage — browser to your bucket.
Re-assigning a container automatically migrates older block versions to your storage, so you don't need to republish.
Control the filename pattern for block files, e.g. {blockId}-v{version}.json or blocks/{accountSlug}/{blockId}.json.
When you assign a block to a container with own storage configured:
{ "url": "https://your-bucket/..." }.<cellpy-block> runtime fetches the container JSON from Cellpy's CDN, reads the URL, then fetches the block file directly from your storage.GET requests from any origin (CORS). The runtime fetches block files directly in the browser — no server-side proxy.Follow these steps to create and configure an S3 bucket for Cellpy block storage.
In the AWS Console, go to S3 → Create bucket. Choose:
my-company-cellpy-blocksIn your bucket, go to Permissions → Bucket policy and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}Replace YOUR-BUCKET-NAME with your actual bucket name. This allows any browser to fetch block files by URL — required for the runtime's direct fetch.
In Permissions → Cross-origin resource sharing (CORS), paste:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]This permits browsers on any domain to fetch block files. If you want to restrict to specific domains, replace "*" in AllowedOrigins with your site URLs.
Cellpy needs credentials to upload block files. Create a dedicated IAM user:
cellpy-blocks-writer.{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}On the IAM user page, go to Security credentials → Create access key. Select Application running outside AWS. Download or copy:
In the Cellpy dashboard, go to Account → Storage and fill in:
my-company-cellpy-blocks)us-east-1https://my-company-cellpy-blocks.s3.us-east-1.amazonaws.com{blockId}-v{version}.jsonClick Save configuration, then Verify connection. Cellpy uploads a test file to your bucket and fetches it from the public URL to confirm both write access and public read access are working.
Your account slug appears in all CDN URLs and must be set before embedding blocks. Go to Account settings and update the Account slug field to something that represents your brand — e.g. my-company.
In your site code, tell the runtime where to resolve container JSON. Set window.CELLPY_CDN before registering <cellpy-block>. The value depends on whether you have a custom CDN domain:
// Option A — Cellpy shared CDN (default, account slug in path)
window.CELLPY_CDN = 'https://cdn.cellpy.com/my-company'
// Option B — your own custom CDN domain (configured in Account → Domains)
// The domain itself identifies your account — no slug needed in the path
window.CELLPY_CDN = 'https://cdn.my-company.com'
// Then register the element (Vue / Svelte / vanilla JS)
import { CellpyBlock } from '@cellpysdk/runtime'
customElements.define('cellpy-block', CellpyBlock)<script> CDN tag instead of the npm package, add a data-cdn attribute: <script src="https://cdn.cellpy.com/runtime.js" data-cdn="https://cdn.my-company.com" defer></script>Go to Assign, select a container and a published block version, and click assign. Cellpy will upload the block JSON to your S3 bucket and update the container pointer. Check your bucket in the AWS Console — you should see a file matching your path template within seconds.
The path template controls the key (filename) used when uploading block files to your bucket. Available tokens:
Examples:
{blockId}-v{version}.json → my-hero-section-v3.json (default)
blocks/{blockId}/v{version}.json → blocks/my-hero-section/v3.json
{accountSlug}/{blockId}-v{version}.json → my-company/my-hero-section-v3.jsonAWS S3 Free Tier includes 5 GB storage, 20,000 GET requests, and 2,000 PUT requests per month for the first 12 months. After that, standard pricing applies.
<cellpy-block> element. A page with 5 blocks visited by 4,000 unique users per month = 20,000 GET requests — exactly at the Free Tier ceiling. For high-traffic pages, use Cloudflare R2 (no egress fees, no GET request charges) or budget for S3 costs.Let Claude find freely-licensed stock photos from Pexels and Pixabay while designing blocks.
When you use the Claude Design tool to create or edit a block, Claude can call a built-in photo_search tool to discover and embed stock photos automatically. This tool queries Pexels and Pixabay — both are free services. You only need to supply your own API keys so requests go under your account's quota.
Both Pexels and Pixabay offer free API access with generous rate limits — no credit card required.
All photos are royalty-free and suitable for commercial use under their respective licenses.
Using your own keys means your searches don't count against shared infrastructure limits.
Photo Search works only when at least one key is configured. You can add either or both services.
Pexels offers a free API with up to 200 requests per hour and 20,000 per month on the free plan.
Go to pexels.com and click Join. You can sign up with an email address or continue with Google. Verify your email address if prompted.
After signing in, navigate to pexels.com/api. Click the Your API Key button, or go directly to your account image menu in the top right → Image & Video API.
Fill in the short application form:
Click Get API Key. Approval is instant for most accounts.
Your API key is shown immediately after approval. It looks like a long alphanumeric string, e.g. Kvq4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Copy it now.
In the Cellpy dashboard, go to Settings → API Keys, paste your key into the Pexels API Key field, and click Save keys.
Pixabay's free API gives access to over 4 million images and videos, with up to 100 requests per minute and 5,000 per day on the free plan.
Go to pixabay.com and click Join. Register with an email address or sign in with Google. Confirm your email if required.
After signing in, navigate to pixabay.com/api/docs. Your personal API key is displayed at the top of the page, embedded in the example request URL.
The key appears inside the example URL in a format like: https://pixabay.com/api/?key=12345678-abcdef1234567890abcdef12&q=…. Copy only the key portion (the part after key= and before &).
In the Cellpy dashboard, go to Settings → API Keys, paste your key into the Pixabay API Key field, and click Save keys.
Once you've saved at least one key, open the Claude Design tool and ask Claude to create a block that includes a photo — for example:
Create a hero section with a photo of mountains at sunsetClaude will call photo_search automatically, retrieve matching images, and embed the best match in the block. If no key is configured, Claude will instead use a placeholder or an inline SVG.