The Blog Engine
Velo uses a highly secure, multi-tenant Headless CMS approach. Every client (tenant) has their own completely isolated Sanity database.
CRITICAL SECURITY RULE
The frontend must NEVER hardcode a projectId in sanity.config.ts or similar files. The connection must be strictly dynamic.
Implementation Steps
Step 1: Identify the Tenant
The frontend must look up the tenant's configuration from the Velo database. Determine the slug from the hostname or URL.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const { data: org } = await supabase
.from('organizations')
.select('sanity_project_id, sanity_dataset')
.eq('slug', tenantSlug) // Dynamically determined
.single();Step 2: Dynamically Fetch Content
Use the sanity_project_id to initialize the client on the fly.
import { createClient } from '@sanity/client'
const sanityClient = createClient({
projectId: org.sanity_project_id,
dataset: org.sanity_dataset || 'production',
apiVersion: '2024-05-15',
useCdn: true,
});
const query = `*[_type == "post" && status == "published"] | order(publishedAt desc) {
_id,
title,
"slug": slug.current,
mainImage { asset->{ url } },
publishedAt,
body, // TipTap JSON format
seo
}`;
const posts = await sanityClient.fetch(query);Rendering TipTap JSON
Velo stores body content as stringified TipTap JSON. You must parse and render it using TipTap extensions.
import { generateHTML } from '@tiptap/html'
import StarterKit from '@tiptap/starter-kit'
// 1. Parse the JSON
const jsonContent = JSON.parse(post.body);
// 2. Generate HTML (or use a TipTap component)
const html = generateHTML(jsonContent, [StarterKit]);
// 3. Render
<div dangerouslySetInnerHTML={{ __html: html }} className="prose" />AI Agent Prompt
Notes for AI Agents
No Portable Text
Velo does NOT use standard Sanity Portable Text. Ignore instructions about @portabletext/react. Use TipTap utilities instead.
Multi-Tenancy Guard
The projectId is different for every customer. Always fetch it dynamically from the organizations table in Supabase.
Security
Never expose the sanity_write_token on the frontend. Use it only in server-side dashboard actions.
