Read @webhouse/cms content from a Laravel application. Helper class, routes, Blade templates.
Setup
Place your Laravel project and @webhouse/cms content side by side:
my-project/
cms.config.ts # Content model
content/ # JSON documents (read by Laravel)
public/uploads/ # Media files (served by Laravel)
app/ # Your Laravel app
resources/views/ # Blade templatesHelper class
Create app/Services/Webhouse.php:
<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Collection;
class Webhouse
{
public static function contentDir(): string
{
return base_path('content');
}
/**
* List all published documents in a collection.
*
* @param string $collection e.g. 'posts'
* @param string|null $locale e.g. 'en' — null means all locales
* @return Collection<int, array>
*/
public static function collection(string $collection, ?string $locale = null): Collection
{
$dir = self::contentDir() . '/' . $collection;
if (!File::isDirectory($dir)) {
return collect();
}
return collect(File::files($dir))
->filter(fn($f) => $f->getExtension() === 'json')
->map(fn($f) => json_decode(File::get($f->getPathname()), true))
->filter(fn($d) => ($d['status'] ?? null) === 'published')
->when($locale, fn($c) => $c->filter(fn($d) => ($d['locale'] ?? 'en') === $locale))
->sortByDesc(fn($d) => $d['data']['date'] ?? '')
->values();
}
/**
* Load a single document by slug.
*/
public static function document(string $collection, string $slug): ?array
{
$path = self::contentDir() . "/{$collection}/{$slug}.json";
if (!File::exists($path)) return null;
$doc = json_decode(File::get($path), true);
return ($doc['status'] ?? null) === 'published' ? $doc : null;
}
}Routes
// routes/web.php
use App\Services\Webhouse;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
$posts = Webhouse::collection('posts', 'en');
return view('home', ['posts' => $posts]);
});
Route::get('/blog/{slug}', function (string $slug) {
$post = Webhouse::document('posts', $slug);
abort_unless($post, 404);
return view('post', ['post' => $post]);
});Blade template
{{-- resources/views/post.blade.php --}}
@extends('layouts.app')
@section('content')
<article>
<h1>{{ $post['data']['title'] }}</h1>
<time>{{ $post['data']['date'] ?? '' }}</time>
<div class="prose">
{!! \Illuminate\Support\Str::markdown($post['data']['content'] ?? '') !!}
</div>
@foreach (($post['data']['tags'] ?? []) as $tag)
<a href="/tags/{{ $tag }}" class="tag">#{{ $tag }}</a>
@endforeach
</article>
@endsectionServing uploaded media
@webhouse/cms stores uploaded media in public/uploads/. Laravel already serves the public/ directory, so /uploads/my-image.jpg just works.
i18n: reading both locales
// Get all Danish posts
$daPosts = Webhouse::collection('posts', 'da');
// Get all English posts
$enPosts = Webhouse::collection('posts', 'en');
// Find the translation of a specific post
$post = Webhouse::document('posts', 'hello-world');
$translationGroup = $post['translationGroup'] ?? null;
$translation = Webhouse::collection('posts')
->firstWhere(fn($d) => ($d['translationGroup'] ?? null) === $translationGroup && ($d['locale'] ?? null) !== $post['locale']);Caching
For production, cache parsed JSON:
use Illuminate\Support\Facades\Cache;
public static function collection(string $collection, ?string $locale = null): Collection
{
return Cache::remember("webhouse:{$collection}:{$locale}", 60, function () use ($collection, $locale) {
// ... same logic as before
});
}Clear the cache when content changes — either via a file watcher or a CMS admin webhook.
Next steps
- See the Laravel example
- Read about i18n for multi-language patterns
- Learn about Framework-Agnostic Architecture