Configuration
Every key has a default and the common ones are env-backed, so the package works without a published config. The published config/posts.php in full:
return [
'model' => RoundlyConsulting\Posts\Models\Post::class,
'key_type' => env('POSTS_KEY_TYPE', 'bigint'), // your author model's key
'primary_key_type' => env('POSTS_PRIMARY_KEY_TYPE', 'bigint'), // the posts tables' own ids
'tables' => [
'posts' => 'posts',
'categories' => 'post_categories',
'category_post' => 'category_post',
'tags' => 'post_tags',
'tag_post' => 'post_tag',
],
'author' => [
'morph-name' => 'author',
'nullable' => true,
],
'locales' => [
'fallback' => env('POSTS_FALLBACK_LOCALE', config('app.fallback_locale', 'en')),
],
'slugs' => [
'source' => 'title',
'separator' => '-',
'unique' => true,
'route-binding' => true,
'history' => env('POSTS_SLUG_HISTORY', false),
'lock-when-published' => false,
],
'seo' => [
'site-name' => env('POSTS_SITE_NAME'),
'twitter-site' => env('POSTS_TWITTER_SITE'),
'default-card' => 'summary_large_image', // 'summary' | 'summary_large_image' | 'app' | 'player'
'default-robots' => 'index,follow',
],
'json-ld' => [
'type' => 'BlogPosting', // 'BlogPosting' | 'Article' (anything else throws)
'author-attribute' => 'name',
'publisher' => [
'name' => env('POSTS_PUBLISHER_NAME'),
'logo' => env('POSTS_PUBLISHER_LOGO'),
],
],
'media' => [
'featured_bucket' => 'featured',
'gallery_bucket' => 'gallery',
'content_bucket' => 'content',
'disk' => env('POSTS_MEDIA_DISK'),
'featured_fallback_url' => env('POSTS_MEDIA_FEATURED_FALLBACK'),
'responsive_widths' => null,
'seo_og_image' => true,
'og_variant' => '',
'warm_on_publish' => true,
'inline' => [
'enabled' => true,
'default_variant' => '',
'on_missing' => 'strip', // 'strip' | 'keep' (anything else throws)
],
],
'moderation' => [
'on_resolved' => 'archive', // 'archive' | 'draft' | null — anything else throws
'auto_unpublish' => true,
],
];Every key
| Key | Default | Env | Purpose |
|---|---|---|---|
model | Post::class | — | The post model — point it at your subclass to extend it. |
key_type | bigint | POSTS_KEY_TYPE | Outbound: your author model’s key type (bigint, uuid or ulid); types author_id. Anything else throws InvalidConfigurationException. |
primary_key_type | bigint | POSTS_PRIMARY_KEY_TYPE | Inbound: the key type of the posts, categories and tags tables and their pivots (bigint, uuid or ulid). Anything else throws InvalidConfigurationException. |
tables.posts | posts | — | Posts table. |
tables.categories | post_categories | — | Categories table. |
tables.category_post | category_post | — | Category ⇄ post pivot. |
tables.tags | post_tags | — | Tags table. |
tables.tag_post | post_tag | — | Tag ⇄ post pivot. |
author.morph-name | author | — | Morph relation name and column prefix (author_type / author_id). |
author.nullable | true | — | Whether a post may have no author. |
locales.fallback | app.fallback_locale | POSTS_FALLBACK_LOCALE | Fallback locale for translation reads and the slug chain. |
slugs.source | title | — | Attribute post slugs are generated from (categories and tags always use name). |
slugs.separator | - | — | Slug word separator. |
slugs.unique | true | — | Unique per locale (-2, -3, …, trashed rows included); at migrate time also builds the per-locale unique indexes. |
slugs.route-binding | true | — | The slug is the post’s route key; {post} binds along the locale chain. |
slugs.history | false | POSTS_SLUG_HISTORY | Remember retired post slugs and 301 old URLs (needs sluggable-migrations). |
slugs.lock-when-published | false | — | Freeze a published post’s slugs; a manual change throws SlugLockedException. |
seo.site-name | null | POSTS_SITE_NAME | Default og:site_name. |
seo.twitter-site | null | POSTS_TWITTER_SITE | Default twitter:site handle. |
seo.default-card | summary_large_image | — | Default twitter:card type: summary, summary_large_image, app or player; anything else throws. |
seo.default-robots | index,follow | — | Default robots directive. |
json-ld.type | BlogPosting | — | schema.org @type: BlogPosting or Article; anything else throws. |
json-ld.author-attribute | name | — | Author-model attribute used as the author name. |
json-ld.publisher.name | null | POSTS_PUBLISHER_NAME | Publisher organisation name (publisher omitted when unset). |
json-ld.publisher.logo | null | POSTS_PUBLISHER_LOGO | Publisher logo URL. |
media.featured_bucket | featured | — | Single-file featured-image bucket. |
media.gallery_bucket | gallery | — | Multi-file gallery bucket. |
media.content_bucket | content | — | Bucket owning the media referenced inline by [media:UUID]. |
media.disk | null | POSTS_MEDIA_DISK | Disk for post media (null = media-library default). |
media.featured_fallback_url | null | POSTS_MEDIA_FEATURED_FALLBACK | URL featuredImageUrl() returns when no featured image is set (null = ''). |
media.responsive_widths | null | — | Responsive width ladder of positive integers (null = media-library default, [] = no variants); any other entry throws. |
media.seo_og_image | true | — | Fall back og:image and the JSON-LD image to the featured image. |
media.og_variant | '' | — | Variant used for that fallback ('' = the original) — one media-library generates for the featured bucket, e.g. responsive-640; until it exists, or for an unknown name, the original’s URL is used. |
media.warm_on_publish | true | — | Queue variant generation for the post’s media on publish. |
media.inline.enabled | true | — | Expand [media:UUID] tokens in renderContent(). |
media.inline.default_variant | '' | — | Variant for tokens without |variant; like a token’s own, an unknown or not-yet-generated one renders the original image. |
media.inline.on_missing | strip | — | strip or keep a token whose media the post does not own; anything else throws. |
moderation.on_resolved | archive | — | Action when a report against a published or scheduled post is upheld: archive, draft, or null or blank (off). A typo throws instead of skipping the auto-unpublish. |
moderation.auto_unpublish | true | — | Archive a published or scheduled post that crosses the global reports.threshold. |
Environment
The site-specific values are env-driven, so most apps never publish the config:
POSTS_KEY_TYPE=bigint
POSTS_PRIMARY_KEY_TYPE=bigint
POSTS_FALLBACK_LOCALE=en
POSTS_SLUG_HISTORY=false
POSTS_SITE_NAME="Example Blog"
POSTS_TWITTER_SITE=@example
POSTS_PUBLISHER_NAME="Example Ltd"
POSTS_PUBLISHER_LOGO=https://example.com/logo.png
POSTS_MEDIA_DISK=public
POSTS_MEDIA_FEATURED_FALLBACK=https://example.com/images/placeholder.pngStrict values
Every bool switch is read strictly: true/1/on/yes turn it on, false/0/off/no turn it off, a blank value (POSTS_SLUG_HISTORY=) is not set so the default applies, and anything else (say POSTS_SLUG_HISTORY=disabled) throws RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException instead of quietly reading as the default.
Every other setting is just as strict. A setting that is not set — absent, null, or blank like a host’s POSTS_SITE_NAME= — takes its default, and an optional one (the SEO / JSON-LD publisher values, the media disk or fallback URL) stays unset. A string setting — a table or bucket name, the morph name, the slug source or separator, a locale, an SEO or JSON-LD value, the media disk or fallback URL — must be a string when set: a non-string value throws rather than being cast to '' or replaced by the default. A seo.default-card, json-ld.type, media.inline.on_missing or moderation.on_resolved value outside its list throws, and so does a responsive width that isn’t a positive integer. The variant names — media.og_variant and media.inline.default_variant — take any string, '' meaning the original.
Checking the setup
php artisan aboutphp artisan about gets a Posts section: the model’s base name, both key types, whether an author is optional, the table count (DEFAULT or CUSTOMISED — a blank table name counts as the default the models use, a junk one shows INVALID), route binding, unique slugs, slug history and lock, the media disk (SET or MEDIA DEFAULT), inline media, warm-on-publish, SEO and JSON-LD presence, and the moderation switches. It never prints a configured value such as a table name, disk, publisher or site name. A broken media disk, inline-media, SEO, JSON-LD or moderation setting shows as INVALID.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.