Blok Geliştirme

11 min readUpdated 27 Apr 2026

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:

src/lib/pagebuilder/types.ts (excerpt)
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;
    }
typescript

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.

TierBlocksAvailable on
Tier 1Foundation setAll plans
Tier 2Conversion-focusedPro and above
Tier 3Long-tail / advancedBusiness 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.

src/lib/pagebuilder/registry.ts (add this entry)
{
      type: "pricing-slider",
      displayName: "Pricing Slider",
      icon: "SlidersHorizontal",
      category: "conversion",
      tier: 2,
      isContainer: false,
      isServerBlock: false,
    },
typescript

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.

src/lib/pagebuilder/schemas/pricing-slider.ts
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>;
typescript

Ş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):

src/components/pagebuilder/blocks/PricingSlider.tsx
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>
      );
    }
typescript

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:

src/components/pagebuilder/BlockRenderer.tsx (add case)
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} />;
    }
typescript

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 typeDisplay nameTierAvailable on
`Container`Container1All plans
`Button`Button1All plans
`Image`Image1All plans
`Heading`Heading1All plans
`PostGrid`Post Grid1All plans
`Countdown`Countdown Timer2Pro and above
`CustomCode`Custom Code3Business and Agency

BlockMeta alanları referansı

FieldTypeDescription
`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ı

TierAvailable onBlock countExample blocks
1Tüm planlar~10`Container`, `Heading`, `Image`
2Pro ve üzeri~15`Slider`, `Countdown`, `PricingTable`
3Business ve Agency~20+`Chart`, `BookingForm`, `CustomCode`

Blok şeması için Zod desenleri

Block schema — common field patterns
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("/"));
typescript

Sunucu bloğu veri çekme deseni

src/lib/pagebuilder/server-data.ts (pattern)
// 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
    }
typescript

Blok AST düğüm yapısı

Block AST node (PageAST.blocks[id])
// 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
    }
typescript