Adding a "what's new" widget to a Laravel app is a one line change. Getting it to appear on every page, survive an Inertia navigation, and not get blocked by your own Content Security Policy takes about ten minutes more, and those are the parts nobody writes down.
This is written for Laravel 10, 11 and 12. Nothing here is version specific.
Where the tag goes
Laravel apps almost always have one layout that wraps everything. Put the script there once, not in individual views.
If you are using a plain Blade layout, that file is usually
resources/views/layouts/app.blade.php:
{{-- ... your page content ... --}}
@vite(['resources/js/app.js'])
<script
src="https://patchlog.io/widget.js"
data-project="YOUR_PROJECT_ID"
defer
></script>
</body>
</html>
If you scaffolded with Breeze, Jetstream or a starter kit, the file to edit is
resources/views/app.blade.php for the Inertia stacks and
resources/views/layouts/app.blade.php for the Blade ones. Check which one
actually renders by looking at what your controllers return: view('...') uses
the Blade layout, Inertia::render('...') uses app.blade.php.
data-project takes the project ID, not the slug. The slug is the subdomain your
hosted changelog lives on; the ID is what the widget queries. Copy the snippet
out of your project's widget settings and it is already correct.
What changes with Inertia
This is the part that catches people out. Inertia does not reload the page
between navigations, so a script tag placed in the root template runs exactly
once, on first load. That is actually what you want here: the widget mounts once
and stays mounted, and it does not need to re-initialise when the user moves from
/dashboard to /settings.
What you should not do is inject the script from inside a Vue or React
component with onMounted or useEffect. On an Inertia app that component may
unmount and remount several times in a session, and you will end up with two or
three copies of the widget on the page. Put it in the root Blade template and
leave it alone.
There is one exception. If you only want the widget for signed-in users, wrap it:
@auth
<script
src="https://patchlog.io/widget.js"
data-project="YOUR_PROJECT_ID"
defer
></script>
@endauth
That still evaluates once per full page load, which with Inertia means once per session, so it holds.
The two things that silently break it
A strict Content Security Policy. If you are running a CSP, either through
middleware you wrote or via a package like spatie/laravel-csp, the browser will
refuse to load a script from a domain you have not listed, and it will do so
without touching your Laravel logs. You will see nothing server side and an error
in the browser console.
Add the domain to your script and connect directives:
// In your CSP policy class
->addDirective(Directive::SCRIPT, 'https://patchlog.io')
->addDirective(Directive::CONNECT, 'https://patchlog.io')
SCRIPT lets the file load. CONNECT lets it fetch your entries once it has
loaded. Forgetting the second is the more common mistake, because the widget
appears to load fine and then shows nothing.
A cached view. If you edited the Blade file on a server where you have run
php artisan view:cache, your change is sitting in a compiled view that is not
being used. Run php artisan view:clear, or add both to your deploy script,
which you probably want anyway:
php artisan view:clear
php artisan view:cache
Checking it actually works
Load any page and open the console. Two things should be true:
https://patchlog.io/widget.jsappears in the network tab with a 200.- A request to
patchlog.io/api/widget/YOUR_PROJECT_IDfollows it, also 200.
If the first fails, it is your CSP or a typo in the src. If the first succeeds
and the second 404s, the data-project value is wrong, and it is almost always
because the slug was used instead of the ID.
If both are 200 and you still see nothing, the project has no published entries yet. The widget renders nothing rather than an empty box, which is deliberate but confusing the first time.
Why it will not break your styling
Laravel apps tend to have opinionated CSS, whether that is Tailwind with a preflight reset or a Bootstrap theme that styles bare elements globally. Embedded widgets have historically been terrible about this, inheriting your button styles or bleeding their own into your page.
Patchlog renders inside a Shadow DOM, so nothing on either side crosses the
boundary. Your button { } rule does not reach into it and its styles do not
reach out. There is nothing to configure for that; it is how the widget mounts.
The part that saves the most time
Writing the changelog is what actually stops people from having one, not installing it. If you are already pushing to GitHub, you can have the entries drafted for you: add the official GitHub Action to your workflow and each push is sent for drafting, then on whatever cadence you pick an LLM turns the buffered commits into one draft entry per user facing change, which you review before anything publishes.
- uses: michaelyousrie/patchlog-action@v1
with:
token: ${{ secrets.PATCHLOG_TOKEN }}
project-id: ${{ vars.PATCHLOG_PROJECT_ID }}
That needs fetch-depth: 0 on your checkout step so the action can see the
commit range. Lockfiles, .env, vendor and build output are stripped locally
and never leave the runner.
That part is on the paid plan. The widget, the hosted page and the RSS feed are on the free one, which is enough to answer whether your users care before you pay for anything.