DEV Community

Marvin Tang
Marvin Tang

Posted on AI-assisted

Building a browser-first PHP tools directory without pretending everything is offline

Kitset is a PHP-rendered directory that currently exposes 423 small browser tools. The interesting engineering problem was not adding one more calculator. It was deciding, for every tool, which work belongs on the server, which work belongs in the browser, and what the offline promise should actually mean.

Here is the boundary I ended up using.

1. Let PHP own discovery, not the user's working data

The server renders stable routes, titles, descriptions, categories, related tools, and the component configuration for each page. That gives every tool a predictable URL and keeps navigation usable before the interactive code mounts.

The tool itself is selected by a small component key and a JSON configuration. On the client, a registry mounts the matching module:

Kitset.register('gsc-workspaces', init);

function init(el, config) {
  if (config.mode === 'seo-launch-kit') seoWorkspace(Kitset, el);
}
Enter fullscreen mode Exit fullscreen mode

That split matters. PHP does not need the title or meta description a visitor is drafting. It only needs to deliver the page and the code that can do the work.

2. Make privacy claims at tool level

For the SEO Launch Kit, the browser reads four fields: page title, site name, page URL, and meta description. A click handler builds a SERP preview, a robots.txt starter, a sitemap entry, and a WebPage JSON-LD object. There is no request in that calculation path.

The same pattern works for many text, calculation, and file utilities: use browser APIs, keep the working value in memory, and offer copy or download only after the result exists.

But I avoid turning that into a blanket architecture claim. Some utilities may need a remote URL, an API route, or a browser capability that is unavailable on a particular device. The public rule is narrower: inputs stay on the device whenever the task allows it. Each tool still needs its own review.

3. Treat the service worker as a cache policy, not a magic offline switch

Kitset precaches a small shell: the home page, the tools directory, My Kitset, core CSS and JavaScript, and icons. It does not precache 423 tool pages.

The fetch handler also has explicit boundaries:

if (request.method !== 'GET') return;
if (url.origin !== self.location.origin ||
    url.pathname.indexOf('/api/') === 0) return;
Enter fullscreen mode Exit fullscreen mode

Static assets use a cached response when available while refreshing from the network. Navigations are network-first and fall back to a previously cached page, then to the home shell. That means an already visited route may remain useful during a connection failure. It does not mean the entire catalog is available on first install.

This wording is less exciting than “works fully offline,” but it survives inspection.

4. Keep local persistence equally bounded

Favorites, recent tools, and remembered setups use localStorage. The shared helper cleans URLs and limits stored collections. Result text is not sent through analytics; only an allowlist of aggregate event fields can reach gtag when it is present.

LocalStorage is convenience, not durable storage. It may be unavailable, cleared, or shared with anyone using the same browser profile. The UI therefore treats saved state as optional and keeps copy/download as the explicit handoff.

A checklist I now use

  • Can the task run entirely with browser APIs? If not, name the remote dependency.
  • Does any input cross the network? Verify the actual event path, not only the page copy.
  • What is precached, runtime-cached, or deliberately bypassed?
  • Does an offline claim describe a visited route or the whole product?
  • What is kept in memory, localStorage, a download, or a server database?
  • Can a user understand those boundaries without reading the source?

The main lesson is that “browser-first” is not one implementation technique. It is a collection of narrow, testable decisions. Server-rendered discovery, client-side work, bounded local persistence, and a conservative cache policy can coexist—as long as the product copy does not collapse them into a promise the code cannot keep.

Top comments (0)