Plugin Geliştirme
VeloCMS plugin'leri bir V8 isolate içinde çalışır. Bu, kodunuza yalnızca belirli bir API yüzeyine erişim izni veren ve başka hiçbir şeye karışmasına izin vermeyen katı bir sanal alan (sandbox) sınırı demek. Yani `require()`, `fs` veya `process.env` gibi şeyler yok. Elinizin altında sadece `velocms` global nesnesi (posts, media, settings, network, logger gibi) ve site sahibinin yetenek manifestosunda izin verdiği ne varsa o var. Bu mimarinin sebebi basit: bir başkasının blogunda üçüncü parti kod çalıştırmak ciddi bir güven meselesi. İşte `isolated-vm` de bu sorunu ayrı bir alt süreç (subprocess) yükü olmadan çözüyor.
Sandbox modeli
Her plugin çalıştırıldığında, sıfırdan bir `isolated-vm` Isolate'i oluşturulur. `VelocmsPluginAPI` köprüsü, etkileşim anında kopyalanan bir host nesnesi olarak içeri enjekte edilir, plugin kodunuz bu isolate içinde çalışır ve hook işlemi bittiğinde de isolate yok edilir. Bellek kullanımı isolate bellek limitiyle (varsayılan: 32MB) sınırlıdır. CPU süresi ise katı bir zaman aşımıyla (varsayılan: her hook için 5s) kısıtlanır. Eğer plugin'iniz bu limitlerden birini aşarsa, anında sonlandırılır ve hook zinciri onsuz yoluna devam eder.
Sandbox içinde yapamayacaklarınız şunlar: Node.js modüllerini import etmek, `require()` çağırmak, global `process` veya `__dirname` değişkenlerine erişmek, `setTimeout` ya da `setInterval` kullanmak (asenkron işlemler `velocms.network.fetch` üzerinden yürür) veya dosya sistemine dokunmak. Peki ne yapabilirsiniz? `velocms.content.posts.list()` ile içerikleri çekebilir, `velocms.settings` aracılığıyla ayarları okuyup yazabilir, izin verilen URL'lere HTTP istekleri atabilir ve `velocms.logger` ile yapılandırılmış loglar bırakabilirsiniz.
Yetenek manifestosu
Yetenekler, plugin'inizin `manifest.json` dosyasında beyan edilir ve plugin'iniz yayınlanmadan önce VeloCMS pazar yeri ekibi tarafından incelenir. Site sahibi, plugin'inizin tam olarak neye ihtiyaç duyduğunu görür ve eğer bu izinler içine sinmezse kurulumu reddedebilir. İşte tüm yeteneklerin listesi:
| Yetenek | Ne işe yarar | Risk seviyesi |
|---|---|---|
| `content:posts:read` | Yazıları ve sayfaları okuma izni verir (`velocms.content.posts.list`, `velocms.content.posts.get`). | Düşük |
| `content:posts:write` | Yazı oluşturma/güncelleme izni verir (`velocms.content.posts.create`, `velocms.content.posts.update`). | Orta |
| `settings:read` | Site ayarlarını okuma izni verir (`velocms.settings.get`). | Düşük |
| `settings:write` | Site ayarlarını değiştirme izni verir (`velocms.settings.set`). | Yüksek |
| `network:fetch` | Manifest dosyasında belirtilen URL'lere HTTP istekleri yapma izni verir. | Orta |
Hook API'ı
Hook'lar, VeloCMS içerik yaşam döngüsünün belirli anlarında tetiklenir. Siz de plugin tanımınızda, bu hook isimlerine karşılık gelen handler fonksiyonları kaydedersiniz. Bütün hook'lar asenkrondur ve tipli bir payload alır; `beforePostPublish` ve `beforePostCreate` gibi bazı hook'lar ise değiştirilmiş bir payload verisi döndürebilir. Böyle bir durumda VeloCMS orijinal veri yerine sizin döndürdüğünüzü kullanır.
| Hook | Ne zaman tetiklenir | Payload'u değiştirebilir mi |
|---|---|---|
| `beforePostCreate` | Yeni bir yazı kaydedilmeden hemen önce. | Evet |
| `afterPostCreate` | Yeni bir yazı veritabanına kaydedildikten hemen sonra. | Hayır |
| `beforePostPublish` | Bir yazı yayınlanmadan hemen önce. | Evet |
| `afterPostPublish` | Bir yazı yayınlandıktan hemen sonra. | Hayır |
Hello World — baştan sona bir plugin
Hadi işe yarar en basit plugin'i yapalım: her yazı yayınlandığında çalışma log'una bir mesaj bırakan bir plugin. Sadece üç dosya lazım: `manifest.json`, `index.ts` ve bir de test dosyası.
{
"id": "my-org.publish-logger",
"name": "Publish Logger",
"version": "1.0.0",
"description": "Logs a structured message whenever a post is published.",
"author": {
"name": "My Org",
"email": "[email protected]"
},
"capabilities": ["content:read"],
"hooks": ["afterPostPublish"],
"builtin": false,
"engines": {
"velocms": ">=1.0.0"
},
"pricing": {
"model": "free"
}
}import type { VelocmsPluginAPI } from "@velocms/plugin-sdk";
export async function afterPostPublish(
payload: { post: { id: string; title: string; slug: string } },
velocms: VelocmsPluginAPI
): Promise<void> {
velocms.logger.info("Post published", {
postId: payload.post.id,
title: payload.post.title,
url: `/blog/${payload.post.slug}`,
});
}`mockVelocms` ile test etme
SDK, tamamen bellekte çalışan bir `VelocmsPluginAPI` oluşturan `mockVelocms` adında bir test yardımcısıyla birlikte gelir. Yani gerçek bir PocketBase bağlantısına falan gerek yok. Vitest içinde hook handler'larınızı hiçbir altyapı kurmadan test edebilirsiniz:
import { describe, it, expect } from "vitest";
import { createMockVelocms } from "@velocms/plugin-sdk/testing";
import { afterPostPublish } from "./index";
describe("publish-logger", () => {
it("logs a message with post metadata when afterPostPublish fires", async () => {
const velocms = createMockVelocms();
await afterPostPublish(
{ post: { id: "post123", title: "Hello World", slug: "hello-world" } },
velocms
);
expect(velocms.logger.info).toHaveBeenCalledWith(
"Post published",
expect.objectContaining({ postId: "post123" })
);
});
});Kill-switch (durdurma) mekanizması
Kurulu her plugin, site sahibi tarafından admin panelinden (Settings → Plugins) kaldırılmadan durdurulabilir. Bir plugin'i bu şekilde durdurmak, `site_settings.installed_plugins` içindeki `enabled` bayrağını `false` yapar. Böylece hook yükleyici, sonraki her çağrıda bu plugin'i atlar. Ne yeniden başlatma gerekir ne de yeniden deploy etme. Eğer bir plugin hataya neden olursa, VeloCMS art arda 3 hook hatasından sonra onu otomatik olarak devre dışı bırakır ve site sahibini e-posta ile bilgilendirir.
Pazar yerine gönderme
Plugin'iniz hazır olduğunda, `/developers` adresine gidin ve başvuru formunu doldurun. `manifest.json`, derlenmiş `index.js` dosyanız (veya TypeScript kaynak kodunuz) ve isteğe bağlı olarak bir `README.md` içeren bir ZIP dosyası yükleyeceksiniz. İnsan incelemesinden önce otomatik güvenlik taramaları yapıyoruz (beyan edilen yeteneklerle gerçekte erişilen API yüzeyini karşılaştırıyoruz). İlk başvurunuzun onaylanması 3-7 gün sürebilir, hazırlıklı olun.
Referans
Hook kataloğu
| Hook | Ne zaman tetiklenir | Payload'u değiştirebilir mi | Payload tipi |
|---|---|---|---|
| `beforePostCreate` | Yeni bir yazı kaydedilmeden hemen önce. | Evet | `{ post: Post }` |
| `afterPostCreate` | Yeni bir yazı veritabanına kaydedildikten hemen sonra. | Hayır | `{ post: Post }` |
| `beforePostUpdate` | Bir yazı güncellenmeden hemen önce. | Evet | `{ post: Post, oldPost: Post }` |
| `afterPostUpdate` | Bir yazı güncellendikten hemen sonra. | Hayır | `{ post: Post }` |
| `beforePostPublish` | Bir yazı yayınlanmadan hemen önce. | Evet | `{ post: Post }` |
| `afterPostPublish` | Bir yazı yayınlandıktan hemen sonra. | Hayır | `{ post: Post }` |
| `beforePostDelete` | Bir yazı silinmeden hemen önce. | Hayır | `{ postId: string }` |
| `afterPostDelete` | Bir yazı silindikten hemen sonra. | Hayır | `{ postId: string }` |
Yetenek manifestosu seçenekleri
| Yetenek | Ne işe yarar | Risk seviyesi |
|---|---|---|
| `content:posts:read` | Yazıları ve sayfaları okuma izni verir (`velocms.content.posts.list`, `velocms.content.posts.get`). | Düşük |
| `content:posts:write` | Yazı oluşturma/güncelleme izni verir (`velocms.content.posts.create`, `velocms.content.posts.update`). | Orta |
| `content:media:read` | Medya kütüphanesini listeleme izni verir. | Düşük |
| `content:media:write` | Medya kütüphanesine dosya yükleme izni verir. | Orta |
| `settings:read` | Site ayarlarını okuma izni verir (`velocms.settings.get`). | Düşük |
| `settings:write` | Site ayarlarını değiştirme izni verir (`velocms.settings.set`). | Yüksek |
| `network:fetch` | Manifest dosyasında belirtilen URL'lere HTTP istekleri yapma izni verir. | Orta |
`VelocmsPluginAPI` yüzeyi
Aşağıdaki alanlar, her plugin sanal alanı içindeki global `velocms` nesnesinde mevcuttur. 'İsteğe bağlı' olarak işaretlenen alanlar, yalnızca ilgili yetenek `manifest.json` dosyasında beyan edildiğinde bulunur.
| Alan | Tip | İsteğe bağlı | Açıklama |
|---|---|---|---|
| `content.posts` | `{ list, get, create, update, delete }` | Evet | Yazı içeriğini yönetmek için metotlar. `read` ve `write` yeteneklerine bağlıdır. |
| `content.media` | `{ list, upload }` | Evet | Medya kütüphanesini yönetmek için metotlar. |
| `settings` | `{ get, set }` | Evet | Site ayarlarını okumak ve yazmak için metotlar. |
| `network.fetch` | `(url: string, options: RequestInit) => Promise<Response>` | Evet | HTTP istekleri yapmak için standart Fetch API'ı. |
| `logger` | `{ info, warn, error }` | Hayır | Yapılandırılmış logları plugin çalıştırma loglarına yazmak için. |
Plugin durdurma (kill-switch) davranışı
| Durum | Nasıl tetiklenir | Etkisi | Kurtarma |
|---|---|---|---|
| Manuel olarak devre dışı bırakıldı | Site sahibi admin panelinden plugin'i devre dışı bırakır. | Plugin'in hiçbir hook'u çalıştırılmaz. | Site sahibi admin panelinden tekrar etkinleştirir. |
| Otomatik olarak devre dışı bırakıldı | Bir hook art arda 3 kez hata verir. | Plugin'in hiçbir hook'u çalıştırılmaz. Site sahibine e-posta gönderilir. | Site sahibi admin panelinden tekrar etkinleştirir. |
Sandbox limitleri
| Limit | Varsayılan | Ayarlanabilir mi | Aşıldığında ne olur |
|---|---|---|---|
| Bellek | 32MB | Hayır | Isolate sonlandırılır, hook hatası loglanır. |
| CPU Zamanı | 5 saniye | Hayır | Isolate sonlandırılır, hook hatası loglanır. |
| Log Çıktısı | 1MB | Hayır | Daha fazla log mesajı yoksayılır. |
Plugin manifestosu — minimum gerekli alanlar
{
"id": "your-org.plugin-name",
"name": "Human Readable Name",
"version": "1.0.0",
"description": "One sentence.",
"author": { "name": "Your Name", "email": "[email protected]" },
"capabilities": [],
"hooks": ["afterPostPublish"],
"builtin": false,
"engines": { "velocms": ">=1.0.0" },
"pricing": { "model": "free" }
}Hook handler TypeScript imzası
// Every hook handler follows this shape:
export async function hookName(
payload: HookPayload, // typed per-hook (see Hook catalog above)
velocms: VelocmsPluginAPI // the sandbox API bridge
): Promise<void | Partial<HookPayload>> {
// Return void for observation-only hooks (afterPost*, afterMember*, onDailyDigest)
// Return Partial<HookPayload> to modify the subject (beforePostPublish only)
}`mockVelocms` ile test — tüm API
import { createMockVelocms } from "@velocms/plugin-sdk/testing";
const velocms = createMockVelocms({
// Pre-seed posts for content:read testing
posts: [{ id: "1", title: "Test Post", slug: "test-post", status: "published", ... }],
// Pre-seed settings for settings API testing
settings: { "my-plugin.api-key": "test-key-123" },
});
// The mock exposes vi.fn() spies on all methods:
expect(velocms.logger.info).toHaveBeenCalledWith("...", expect.any(Object));
expect(velocms.content.posts.list).toHaveBeenCalledOnce();
// Reset between tests:
velocms.reset();