Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
release:
uses: saucebase-dev/saucebase/.github/workflows/semantic-release.yml@main
with:
version_file: 'module.json'
version_file: 'composer.json'
version_file_path: '.version'
prerelease: false
secrets:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ jobs:
test:
uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main
with:
module: Blog
module: blog
dependencies: saucebase/auth
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ When adding a field visible on the frontend, update **all three** in sync:
php -d memory_limit=2048M artisan test --testsuite=Modules --filter='^Modules\\Blog\\Tests'

# E2E
npx playwright test --project="@Blog*"
npx playwright test --project="@blog*"
```

---
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Saucebase
Copyright (c) 2026 Saucebase

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
10 changes: 8 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ tasks:

test:e2e:
desc: Run E2E tests for Blog module
cmd: npx playwright test --project="@Blog*" {{.CLI_ARGS}}
cmd: npx playwright test --project="@blog*" {{.CLI_ARGS}}
interactive: true

# ── Database ──────────────────────────────────────────────────

db:seed:
desc: Seed the Blog module database
cmd: php artisan modules:seed --module=blog

# ── Code Generation ────────────────────────────────────────────

types:generate:
desc: Generate TypeScript types from PHP DTOs and enums
cmd: php artisan module:generate-types Blog
cmd: php artisan module:generate-types blog
16 changes: 0 additions & 16 deletions app/Providers/BlogServiceProvider.php

This file was deleted.

17 changes: 0 additions & 17 deletions app/Providers/RouteServiceProvider.php

This file was deleted.

13 changes: 9 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "saucebase/blog",
"description": "",
"description": "Blog module",
"type": "saucebase-module",
"license": "proprietary",
"version": "0.1.0",
Comment on lines +3 to +6
"authors": [
{
"name": "Saucebase",
Expand All @@ -10,13 +12,15 @@
],
"extra": {
"laravel": {
"providers": [],
"providers": [
"Modules\\Blog\\Providers\\BlogServiceProvider"
],
"aliases": {}
}
},
"autoload": {
"psr-4": {
"Modules\\Blog\\": "app/",
"Modules\\Blog\\": "src/",
"Modules\\Blog\\Database\\Factories\\": "database/factories/",
"Modules\\Blog\\Database\\Seeders\\": "database/seeders/"
}
Expand All @@ -25,5 +29,6 @@
"psr-4": {
"Modules\\Blog\\Tests\\": "tests/"
}
}
},
"minimum-stability": "stable"
}
48 changes: 22 additions & 26 deletions database/seeders/BlogDatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ public function run(): void

$content = fn (string $file): string => file_get_contents(__DIR__.'/content/'.$file);

Post::create([
$this->createPost([
'title' => 'What Is Saucebase? The Modular Laravel Starter Kit',
'excerpt' => 'Every SaaS starts the same way: weeks of setup before you write a single line of real product code. Saucebase is the modular Laravel starter kit that skips that part.',
'content' => $content('post-1-what-is-saucebase.html'),
'status' => PostStatus::Published,
'published_at' => now()->subMonths(5),
'category_id' => $gettingStarted->id,
'author_id' => $author?->id,
]);
], public_path('images/blog/what-is-saucebase.jpg'));

Post::create([
$this->createPost([
'title' => 'Stop Rebuilding the Same Boilerplate Every Project',
'excerpt' => "If you've launched more than one Laravel app, you know the feeling — two weeks gone before you write any real product code. There's a better way.",
'content' => $content('post-2-stop-rebuilding-boilerplate.html'),
Expand All @@ -40,59 +40,55 @@ public function run(): void
'author_id' => $author?->id,
]);

Post::create([
'title' => 'The VILT Stack: Laravel, Vue, Inertia, and Tailwind Done Right',
'excerpt' => "Modern web dev has a tension between server productivity and rich interactivity. The VILT stack resolves it — and Saucebase wires all four pieces together so you don't have to.",
$this->createPost([
'title' => 'The Modern Laravel Stack: Inertia.js, Vue 3 or React, and Tailwind Done Right',
'excerpt' => "Modern web dev has a tension between server productivity and rich interactivity. Laravel, Inertia.js, and Tailwind resolve it — and Saucebase wires all the pieces together so you don't have to.",
'content' => $content('post-3-vilt-stack.html'),
'status' => PostStatus::Published,
'published_at' => now()->subMonths(3),
'category_id' => $devExperience->id,
'author_id' => $author?->id,
]);
], public_path('images/blog/tech-stack.jpg'));

$post4 = Post::create([
$this->createPost([
'title' => 'Your First Module: Scaffold and Ship in Under 10 Minutes',
'excerpt' => "One Artisan command scaffolds a fully structured module — service provider, controller, routes, Vue pages, migrations, factory, tests, and Vite config. Here's how it works.",
'content' => $content('post-4-your-first-module.html'),
'status' => PostStatus::Published,
'published_at' => now()->subMonths(2),
'category_id' => $featuresModules->id,
'author_id' => $author?->id,
]);
], public_path('images/blog/add-your-saucebase.jpg'));

$image4 = public_path('images/blog/add-your-saucebase.jpg');
if (file_exists($image4)) {
$post4->addMedia($image4)->preservingOriginal()->toMediaCollection('cover');
}

$post5 = Post::create([
$this->createPost([
'title' => 'Auth, Billing, and Privacy: The Three Modules Every SaaS Needs',
'excerpt' => "Three modules every SaaS needs — and Saucebase ships them already built. Here's what's included and why they matter.",
'content' => $content('post-5-auth-billing-privacy.html'),
'status' => PostStatus::Published,
'published_at' => now()->subMonth(),
'category_id' => $featuresModules->id,
'author_id' => $author?->id,
]);
], public_path('images/blog/cookies-or-privacy.jpg'));

$image5 = public_path('images/blog/cookies-or-privacy.jpg');
if (file_exists($image5)) {
$post5->addMedia($image5)->preservingOriginal()->toMediaCollection('cover');
}

$post6 = Post::create([
$this->createPost([
'title' => 'Copy-and-Own: The Philosophy Behind Saucebase Modules',
'excerpt' => "Most starter kits give you a locked box. Saucebase takes the opposite approach: when you install a module, the source code lands in your repo and it's yours to read, edit, and own.",
'content' => $content('post-6-copy-and-own.html'),
'status' => PostStatus::Published,
'published_at' => now()->subWeeks(2),
'category_id' => $devExperience->id,
'author_id' => $author?->id,
]);
], public_path('images/blog/your-recipes.jpg'));
}

$image6 = public_path('images/blog/your-recipes.jpg');
if (file_exists($image6)) {
$post6->addMedia($image6)->preservingOriginal()->toMediaCollection('cover');
private function createPost(array $data, string $coverImage = ''): Post
{
$post = Post::create($data);

if ($coverImage && file_exists($coverImage)) {
$post->addMedia($coverImage)->preservingOriginal()->toMediaCollection('cover');
}

return $post;
}
}
13 changes: 13 additions & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Modules\Blog\Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call(BlogDatabaseSeeder::class);
}
}
4 changes: 2 additions & 2 deletions database/seeders/content/post-1-what-is-saucebase.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

<p>Saucebase exists to solve that problem.</p>

<p>Saucebase is a modular Laravel SaaS starter kit built on the <strong>VILT stack</strong>: Vue 3, Inertia.js, Laravel 13, and Tailwind CSS 4. It ships with the infrastructure every SaaS needs and gets it out of your way so you can focus on building what makes your product unique.</p>
<p>Saucebase is a modular Laravel SaaS starter kit built on Laravel 13, Inertia.js, and Tailwind CSS 4 — with your choice of Vue 3 or React. It ships with the infrastructure every SaaS needs and gets it out of your way so you can focus on building what makes your product unique.</p>

<p>The core of Saucebase is its module system, powered by <code>nwidart/laravel-modules</code>. Features are packaged as self-contained modules — Auth, Billing, Settings, Blog — each with their own routes, controllers, models, migrations, and Vue pages. Enable what you need, disable what you don't.</p>
<p>The core of Saucebase is its module system, powered by <code>internachi/modular</code>. Features are packaged as self-contained modules — Auth, Billing, Settings, Blog — each with their own routes, controllers, models, migrations, and frontend pages. Modules are active when installed via Composer and live directly in your repository.</p>

<p>But here's what sets Saucebase apart: modules are <strong>copy-and-own</strong>. When you install a module, you own it. It lives in your repository. You can edit it, extend it, break it, and fix it. There are no locked vendor packages to work around, no version conflicts to manage. Just clean, readable Laravel code that does exactly what you'd write yourself — except already written.</p>

Expand Down
10 changes: 5 additions & 5 deletions database/seeders/content/post-3-vilt-stack.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<p>Modern web development has a tension at its core: server-side frameworks are productive but struggle with rich interactivity, while JavaScript SPAs offer great UX but introduce enormous complexity. The VILT stack resolves that tension elegantly — and Saucebase is built on it from the ground up.</p>
<p>Modern web development has a tension at its core: server-side frameworks are productive but struggle with rich interactivity, while JavaScript SPAs offer great UX but introduce enormous complexity. The Laravel + Inertia.js stack resolves that tension elegantly — and Saucebase is built on it from the ground up.</p>

<h2>Laravel — the foundation</h2>

<p>Version 13, with PHP 8.4 constructor promotion, typed properties, Eloquent relationships, and a world-class ecosystem. Laravel handles routing, database access, authentication, queuing, broadcasting, and more — all with conventions that keep your codebase organised as it grows.</p>

<h2>Inertia.js — the bridge</h2>

<p>Version 3 replaces traditional Blade views with Vue components while keeping your server-side routing and controllers intact. No REST API, no GraphQL, no data-fetching layer to maintain. Your controller returns a component name and props. Inertia handles the rest.</p>
<p>Version 3 replaces traditional Blade views with Vue or React components while keeping your server-side routing and controllers intact. No REST API, no GraphQL, no data-fetching layer to maintain. Your controller returns a component name and props. Inertia handles the rest.</p>

<p>Version 3 brings deferred props, optimistic updates with automatic rollback, instant visits, and simplified SSR via the Vite plugin — all of which Saucebase uses by default.</p>

<h2>Vue 3 — the UI layer</h2>
<h2>Vue 3 or React — the UI layer</h2>

<p>Composition API, <code>&lt;script setup&gt;</code>, and TypeScript throughout. Pages live in <code>resources/js/pages/</code>, components in <code>resources/js/components/</code>. Types are auto-generated from your PHP enums and DTOs via <code>php artisan module:generate-types</code> — your backend and frontend stay in sync without manual work.</p>
<p>Saucebase ships with full support for both frameworks. Pick Vue 3 (Composition API, <code>&lt;script setup&gt;</code>) or React 19 (hooks, JSX) — the choice is made during setup and everything is wired up for you. TypeScript is used throughout either way, and types are auto-generated from your PHP enums and DTOs via <code>php artisan module:generate-types</code> — your backend and frontend stay in sync without manual work.</p>

<h2>Tailwind CSS 4 — the styling layer</h2>

Expand All @@ -21,7 +21,7 @@ <h2>Tailwind CSS 4 — the styling layer</h2>
<h2>The full picture</h2>

<ul>
<li><strong>Vite 6.4</strong> — HMR, SSR, and module-aware asset discovery</li>
<li><strong>Vite</strong> — HMR, SSR, and module-aware asset discovery</li>
<li><strong>Filament 5</strong> — admin panel</li>
<li><strong>Ziggy</strong> — named Laravel routes available in JavaScript</li>
<li><strong>Playwright</strong> — end-to-end tests, auto-discovered per module</li>
Expand Down
11 changes: 4 additions & 7 deletions database/seeders/content/post-4-your-first-module.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@

<h2>Enabling your module</h2>

<p>After scaffolding, three more commands and your module is live:</p>
<p>After scaffolding, run your migrations and rebuild assets:</p>

<pre><code>composer dump-autoload
php artisan module:enable ModuleName
php artisan module:migrate ModuleName --seed</code></pre>

<p>Then rebuild assets with <code>npm run build</code> or restart <code>npm run dev</code>.</p>
<pre><code>php artisan migrate
npm run build</code></pre>

<h2>What you get for free</h2>

<ul>
<li>Service provider registered and booted automatically</li>
<li>Routes loaded and scoped to the module</li>
<li>Filament plugin ready for your admin resources</li>
<li>Vue pages discoverable by Inertia out of the box</li>
<li>Vue or React pages discoverable by Inertia out of the box</li>
<li>TypeScript types generated via <code>php artisan module:generate-types</code></li>
<li>Playwright config auto-discovered by <code>module-loader.js</code></li>
</ul>
Expand Down
2 changes: 0 additions & 2 deletions database/seeders/content/post-5-auth-billing-privacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ <h2>Auth module</h2>
<li>Registration, login, and logout</li>
<li>Email verification and password reset</li>
<li>Social login via Laravel Socialite</li>
<li>Two-factor authentication</li>
<li>Role-based access with Spatie Laravel Permission (<code>admin</code> and <code>user</code> seeded by default)</li>
</ul>

Expand All @@ -33,7 +32,6 @@ <h2>Settings module</h2>
<li>Profile editing and avatar upload</li>
<li>Email change with re-verification</li>
<li>Password management</li>
<li>Notification preferences</li>
</ul>

<h2>Privacy and compliance</h2>
Expand Down
2 changes: 1 addition & 1 deletion database/seeders/content/post-6-copy-and-own.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h2>What copy-and-own means</h2>

<h2>The practical benefit</h2>

<p>When a bug appears in your authentication flow, you look at <code>modules/Auth/app/Http/Controllers/</code>. When you want to add a field to the billing portal, you edit <code>modules/Billing/resources/js/pages/</code>. There's no mental overhead of figuring out which hook to use or which config key controls the behaviour you need. You find the code and change it.</p>
<p>When a bug appears in your authentication flow, you look at <code>modules/auth/app/Http/Controllers/</code>. When you want to add a field to the billing portal, you edit <code>modules/billing/resources/js/pages/</code>. There's no mental overhead of figuring out which hook to use or which config key controls the behaviour you need. You find the code and change it.</p>

<h2>The tradeoff</h2>

Expand Down
15 changes: 0 additions & 15 deletions module.json

This file was deleted.

12 changes: 1 addition & 11 deletions resources/js/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
import { registerIcon } from '@/lib/navigation';

import '../css/style.css';

import IconBlog from '~icons/heroicons/newspaper';

export function setup() {
registerIcon('blog', IconBlog);
}

export function afterMount() {}
export * from './vue/app';
2 changes: 1 addition & 1 deletion resources/js/types/page-props.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Augmentations here are auto-picked up when the module is enabled (no core edits needed).
// See docs: architecture/module-system#typescript-page-props

// declare module '@inertiajs/vue3' {
// declare module '@inertiajs/core' {
// interface PageProps {
// // my_shared_prop?: MyType;
// }
Expand Down
11 changes: 11 additions & 0 deletions resources/js/vue/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { registerIcon } from '@/lib/navigation';

import '@modules/blog/resources/css/style.css';

import IconBlog from '~icons/heroicons/newspaper';

export function setup() {
registerIcon('blog', IconBlog);
}

export function afterMount() {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ defineProps<{
<section class="mx-auto w-full max-w-6xl px-6 py-24">
<div class="mb-12 flex items-end justify-between">
<div>
<h2 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
<h2
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white"
>
{{ $t('From the blog') }}
</h2>
<p class="mt-2 text-lg text-gray-500 dark:text-gray-400">
Expand Down
Loading
Loading