LaralCN-UI blocks: installing a navbar and getting a navbar
Every navbar block shipped a full demo page: a min-h-screen wrapper and a hero section that had nothing to do with the navbar. This pass strips them to the header alone, drops Alpine from the docs site itself so it stops proving the opposite of what it documents, and moves syntax highlighting to the server. Including the component attribute that quietly broke the page around it.
This follows on from dropping Alpine.js from the registry. Same release, different half of the problem.
Blocks are the ready-made layouts in LaralCN-UI: php artisan ui:add-block navbar-01 writes one Blade file and installs every component it composes. There are six navbars and an application shell.
All six navbars had the same defect. The file did not contain a navbar. It contained a page:
<div class="min-h-screen bg-background text-foreground">
<header class="sticky top-0 z-40 ...">
{{-- the actual navbar --}}
</header>
{{-- Demo page content (not part of the navbar) --}}
<main class="mx-auto max-w-6xl px-4 py-20">
<h1>Build faster with LaralCN-UI</h1>
...
</main>
</div>
The comment admits it. Install navbar-01 and you got a hero headline, two call-to-action buttons, and a wrapper forcing full viewport height, all of which you then had to delete by hand before the block was usable. Now the file is the <header> and nothing else. Two of the six keep a positioning wrapper, because their design is a contained pill that floats away from the top edge, and that wrapper is genuinely part of the block.
Reusing what already exists
Each navbar's mobile menu had a hand-rolled expanding submenu built on x-collapse. Rewriting six of those in vanilla JavaScript would have meant six copies of the same slide logic.
They now use <x-ui.collapsible>, which already exists in the registry and already had to be rewritten anyway. collapsible became a declared dependency of every navbar block, so ui:add-block navbar-01 installs it for you. The chevron rotation went from an Alpine binding to a CSS rule:
<x-ui.collapsible.trigger class="flex items-center justify-between ...">
Link Four
<svg class="transition-transform group-data-[state=open]/collapsible:rotate-180">
The mega menus in navbar-03, 04 and 05 do carry their own small script, because a full-width panel that opens on hover and closes when you leave the header is not something the collapsible covers. That script uses mouseover delegated from document rather than mouseenter, since mouseenter does not bubble and there is only one listener for the whole page.
The docs site was proving the opposite
The website that documents all of this was itself built on Alpine: the copy buttons, the tab strips, the dark mode toggle, the preview scaler. Leaving it that way meant the previews rendered in an environment where Alpine happened to be loaded, so a component that still secretly needed Alpine would have looked fine.
So the site lost Alpine too. Its own chrome is now about sixty lines of delegated listeners in resources/js/app.js. The client bundle went from roughly 90KB with Alpine to 44KB.
Then it went to 1.1KB, because highlight.js left as well.
Highlighting on the server
Code samples were highlighted in the browser with highlight.js, which meant shipping a highlighter to every visitor to colour text that never changes. The samples are Blade, and Blade is exactly what highlight.js is worst at.
spatie/shiki-php handles Blade properly, so highlighting moved to the server:
$highlighted = Shiki::highlight(code: $code, language: 'blade', theme: 'github-dark');
Shiki shells out to Node, which costs around 150ms per block. A docs page has up to ten. The source only changes when the registry does, so the rendered HTML is cached on a hash of the code, and a warm page renders in about 30ms against 900ms cold.
That decision came back to bite me in production, which is the next post.
Previews that fit what they show
Removing the demo page from the navbar blocks left the docs preview looking broken. The preview iframe was a fixed 1280x720, sized for a block that filled a screen, and a 64px navbar in an 800px box reads as an error.
The preview now measures the furthest bottom edge of the iframe's own content and sizes itself to that, clamped between 200 and 800 pixels. A navbar block gets a compact strip and the sidebar shell still gets a tall frame, from the same code.
The attribute that broke the page around it
Worth naming, because it was mine and it was live.
The tabs component put its panel key on a plain data-tab attribute:
<button role="tab" data-ui-tab data-tab="{{ $key }}">
The docs site uses the same convention for its own Preview and Code strips. So on the tabs documentation page, clicking Password ran both handlers: the component switched its panel, and the site's handler found the nearest [data-tabs] ancestor, which was the page's own strip, and hid every panel that did not match password. The entire preview section disappeared.
The fix is that the key now lives on the data-ui-* attribute itself:
<button role="tab" data-ui-tab="{{ $key }}">
The real lesson is not about tabs. data-tab is far too common a name to put in a file that gets copied into somebody else's application, where I have no idea what other attributes are already in use. Anything a distributed component reads from the DOM now has to be namespaced, and the site's own handlers moved to data-site-* so they can never catch a consumer's markup either.
Where it is
Six navbars, one application shell, all installable with php artisan ui:add-block <name>, all of which now install the block and stop there.
Docs at laralcn-ui.abdulkadersafi.com/blocks, code on GitHub.
Last post in this run is about shipping 0.3.0, where the code was correct and the deployment was not.
Building scalable systems and developer-first tools. Lead Software Engineer at DSRPT.
Frequently asked
-
A ready-made layout composed from registry components, installed with php artisan ui:add-block <name>. The command writes one Blade file and resolves every component it depends on, so ui:add-block navbar-01 also installs button, collapsible, dropdown-menu and sheet. There are six navbars and one application shell.
-
The samples are Blade and they never change between requests, so shipping a highlighter to every visitor is wasted bytes. spatie/shiki-php handles Blade properly, where highlight.js does not. It shells out to Node at about 150ms per block, so the rendered HTML is cached on a hash of the source: a warm page renders in roughly 30ms instead of 900ms, and the client bundle dropped to about 1KB.
-
Because the file ends up in somebody else's application, next to markup you cannot see. The tabs component used a plain data-tab attribute, which collided with the documentation site's own tab strips and made an entire preview section vanish on click. Anything a distributed component reads from the DOM is now under data-ui-*, and the site's own handlers use data-site-* so they cannot catch a consumer's markup either.