dashed/dashed-articles
A Laravel package that adds article management to Dashed applications, providing models, migrations, and CRUD workflows for creating, editing, organizing, and publishing content within existing Dashed projects.
This is a Dashed CMS module (zero stars, zero dependents, MIT), so treat the README as a starting hint rather than documentation and plan to read source. Expect a small package: a service provider, an Eloquent model, migrations, routes, and Blade views.
Install and wire up
composer require dashed/dashed-articles
php artisan vendor:publish --provider="Dashed\Articles\ArticlesServiceProvider"
php artisan migrate
Check composer.json first for a dashed/* core dependency — you almost certainly need the base CMS installed and its admin panel registered before this module does anything visible. Laravel package auto-discovery should register the provider; if the admin UI doesn't appear, add it explicitly to bootstrap/providers.php (Laravel 11+) or config/app.php.
Where to look first, in order
src/*ServiceProvider.php — what it publishes, what it binds into the container, which routes/migrations/views it loads. This single file tells you 80% of the package's shape.config/articles.php (after publishing) — model class bindings, route prefixes, middleware, pagination, disk for media. This is where you override behavior without forking.database/migrations/ — the schema is the real API contract. Note column names, nullability, and any foreign keys before you write queries against it.src/Models/ and src/Http/ — scopes, relationships, and controller actions.routes/ — whether routes are published (editable) or loaded from the package (not editable).First use case
Create one article through the admin panel, then render it on the front end with route-model binding by slug. That end-to-end pass exposes slug generation, published-state scoping, and view resolution in one go — the three things most likely to be configured differently than you assume.
$article = Article::published()->where('slug', $slug)->firstOrFail();
If published() doesn't exist, check the model for whatever the real scope is named (scopePublished, a Publishable trait, or a published_at cast) — don't assume.
Override the model via config, not by editing vendor code. Most Dashed modules resolve the model through the container or a config key. Subclass the shipped model, add your accessors/relationships, and rebind:
// config/articles.php
'models' => ['article' => App\Models\Article::class],
class Article extends \Dashed\Articles\Models\Article
{
public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
}
This survives composer update; a fork does not. Verify the binding is actually consulted (app(config('articles.models.article')) in tinker) before building on it.
Separate admin and public routing. Typical CMS shape: admin CRUD under an authenticated prefix, public read routes under a catch-all or slug segment. Keep your own presentation routes in routes/web.php pointing at your controllers, and let the package own only the admin surface. That keeps you free to change URLs without touching the module.
Cache the published list, invalidate on write. Article pages are read-heavy and rarely change:
// App\Observers\ArticleObserver
public function saved(Article $article): void
{
Cache::forget("articles.index.{$article->locale}");
Cache::forget("articles.show.{$article->slug}");
}
Register the observer on your subclass; if the package dispatches its own events (ArticleCreated, ArticleUpdated), listen to those instead so you catch writes that bypass your model.
Eager-load aggressively. An article index almost always touches author, tags/categories, and a cover image. A single with(['author', 'tags', 'media']) on the listing query is the difference between 3 queries and 60.
Wrap writes in an action class rather than calling Article::create() from a controller. Publishing usually involves slug generation, media attachment, and event dispatch; keeping that in App\Actions\PublishArticle makes it testable and gives you one place to hook when the package's own creation flow does something you don't expect.
Treat taxonomy as your integration seam. If the package ships tags/categories, sync them to your own taxonomy tables via events rather than querying the package's pivot tables from your views — that coupling is the hardest thing to unwind later.
Testing. The package likely ships no factories. Write your own against your subclass, and use RefreshDatabase with the package migrations included:
public function test_published_scope_excludes_drafts(): void
{
Article::factory()->create(['published_at' => null]);
Article::factory()->create(['published_at' => now()->subDay()]);
$this->assertCount(1, Article::published()->get());
}
That test is worth writing early — published-state semantics are the most common source of "why is my article invisible" tickets.
Zero stars, zero dependents means zero external answers. There is no StackOverflow corpus for this package, no blog posts, no third-party tutorials. Budget time to read the source, and pin the version in composer.json ("dashed/dashed-articles": "1.0.*" or an exact tag) rather than floating on ^. An unreviewed patch release on a package with no downstream users can break you silently.
Verify the release actually exists. The listed last-release date is 2026-03-31, which is ahead of the current date. Run composer show dashed/dashed-articles --all and confirm the tag resolves before you build a dependency on it; you may be looking at a dev branch or a mislabeled tag.
Migrations order matters. If your own tables add foreign keys to articles, timestamp your migrations after the package's, or you'll get intermittent failures on fresh installs that don't reproduce locally. Run php artisan migrate:fresh in CI, not just on your machine.
Slug regeneration silently breaks URLs. Many CMS models regenerate the slug on every save. If you change a title, inbound links 404. Either lock the slug after creation (check for a slug config flag or override booted()), or add a redirect table keyed on the old slug.
config:cache will freeze your model override. After changing the model binding in config, run php artisan config:clear in dev and remember to re-cache in deploy. This bites in the opposite direction too: a stale cached config in production can mean your subclass is ignored while local works fine.
Timezone drift on published_at. Scheduled publishing compares a stored timestamp against now(). If the column is stored in UTC but interpreted in app timezone, articles go live hours early or late. Check the cast and config('app.timezone') explicitly; write a test with $this->travelTo().
Route collisions with a catch-all. If public article routes use a {slug} catch-all, they will swallow other routes registered later. Register specific routes first, or constrain with ->where('slug', '[a-z0-9-]+').
Watch the N+1 in admin listings. CMS admin tables are the classic offender — the index shows an author name per row. Check with DB::listen() or Telescope on a 50-row page before shipping.
Extension points to prefer, in order: container bindings and config keys → model subclassing → events/listeners → macros → forking. Forking a package with no upstream activity means you own it forever; exhaust the first four.
Debugging recipes
php artisan route:list --name=articles # confirm what's registered and its middleware
php artisan about # confirm the provider was discovered
php artisan tinker --execute="dump(get_class(app(config('articles.models.article'))));"
That last one answers "is my override live?" in one line — run it first whenever behavior doesn't match your code.
How can I help you explore Laravel packages today?