Patchlog
Patchlog
Back to blog
tutorial

How to Add a Changelog to a Laravel App

Where the script tag actually goes in a Blade layout, what changes if you are running Inertia, and the two things that silently break it: a strict CSP and a cached view.

· 5 min read

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:

  1. https://patchlog.io/widget.js appears in the network tab with a 200.
  2. A request to patchlog.io/api/widget/YOUR_PROJECT_ID follows 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.

Keep your customers in the loop

Patchlog gives your app an in-app changelog widget and a hosted updates page in minutes. One script tag, no SDK.

Start for free →

Free plan available. No credit card required.