shazzoo / content-studio-laravel
Shows the blog articles from the Content Studio Strategy Engine on a Laravel site.
Package info
github.com/Shazzoo/content-studio-laravel-plugin
pkg:composer/shazzoo/content-studio-laravel
Requires
- php: ^8.3
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Shows the blog articles from the Content Studio Strategy Engine on a plain Laravel site. The package syncs the articles into your database every 15 minutes and serves an overview page and an article page.
Installation
composer require shazzoo/content-studio-laravel php artisan migrate php artisan vendor:publish --tag=content-studio-assets
The images from the Engine are stored on the public disk, so the site needs the storage symlink. Most Laravel projects already have it; if yours doesn't:
php artisan storage:link
The publish command copies the tracking script to public/vendor/content-studio/tracking.js. Without it the article pages ask for a file that isn't there and nothing is tracked.
Keeping the script up to date
The script is published under the laravel-assets tag as well, which the default Laravel skeleton republishes on every composer update:
"post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" ]
That covers composer update, but not composer install, and not a project whose composer.json lacks that script. To be sure, add this to your deploy:
php artisan vendor:publish --tag=content-studio-assets --force
A site that serves the file itself, through its own build or a CDN, can point the config at that URL instead and skip publishing:
'tracking' => [ 'script_url' => 'https://cdn.example.com/tracking.js', ],
Add the settings to .env:
CONTENT_STUDIO_API_KEY= CONTENT_STUDIO_PROJECT_CODE= CONTENT_STUDIO_ROUTE=blog CONTENT_STUDIO_ARTICLES_PER_PAGE=12
The sync runs through the Laravel scheduler, so make sure php artisan schedule:run runs every minute. To sync right away:
php artisan content-studio:sync
php artisan content-studio:sync --status=all # also fetch articles that are already live
An article whose images cannot be downloaded is not stored at all, so the site never shows a half-finished article; an article that is already live keeps the version it has. The command exits with an error, the article stays unconfirmed in the Engine's approved list, and the next sync downloads the images again. Images that did come through stay on disk, so a retry only fetches what is missing.
Routes
| URL | Name |
|---|---|
/{route} |
content-studio.index |
/{route}/{slug} |
content-studio.show |
/{route}/sitemap.xml |
content-studio.sitemap |
Set CONTENT_STUDIO_ROUTES=false to register the blog routes yourself; see Multiple languages.
Add the sitemap to your robots.txt or your own sitemap index:
Sitemap: https://example.com/blog/sitemap.xml
Latest articles on other pages
<x-content-studio::latest-articles :limit="3" title="From the blog" />
Without a title the heading and the "All articles" link are left out. The component renders nothing when there are no articles.
Using your own layout
By default the pages use a bare layout from the package. To render the blog inside your site, publish the config and point layout at your own layout:
php artisan vendor:publish --tag=content-studio-config
'layout' => 'layouts.app',
Your layout needs @yield('content'), and @stack('head') inside <head> for the title, meta description, Open Graph tags and JSON-LD.
Multiple languages
The blog is single language out of the box: it shows the articles in the main language of the Engine project, on the routes of the package. The sync always stores every language the Engine returns, so switching this on later needs no re-sync.
A site with multiple languages keeps its own way of working: its own routes, its own locale detection, its own URLs. The package asks the site for those three things instead of deciding them.
1. Register the routes yourself
CONTENT_STUDIO_ROUTES=false
The package then registers no blog routes, only the tracking script. Put the controller wherever it belongs, for example inside your own locale group:
use Shazzoo\ContentStudio\Http\Controllers\ArticleController; Route::prefix('{locale}')->whereIn('locale', ['nl', 'en'])->group(function () { Route::get('blog', [ArticleController::class, 'index'])->name('blog.index'); Route::get('blog/{slug}', [ArticleController::class, 'show'])->name('blog.show'); });
The article route must have a parameter named slug; every other parameter is yours. Add your own middleware, a translated prefix or a subdomain as you like.
2. Say which language to show
use Shazzoo\ContentStudio\ContentStudio; // In AppServiceProvider::boot() ContentStudio::resolveLocaleUsing(fn () => request()->route('locale') ?? app()->getLocale());
The overview, the article page, the sitemap and the latest-articles component all use this. The locale also becomes the app locale, so the package translations follow along. Default: the main language of the Engine project.
3. Say what the URLs look like
ContentStudio::resolveUrlUsing(fn (Article $article) => route('blog.show', [ 'locale' => $article->locale, 'slug' => $article->slug, ])); ContentStudio::resolveIndexUrlUsing(fn (string $locale) => route('blog.index', ['locale' => $locale]));
Every link in the views runs through these, so the cards, the previous/next links, the breadcrumbs, the canonical URL and the sitemap all follow your routes. Default: /{route}/{slug} and /{route}.
Translations of an article
Each language is its own record with its own slug, linked by the article id from the Engine. translations() finds the others, which is what you need for a language switcher or hreflang tags:
@foreach ($article->translations as $translation) <a href="{{ $translation->url() }}">{{ strtoupper($translation->locale) }}</a> @endforeach
What stays yours
- Tracking: the article view includes
content-studio::partials.tracking. Write your own article view and you add it yourself:@include('content-studio::partials.tracking', ['article' => $article])
- Sitemap: the package sitemap covers one language. For several languages, build your own from
Article::query()->visible($locale)->get(). hreflangand the canonical per language: add these to your own layout withtranslations().- A missing translation gives a 404. Publish the views if you want to fall back to another language.
Styling
The views use Tailwind CSS (v4) with neutral default styling and dark: variants. Tailwind only generates classes from files it scans, so add the package views to your CSS entry file:
@source '../../vendor/shazzoo/content-studio-laravel/resources/views';
The bundled layout loads resources/css/app.css through Vite when a build or dev server is present. Every element also has a cs-* class to hook your own CSS onto. To change the markup itself, publish the views:
php artisan vendor:publish --tag=content-studio-views php artisan vendor:publish --tag=content-studio-lang
Tests
composer test