Content Drivers
The ContentDriver contract, FileDriver, and HTTP-backed indexing patterns for VelvetCMS.
Primary contract: VelvetCMS\Contracts\ContentDriver
ContentDriver Interface #
Defines how pages load, persist, enumerate, and report freshness. VelvetCMS\Drivers\Content\FileDriver ships with Core and is bound to content.driver. Remote gateways must implement every interface method, including lastModified().
Definition #
interface ContentDriver
{
/**
* @throws \VelvetCMS\Exceptions\NotFoundException
*/
public function load(string $slug): Page;
/**
* @throws \VelvetCMS\Exceptions\ValidationException
*/
public function save(Page $page): bool;
public function list(array $filters = []): Collection;
public function paginate(int $page = 1, int $perPage = 20, array $filters = []): Collection;
/**
* @throws \VelvetCMS\Exceptions\NotFoundException
*/
public function delete(string $slug): bool;
public function exists(string $slug): bool;
public function count(array $filters = []): int;
public function lastModified(string $slug): ?int;
}
Methods #
load() #
Load a page by its slug.
public function load(string $slug): Page
| Parameter | Type | Description |
|---|---|---|
$slug |
string |
The page slug |
Returns: Page instance
Throws: NotFoundException if page doesn't exist
Example:
$driver = app('content.driver');
try {
$page = $driver->load('welcome');
echo $page->title;
} catch (NotFoundException $e) {
// Handle missing page
}
save() #
Save a page (create or update).
public function save(Page $page): bool
| Parameter | Type | Description |
|---|---|---|
$page |
Page |
The page to save |
Returns: true on success
Throws: ValidationException if page data is invalid
Example:
$page = new Page();
$page->slug = 'new-page';
$page->title = 'New Page';
$page->content = '# Hello World';
$page->status = 'draft';
$driver->save($page);
list() #
Get all pages matching filters.
public function list(array $filters = []): Collection
| Parameter | Type | Description |
|---|---|---|
$filters |
array |
Optional filter criteria |
Returns: Collection of Page objects
Example:
// All pages
$pages = $driver->list();
// Published pages only
$published = $driver->list(['status' => 'published']);
// With ordering
$recent = $driver->list([
'order_by' => 'updated_at',
'order_dir' => 'desc',
'limit' => 10,
]);
paginate() #
Get paginated pages.
public function paginate(int $page = 1, int $perPage = 20, array $filters = []): Collection
| Parameter | Type | Default | Description |
|---|---|---|---|
$page |
int |
1 |
Page number |
$perPage |
int |
20 |
Items per page |
$filters |
array |
[] |
Filter criteria |
Returns: Collection of Page objects
Example:
$pages = $driver->paginate(2, 15, ['status' => 'published']);
delete() #
Delete a page by slug.
public function delete(string $slug): bool
| Parameter | Type | Description |
|---|---|---|
$slug |
string |
The page slug |
Returns: true on success
Throws: NotFoundException if page doesn't exist
Example:
$driver->delete('old-page');
exists() #
Check if a page exists.
public function exists(string $slug): bool
Example:
if ($driver->exists('about')) {
$page = $driver->load('about');
}
count() #
Count pages matching filters.
public function count(array $filters = []): int
Example:
$total = $driver->count();
$published = $driver->count(['status' => 'published']);
lastModified() #
Return the last modified timestamp for a page, or null when the page does not exist.
$mtime = $driver->lastModified('about');
Filters #
The built-in file driver accepts these indexed filters:
| Filter | Type | Description |
|---|---|---|
status |
string |
'draft' or 'published' |
order_by |
string |
created_at, updated_at, published_at, slug, title, or status |
order_dir |
string |
'asc' or 'desc' |
limit |
int |
Maximum results |
offset |
int |
Skip first N results |
Example:
$pages = $driver->list([
'status' => 'published',
'order_by' => 'created_at',
'order_dir' => 'desc',
'limit' => 5,
]);
Built-in Driver #
FileDriver #
Stores pages as files in user/content/pages/.
Namespace: VelvetCMS\Drivers\Content\FileDriver
Features:
- Loads
.vltfiles first, then.md - Uses YAML frontmatter for metadata
- Maintains a page index through the
PageIndexabstraction - Supports JSON and SQLite index backends
- Keeps write operations file-native while making reads and lists efficient
File Structure:
---
title: Welcome
status: published
created_at: 2026-01-01
---
# Welcome to VelvetCMS
Your content here...
Configuration:
// config/content.php
'drivers' => [
'file' => [
'path' => content_path('pages'),
'index' => [
'driver' => 'json',
],
],
],
Custom implementations #
If you want a different storage backend, implement ContentDriver and override the container binding for content.driver.
use VelvetCMS\Contracts\ContentDriver;
$app->singleton('content.driver', fn () => new MyContentDriver(...));
$app->alias('content.driver', ContentDriver::class);
PageService and the rest of Core resolve the driver through the contract, so your custom implementation can replace the default cleanly.
Creating a Custom Driver #
Implement ContentDriver end-to-end. The sketch below uses HttpClient with structured options (query / json) and send() for verbs without sugar helpers (for example HEAD):
use VelvetCMS\Contracts\ContentDriver;
use VelvetCMS\Database\Collection;
use VelvetCMS\Exceptions\NotFoundException;
use VelvetCMS\Http\Client\HttpClient;
use VelvetCMS\Models\Page;
final class ApiDriver implements ContentDriver
{
public function __construct(
private readonly HttpClient $client,
private readonly string $baseUrl,
) {}
private function endpoint(string $path): string
{
return rtrim($this->baseUrl, '/') . '/' . ltrim($path, '/');
}
public function load(string $slug): Page
{
$response = $this->client->get($this->endpoint("/pages/{$slug}"));
if ($response->status() === 404) {
throw new NotFoundException("Page '{$slug}' not found");
}
/** @var array<string, mixed> $payload */
$payload = $response->json();
return Page::fromArray($payload);
}
public function save(Page $page): bool
{
$response = $this->client->put($this->endpoint("/pages/{$page->slug}"), [
'json' => $page->toArray(),
]);
return $response->successful();
}
/** @param array<string, mixed> $filters */
public function list(array $filters = []): Collection
{
$response = $this->client->get($this->endpoint('/pages'), [
'query' => $filters,
]);
/** @var list<array<string, mixed>> $rows */
$rows = $response->json();
$pages = array_map(
static fn (array $row) => Page::fromArray($row),
$rows,
);
return new Collection($pages);
}
public function paginate(int $page = 1, int $perPage = 20, array $filters = []): Collection
{
return $this->list(array_merge($filters, [
'page' => $page,
'per_page' => $perPage,
]));
}
public function delete(string $slug): bool
{
$response = $this->client->delete($this->endpoint("/pages/{$slug}"));
if ($response->status() === 404) {
throw new NotFoundException("Page '{$slug}' not found");
}
return $response->successful();
}
public function exists(string $slug): bool
{
$response = $this->client->send('HEAD', $this->endpoint("/pages/{$slug}"));
return $response->status() === 200;
}
/** @param array<string, mixed> $filters */
public function count(array $filters = []): int
{
$response = $this->client->get($this->endpoint('/pages/count'), [
'query' => $filters,
]);
/** @var array<string, mixed> $payload */
$payload = $response->json();
return (int) ($payload['count'] ?? 0);
}
public function lastModified(string $slug): ?int
{
$response = $this->client->send('HEAD', $this->endpoint("/pages/{$slug}"));
if ($response->status() === 404) {
return null;
}
$header = $response->header('Last-Modified');
return $header !== null ? (strtotime($header) ?: null) : null;
}
}
Register Custom Driver #
use VelvetCMS\Contracts\ContentDriver;
use VelvetCMS\Http\Client\HttpClient;
$app->singleton('content.driver', fn ($app) => new ApiDriver(
$app->make(HttpClient::class),
(string) config('content.drivers.api.url'),
));
$app->alias('content.driver', ContentDriver::class);
Usage with PageService #
The PageService wraps content drivers with caching and events:
// Direct driver access (no caching)
$driver = app('content.driver');
$page = $driver->load('welcome');
// Via PageService (with caching and events)
$pages = app('pages');
$page = $pages->load('welcome'); // Cached for 300s
See PageService for the recommended high-level API.