cnvs/canvas
Canvas is a lightweight Laravel publishing platform for creating and managing a beautiful blog with ease. It offers a clean admin interface, Markdown writing, tags, customizable templates, and SEO-friendly pages, all seamlessly integrated into your Laravel application.
Canvas is a self-hosted publishing package: it ships an admin SPA at a configurable path plus a PHP API, but it does not render your public site. You install it, write/publish posts in the admin UI or via Eloquent, then surface that content from your own routes and Blade views.
Minimal install (4 commands + 1 visit):
composer require austintoddj/canvas
php artisan canvas:install # publishes config, migrations, assets
php artisan migrate
php artisan canvas:user # create your first admin
Then hit /canvas (default path). The admin SPA is served from public/vendor/canvas.
Where to look first:
| Need | Look at |
|---|---|
| Host-facing how-tos | docs/ in the package (install, config, auth, UI, content, webhooks) — readme.md is just a flyer |
| Every knob you can change | config/canvas.php after publishing (config/canvas.php in vendor/cnvs/canvas) |
| Breaking changes between versions | .github/UPGRADE.md |
| What posts actually are | Canvas\Models\Post, Canvas\Enums\Role, database/migrations/ |
First real use case: publish a post in the admin, then read it back on your own front end:
use Canvas\Models\Post;
Route::get('/blog/{slug}', function (string $slug) {
$post = Post::query()
->where('slug', $slug)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->firstOrFail();
return view('blog.show', compact('post'));
});
If that query returns nothing, the post is a draft or scheduled — check published_at before blaming the route.
Canvas gives you data, not templates. The typical workflow:
user, tags/topics) to avoid N+1.<title>, canonical, OG tags from your own controller/view. Canvas stores post meta, but the <head> is yours.Keep the "published" filter in one place (a local scope or query object) so list, detail, sitemap, and feed can't drift.
Canvas models are ordinary Eloquent models, so fixtures, migrations from another CMS, and bulk imports are straightforward:
Post::create([
'title' => 'Hello',
'slug' => 'hello',
'summary' => '…',
'body' => $html,
'published_at' => now(),
'user_id' => $admin->id,
]);
For anything non-trivial, write a one-off artisan command in your app rather than clicking through the SPA. Wrap imports in a transaction and slug-deduplicate up front.
Canvas expects your App\Models\User (or whatever you point the guard at) and roles: Contributor / Editor / Admin (Canvas\Enums\Role). Two integration shapes:
canvas:user, done.Canvas\Concerns\HasCanvasAccess trait to your User model and map your own role column or gate into Canvas's role checks. Decide this before you ship, because retrofitting role sources means auditing every policy and middleware.Also confirm the auth guard in config/canvas.php matches the guard your host app actually logs people in with. Mismatched guards produce "logged in but kicked out of /canvas" symptoms.
php artisan canvas:ui publishes the SPA stubs/views so you can adjust layout, branding, or add small extensions. Prefer this over editing vendor/. Remember the shipped resources/dist/ build is committed — if you touch JS, you own the rebuild step in your deploy pipeline.
Canvas supports webhooks (plus integrations like Unsplash, AI writing, and a weekly digest). Treat webhook delivery as best-effort: accept the delivery, persist it, process in a queued job, and make the handler idempotent (deliveries retry). Store the signing secret in .env and never in config/canvas.php committed to git.
UI strings live in resources/lang/{locale}/app.php — 17 locales, en authoritative. Publishing those lang files lets you reword labels for your editors without touching the React build. If you add a language, mirror the full key set; partial catalogs produce mixed-language UI.
canvas:install is not idempotent in spirit. It publishes config, migrations, and assets. Re-running after an upgrade is normal, but use --force deliberately — it will overwrite any edits you made to published stubs.public/vendor/canvas is a copy, not a symlink. After composer update, re-publish assets or the SPA will talk to an API that changed underneath it. Bake this into your deploy script.config:cache bites. Any change to path, guard, or disks requires php artisan config:clear (or a fresh deploy). A stale config cache is the #1 cause of "route 404 / wrong middleware" confusion./canvas path can shadow a host route or get shadowed by a catch-all. Change path in config rather than fighting route order; also check domain if you're multi-tenant.{!! !!}.storage:link. Featured images land on the configured disk; check config/canvas.php disk settings and that the public URL resolves on your deploy target (S3 vs. local differs)..github/UPGRADE.md before every minor bump; new migrations may add columns that your custom queries or imports must populate.canvas:ui stubs + lang files cover nearly every branding/localization need; a fork costs you every future upgrade.How can I help you explore Laravel packages today?