Your knuckleswtf/Scribe API Docs Are Hanging the Browser. Here’s How to Fix It.

Reading Time: 4 minutes

If you’ve been using knuckleswtf/scribe to generate API documentation for a Laravel application, there’s a good chance you’ve hit this wall at some point: the docs page loads slowly, the browser tab spins for several seconds, or the page just becomes unresponsive. The index.html file for a large API can balloon past 10MB, and the browser has to process all of it before showing you anything.

The natural question is: can Scribe split the documentation into multiple pages โ€” one per group, for instance โ€” so the browser isn’t loading everything at once?

The short answer is no, not natively. But there’s a proper solution, and it involves rethinking the output type rather than the content structure. This post walks through what’s actually happening, how to fix it, and how to handle both a standard config/scribe.php and a custom config file.


What’s Actually Causing the Hang

When you use Scribe’s static or laravel output type, it takes every endpoint you’ve documented โ€” request bodies, response examples, parameter tables, code snippets for every configured language โ€” and renders all of it into HTML. Everything ends up in a single index.html file.

When the browser opens that file, it has to:

  • Parse a very large HTML document
  • Build the full DOM tree for every endpoint
  • Execute the JavaScript that wires up all the interactive elements โ€” collapsible sections, “Try It Out”, the language switcher
  • Layout and paint everything before showing you the page

None of this is deferred or lazy. The browser works through the entire document upfront. On a small API, this is imperceptible. But as endpoint count grows, these costs stack up fast. At a few hundred endpoints you’re looking at 5โ€“15 seconds. At a few thousand, the tab can crash entirely.

The fix is not to fiddle with the HTML structure. The fix is to stop asking Scribe to render the endpoint HTML at all.


The Solution: external_static or external_laravel with Scalar

Scribe has two lesser-used output types documented in the config reference: external_static and external_laravel. Instead of generating a self-contained rendered HTML document, these types produce:

  1. Your openapi.yaml โ€” the full OpenAPI 3 spec for your API
  2. A minimal HTML shell that loads an external renderer and points it at the spec

The renderer then handles everything: parsing the spec, building the UI, powering search and Try It Out. Critically, modern renderers do this lazily โ€” they only render what’s in the viewport. Everything else is deferred until the user scrolls to it.

Scribe supports three external renderers via the theme key:

  • scalar โ€” best performance, cleanest UI, most actively developed
  • elements โ€” Stoplight Elements
  • rapidoc

This post uses Scalar, but the configuration approach is the same for all three.

A note on valid theme values: the theme key is type-dependent. For static and laravel, valid values are default and elements. For external_static and external_laravel, valid values are scalar, elements, and rapidoc. Using default with an external_* type throws a View [external.default] not found error.


Fixing the Default config/scribe.php

Open config/scribe.php and change two values:

// Before
'type' => 'static',
'theme' => 'default',

// After
'type' => 'external_static',
'theme' => 'scalar',

If you’re using laravel type because you serve docs through your app’s routing (with middleware, auth, etc.), use external_laravel instead:

'type' => 'external_laravel',
'theme' => 'scalar',

Then regenerate:

php artisan scribe:generate

That’s it. For external_static, Scribe places the output in the path specified by static.output_path (default: public/docs). For external_laravel, it’s served at the path set in laravel.docs_url (default: /docs). The generated index.html is now a thin shell. The openapi.yaml carries all the actual API data, and Scalar renders it on demand in the browser.


Fixing a Custom Config File

Scribe supports multiple independent doc sets via separate config files, each targeted with the --config flag:

php artisan scribe:generate --config scribe_admin

The fix is identical โ€” change type and theme inside config/scribe_admin.php:

'type' => 'external_static', // or 'external_laravel'
'theme' => 'scalar',

Everything else โ€” your routes array, static.output_path, auth settings, strategies โ€” stays exactly as it was. Only those two lines change.

One thing worth noting from the docs: if you’re running multiple configs with the laravel type (or external_laravel), the built-in routing (laravel.add_routes) won’t work for your secondary configs. You’ll need to add routes manually in routes/web.php:

Route::view('/docs', 'scribe.index')->name('scribe');
Route::view('/admin/docs', 'scribe_admin.index')->name('scribe-admin');

Customising Scalar

When you switch to an external_* type, Scribe generates an HTML shell using a blade template. Publish it into your project so you can edit it:

php artisan vendor:publish --tag=scribe-views

This creates resources/views/vendor/scribe/external/scalar.blade.php. Open it, find the <script id="api-reference"> tag, and add a data-configuration attribute:

<script
  id="api-reference"
  data-url="{{ $openApiUrl }}"
  data-configuration='{"theme":"moon","layout":"modern","darkMode":true}'
></script>

Because this file is in resources/views/vendor/scribe/, it’s part of your project โ€” not generated output. Running php artisan scribe:generate again won’t touch it.

Don’t use external.html_attributes for Scalar config. There’s a known issue where Scribe’s serialisation of that PHP array into HTML attributes produces malformed JSON, which causes Scalar to throw a SyntaxError: Expected property name or '}' in JSON on load. The published blade template is the reliable path.

Scalar Themes

ThemeStyle
defaultClean neutral light
alternateWarmer variant of default
moonCool dark
purpleDark with purple accents
solarizedSolarized palette
bluePlanetDeep blue dark
saturnWarm dark
keplerHigh-contrast dark
marsReddish dark
deepSpaceVery dark, high-contrast
noneCompletely unstyled โ€” for custom CSS

All themes support light and dark mode. Use forceDarkModeState to lock one:

{
  "theme": "moon",
  "forceDarkModeState": "dark",
  "hideDarkModeToggle": true
}

Other Useful Scalar Options

{
  "layout": "modern",
  "hideSearch": false,
  "hideModels": false,
  "defaultOpenFirstTag": true,
  "expandAllResponses": false,
  "showOperationId": false,
  "hideTestRequestButton": false,
  "documentDownloadType": "yaml",
  "defaultHttpClient": {
    "targetKey": "node",
    "clientKey": "axios"
  },
  "hiddenClients": {
    "js": ["jquery", "xhr"],
    "shell": ["httpie", "wget"]
  },
  "metaData": {
    "title": "My API Docs",
    "description": "API reference"
  }
}

layout accepts "modern" (sidebar navigation) or "classic" (Swagger-style). documentDownloadType accepts "json", "yaml", "both", "direct", or "none".


Generating Multiple Separate Doc Sites

If you need genuinely separate documentation sites for different parts of your API, Scribe’s multiple config file approach handles that. Each config file gets its own routes scope and output path:

config/scribe.php            โ†’ public/docs/
config/scribe_admin.php      โ†’ public/admin/docs/
config/scribe_webhooks.php   โ†’ public/webhooks/docs/

Each config has a routes array scoped to only the relevant endpoints:

// config/scribe_admin.php
'routes' => [
    [
        'match' => [
            'prefixes' => ['api/v1/admin/*'],
            'domains' => ['*'],
        ],
    ],
],

'static' => [
    'output_path' => 'public/admin/docs',
],

Generate each independently:

php artisan scribe:generate
php artisan scribe:generate --config scribe_admin
php artisan scribe:generate --config scribe_webhooks

You can wire all of them into a Composer script to run them together:

{
  "scripts": {
    "docs:generate": [
      "php artisan scribe:generate",
      "php artisan scribe:generate --config scribe_admin",
      "php artisan scribe:generate --config scribe_webhooks"
    ]
  }
}
composer docs:generate

Apply external_static + Scalar to all of them for consistent fast rendering across the board.


Summary

Output TypeRenders HTMLGrows with EndpointsLazy Rendering
staticScribeYesNo
laravelScribeYesNo
external_staticScalarNoYes
external_laravelScalarNoYes

If your Scribe docs are hanging in the browser, the fix is two lines in your config:

'type' => 'external_static',
'theme' => 'scalar',

Regenerate, and the browser now loads a lightweight shell in milliseconds. Scalar fetches the openapi.yaml and renders endpoints progressively as you scroll. Your OpenAPI spec stays exactly the same โ€” the only thing that changed is who processes it.

Afolabi 'aphoe' Legunsen on FacebookAfolabi 'aphoe' Legunsen on GithubAfolabi 'aphoe' Legunsen on GoogleAfolabi 'aphoe' Legunsen on LinkedinAfolabi 'aphoe' Legunsen on TwitterAfolabi 'aphoe' Legunsen on Youtube
Afolabi 'aphoe' Legunsen
Software Project Lead at itquette solutions
A software developer with 9 years professional experience. Over the years, he has built and contributed to a number of projects which includes MoneyTalks that won Visa's Financial Literacy Challenge and Opomulero that won the British Council's Culture Shift III.

He currently the Software Project Lead of Itquette Solutions where his team has built Smart PMS and BlueQuiver HRMS among many other products.

Leave a Comment