Blok Geliştirme
VeloCMS Sayfa Oluşturucu'nun kalbinde bir blok kayıt sistemi (registry) yatıyor. Bu sistem, mevcut her blok türünü tanımlayan `BlockMeta` nesnelerinden oluşan basit bir liste aslında. Palet arayüzü, aranabilir blok kartlarını göstermek için bu kaydı okur. Inspector paneli doğru prop düzenleyiciyi bulmak için yine buraya başvurur. Renderer ise bir bloğun sunucu taraflı veri çekmeye ihtiyacı olup olmadığını ya da saf bir client bileşeni olarak render edilip edilemeyeceğini anlamak için bu kaydı kullanır. Yani siz özel bir blok eklediğinizde, aslında bu kayıt sistemine yeni bir girdi ve ona karşılık gelen bir React bileşeni eklemiş oluyorsunuz.
BlockMeta ve BlockType
Temel tipler `src/lib/pagebuilder/types.ts` dosyasında bulunuyor. Kayıt sistemindeki her bir blok, bir `BlockMeta` nesnesiyle tanımlanır:
export interface BlockMeta {
/** The unique string key used in the AST */
type: BlockType;
/** Human-readable name shown in the palette */
displayName: string;
/** Lucide icon name for the palette card */
icon: string;
/** Category for grouping in palette search */
category: "layout" | "content" | "media" | "conversion" | "data" | "utility";
/** Palette tier — affects which plans can use the block */
tier: 1 | 2 | 3;
/** True if the block can contain child blocks */
isContainer: boolean;
/** True if the block requires server-side data (uses fetch in getBlockData) */
isServerBlock: boolean;
}Palet katman sistemi
Tier 1 blokları (temel setimiz: Container, Button, Image, Heading, Lead Form, Icon Box, Post Grid, Accordion, Gallery, Testimonial) tüm planlarda mevcut. Tier 2, slider'lar ve geri sayım sayaçları gibi dönüşüm odaklı blokları kapsıyor ve Pro ve üzeri planlarda kullanılabiliyor. Tier 3 ise daha niş ihtiyaçlara yönelik (grafikler, rezervasyon widget'ları, özel kod gibi) ve yalnızca Business ve Agency planlarında var. Kendi özel bloğunuzu geliştirirken, onun yetenek seviyesine uygun bir katman seçmeniz gerekiyor.
| Tier | Blocks | Available on |
|---|---|---|
| Tier 1 | Foundation set | All plans |
| Tier 2 | Conversion-focused | Pro and above |
| Tier 3 | Long-tail / advanced | Business and Agency |
Adım adım özel blok ekleme
Hadi bir `Pricing Slider` bloğu ekleyelim. Bu blok, 4'ten fazla katmanı olan planlar için kullanışlı, yatay olarak kaydırılabilen bir fiyatlandırma kartları satırı olacak. Bunun için üç şeye ihtiyacımız var: `src/lib/pagebuilder/registry.ts` içinde bir kayıt girdisi, bloğun prop'ları için bir Zod şeması ve bir de render bileşeni.
{
type: "pricing-slider",
displayName: "Pricing Slider",
icon: "SlidersHorizontal",
category: "conversion",
tier: 2,
isContainer: false,
isServerBlock: false,
},Sırada bloğun prop şemasını tanımlamak var. Bu şema hem inspector arayüzünü (blok seçildiğinde sağ panelde hangi alanların görüneceğini) hem de çalışma zamanı doğrulamalarını (Zod, AST'yi yüklerken her blok düğümünü parse eder) yönetir.
import { z } from "zod";
const PricingCardSchema = z.object({
name: z.string().min(1),
price: z.string(), // e.g. "$9/mo"
description: z.string(),
features: z.array(z.string()),
ctaLabel: z.string().default("Get started"),
ctaHref: z.string().url().optional(),
highlighted: z.boolean().default(false),
});
export const PricingSliderSchema = z.object({
type: z.literal("pricing-slider"),
cards: z.array(PricingCardSchema).min(2).max(8),
heading: z.string().optional(),
subheading: z.string().optional(),
});
export type PricingSliderProps = z.infer<typeof PricingSliderSchema>;Şimdi de render bileşenine geçelim. Bu bileşen, doğrulanmış blok verilerini prop olarak alır. Tailwind sınıflarını kullanacağız ve varsayılan olarak bu bir Server Component olacak (`isServerBlock: false` ayarı, sunucudan veri çekmeden render edileceği anlamına gelir):
import { cn } from "@/lib/utils";
import type { PricingSliderProps } from "@/lib/pagebuilder/schemas/pricing-slider";
export function PricingSlider({ cards, heading, subheading }: PricingSliderProps) {
return (
<section className="py-16 px-4">
{heading && (
<h2 className="text-4xl font-extrabold text-center tracking-tighter mb-3">
{heading}
</h2>
)}
{subheading && (
<p className="text-muted-foreground text-center mb-10">{subheading}</p>
)}
<div className="flex gap-6 overflow-x-auto pb-4 snap-x snap-mandatory">
{cards.map((card, i) => (
<div
key={i}
className={cn(
"snap-center shrink-0 w-72 rounded-2xl border p-8",
card.highlighted
? "bg-foreground text-background border-foreground"
: "bg-card border-border/60"
)}
>
<p className="text-sm font-bold uppercase tracking-widest mb-2 opacity-70">
{card.name}
</p>
<p className="text-4xl font-black tracking-tighter mb-4">{card.price}</p>
<p className="text-sm opacity-80 mb-6">{card.description}</p>
<ul className="space-y-2 mb-8 text-sm">
{card.features.map((f, j) => (
<li key={j} className="flex gap-2">
<span>✓</span> {f}
</li>
))}
</ul>
{card.ctaHref && (
<a
href={card.ctaHref}
className={cn(
"block text-center rounded-xl py-3 font-semibold transition-colors",
card.highlighted
? "bg-background text-foreground hover:bg-background/90"
: "bg-foreground text-background hover:bg-foreground/90"
)}
>
{card.ctaLabel}
</a>
)}
</div>
))}
</div>
</section>
);
}Blok renderer'ını kaydetme
`src/components/pagebuilder/BlockRenderer.tsx` içindeki renderer, doğru bileşeni seçmek için `block.type` üzerinde bir switch-case yapısı kullanır. Yeni blok tipiniz için siz de bir `case` ekleyin:
import { PricingSlider } from "./blocks/PricingSlider";
import { PricingSliderSchema } from "@/lib/pagebuilder/schemas/pricing-slider";
// Inside the switch(block.type):
case "pricing-slider": {
const parsed = PricingSliderSchema.safeParse(block);
if (!parsed.success) return null;
return <PricingSlider {...parsed.data} />;
}Sunucu blokları ve veri çekme
Eğer bloğunuzun render zamanında veri çekmesi gerekiyorsa (örneğin son yazıları getiren bir 'Son Yazılar' bloğu gibi), kayıt girdisinde `isServerBlock: true` olarak ayarlayın ve bir `getBlockData` fonksiyonu yazın. Renderer bu fonksiyonu sunucu tarafında çağırır ve dönen sonucu `blockData` prop'u olarak bileşeninize geçirir. `Post Grid` bloğu (`src/components/pagebuilder/blocks/PostGrid.tsx`) bu tür sunucu blokları için en iyi örnektir.
Referans
Tüm blok kayıt listesi
VeloCMS'te şu anda kayıtlı olan tüm blok türleri. Bu liste `src/lib/pagebuilder/types.ts` dosyasından otomatik olarak çekilmiştir.
| Block type | Display name | Tier | Available on |
|---|---|---|---|
| `Container` | Container | 1 | All plans |
| `Button` | Button | 1 | All plans |
| `Image` | Image | 1 | All plans |
| `Heading` | Heading | 1 | All plans |
| `PostGrid` | Post Grid | 1 | All plans |
| `Countdown` | Countdown Timer | 2 | Pro and above |
| `CustomCode` | Custom Code | 3 | Business and Agency |
BlockMeta alanları referansı
| Field | Type | Description |
|---|---|---|
| `type` | `BlockType` | Blok tipi için benzersiz tanımlayıcı. |
| `name` | `string` | Palette gösterilen görünen ad. |
| `description` | `string` | Palet kartı için kısa açıklama. |
| `icon` | `React.ElementType` | İkon bileşeni (lucide-react'ten). |
| `tier` | `1 | 2 | 3` | Katman seviyesi (1, 2 veya 3). |
| `schema` | `ZodObject` | Bloğun prop'ları için Zod şeması. |
| `defaults` | `object` | Yeni bir blok örneği için varsayılan prop'lar. |
| `isServerBlock` | `boolean` | Eğer `true` ise, blok sunucu taraflı veri çekme gerektirir. |
| `getBlockData` | `function` | Sunucu blokları için veri çeken fonksiyon. |
Katman erişim kuralları
| Tier | Available on | Block count | Example blocks |
|---|---|---|---|
| 1 | Tüm planlar | ~10 | `Container`, `Heading`, `Image` |
| 2 | Pro ve üzeri | ~15 | `Slider`, `Countdown`, `PricingTable` |
| 3 | Business ve Agency | ~20+ | `Chart`, `BookingForm`, `CustomCode` |
Blok şeması için Zod desenleri
import { z } from "zod";
// Common patterns used in block schemas:
// Color token (must reference CSS variable, not raw hex)
const colorToken = z.enum([
"primary", "foreground", "muted", "background", "card",
"destructive", "accent"
]);
// Spacing scale (matches Tailwind's 4px grid)
const spacingScale = z.union([
z.literal(0), z.literal(4), z.literal(8), z.literal(12),
z.literal(16), z.literal(24), z.literal(32), z.literal(48),
z.literal(64), z.literal(96), z.literal(128),
]);
// Alignment
const alignment = z.enum(["left", "center", "right"]);
// Button variant
const buttonVariant = z.enum(["primary", "secondary", "ghost", "outline", "link"]);
// Image source (R2 URL or external HTTPS)
const imageSrc = z.string().url().or(z.string().startsWith("/"));Sunucu bloğu veri çekme deseni
// Server blocks declare a getBlockData function alongside the schema.
// The renderer calls this before rendering and passes result as blockData prop.
export async function getBlockData(
block: YourBlockSchema,
context: { tenantId: string; siteUrl: string }
): Promise<YourBlockServerData> {
const db = await getTenantDB(context.tenantId);
const posts = await db.getCollection("posts").getList(1, block.limit ?? 6, {
filter: 'status = "published"',
sort: "-published_at",
});
return { posts: posts.items };
}
// In your component:
export function YourServerBlock({
// ...block schema props
blockData, // YourBlockServerData | null — null if fetch failed
}: YourBlockSchema & { blockData: YourBlockServerData | null }) {
if (!blockData) return null; // graceful fallback
// render with blockData
}Blok AST düğüm yapısı
// Every block node in the AST follows this shape:
interface Block {
id: string; // nanoid — unique per page
type: BlockType; // matches registry entry
props: unknown; // validated by block's Zod schema at render time
childrenIds: string[];// empty for non-containers
parentId: string | null;
locked: boolean; // true = cannot be moved/deleted in editor
hidden: boolean; // true = skip render (draft element)
}
// Example post grid node:
{
id: "blk_4xKm9",
type: "loop-builder",
props: {
type: "loop-builder",
limit: 6,
columns: 3,
showExcerpt: true,
showTags: true,
cardStyle: "default"
},
childrenIds: [],
parentId: "blk_root",
locked: false,
hidden: false
}