---
title: "Multi-Brand Audit Platform — Internal tooling, in production | Harshith Nayaka L"
description: "Internal audit tooling for a multi-brand portfolio: real-browser crawling, a score where every weight traces to a public source, and paste-ready copy fixes."
canonical: "https://harshith-nayaka-l-portfolio.vercel.app/work/brand-audit-platform"
last-updated: "2026-09-10T04:03:52Z"
author: "Harshith Nayaka L"
content-type: "text/markdown"
html-version: "https://harshith-nayaka-l-portfolio.vercel.app/work/brand-audit-platform"
---
# Multi-Brand Audit Platform — Internal tooling, in production

> An internal audit platform a marketing team runs on: it crawls every brand site in the portfolio in a real browser, scores them against a model where every weight is tied to something Google actually published, and hands back paste-ready copy fixes.

Case study by Harshith Nayaka L, AI Engineer (Full-Stack), Bengaluru, India.
Canonical page: https://harshith-nayaka-l-portfolio.vercel.app/work/brand-audit-platform

- **Type:** Internal production platform
- **Context:** Multi-brand portfolio, internal marketing team
- **Focus:** Defensible scoring & evidence
- **Status:** In production — proprietary, no code shown

## The problem

Every SEO tool shows you a number. Almost none of them can tell you where the number came from. Google does not publish numeric ranking weights, and its own Lighthouse SEO score weights every audit equally while its documentation states plainly that the score is not a ranking signal. So a single score is a judgement, and most tools present it as a measurement.

That matters the moment someone acts on it. A team reading a confident 80 will spend real budget on the wrong page — and the tool that produced it has no way to defend the number in a room.

## What I built

Every check declares its own evidence basis: an impact tier, a weight, and a source URL. Five tiers, from blocker (the page cannot be indexed, so it cannot rank at any quality) through confirmed, relevance and appearance down to hygiene. The goal is not false precision — it is that every weight traces to a public statement, and the ordering is defensible even where the exact number is a judgement call.

Applying that honestly meant correcting the tool against Google's own documentation rather than against intuition. Meta description had been weighted like a ranking factor; Google says it is not one, so it was down-weighted to appearance. Heading order had been weighted like a ranking factor; Google says it does not matter if they are out of order, so it was reclassified as hygiene — still worth fixing for accessibility, no longer priced like a ranking lever.

Checks are weighted, not counted. A missing title and one image without alt text are not the same event, and under the old equal-weight model both moved the category to 50. And a failed indexing blocker caps the whole score at 25, because a page carrying noindex could previously report 74 and sit mid-table while its real search value was zero.

The AI layer is additive and boxed in. The whole audit runs end to end with no API keys at all; insights generate only when a key is present, and the free crawl layer never calls the model. Cached insights carry forward onto new runs and record which run they reasoned over, so a superseded read is labelled as based on an earlier crawl rather than presented as current.

## Pipeline

- **Crawl:** Real browser render (Playwright, three viewports)
- **Parse:** Rendered HTML (Images and inline SVG alike)
- **Check:** 20 pure checks (Five categories, unit-tested)
- **Score:** Weighted by evidence (Blocker caps at 25)
- **Explain:** Analyst read (Optional, cached, never required) → Handoff document (PDF · Word · HTML)

## The judgment calls

**A check that cannot run must say so**

Unmeasured checks used to return a soft warn worth 60 points for something nobody tested, and the run-level rollup then averaged pages as equals — so nine inner pages resting on one trivially-passing check outvoted the one page actually assessed, nine to one. Unmeasured checks are now excluded outright and pages are weighted by how much evidence each contributed. On a representative shape that moved a category from 93 to 69, which is the number being honest rather than the number being worse.

**A false alarm costs more than a miss**

The robots check scanned every Disallow line without tracking which user-agent group it belonged to. Two sites blocking a single aggressive crawler — normal hygiene — were reported as blocking their entire site, and the AI layer faithfully amplified that into a catastrophic finding. Nothing was wrong with either site. A false alarm on the most severe finding a tool can report burns the credibility of every other finding on the page, so the parser now groups by user-agent and both real files are pinned as regression tests.

**The AI must never invent facts about the business**

Asked to replace broken placeholder counters on a homepage, the model proposed a set of plausible, well-written, entirely invented business metrics — years trading, projects completed, clients served. Pasted as-is that publishes a lie on a company's own site. The instruction not to invent metrics had been read as SEO metrics only; it now separately forbids inventing facts about the business and requires a bracketed placeholder, and the interface detects any remaining placeholder and warns before the copy can be pasted live.

**Which SVGs actually owe a name**

The obvious accessibility rule — every SVG needs a title — is wrong, and produced 83 findings on one homepage, burying the real ones. Following axe-core's actual criteria instead, a name is owed in two cases: the SVG claims an image role, or it is the entire content of a link or button. That second case caught 60 genuine defects on one site: commercial internal links whose only content was an unnamed icon, so a screen reader announced 'link' and nothing else and Google got zero anchor text for a money page.

**Reasoning tokens share the answer's budget**

The insight feature silently stopped working while reporting success. The model bills its reasoning against the same output ceiling the answer comes from; a real call spent 4,868 tokens thinking and 3,309 answering against a ceiling of 8,192, truncated the JSON mid-string, and the script exited zero — so the button said refreshed over week-old data. The ceiling was resized from measurement across every site, truncation is now detected before parsing rather than surfacing as a parser error, and a run that writes nothing exits non-zero.

**Checks tests cannot make**

Typecheck catches type errors and tests catch broken behaviour, but neither catches a broken promise — code claiming something exists elsewhere when it does not. A doc referenced in a comment but never written, an env var read in code but missing from the example file, an npm script named in documentation but absent from the manifest, a database table written but never read. All four had shipped undetected. A separate doctor command checks exactly that one thing and runs on every change.

## What it changed

**What it proves:** That a score can be built to be defended rather than displayed. Every weight in the model traces to a public statement, every check names its evidence tier, and the tool corrects itself against primary documentation when the two disagree.

**Measured, not guessed:** Crawl profiling found the network-idle wait burning its full timeout on most pages because ad and analytics tags hold connections open — 35 to 58 percent of every page's time spent waiting for nothing. Blocking third-party tag hosts and running inner pages three at a time took one site from 110 seconds to 44, with scores byte-identical before and after. Concurrency is capped at 3 from measurement: sequential 39.5s, three at a time 28.6s, six at a time 46.9s.

**Verification:** 171 tests over the pure checks, the scoring rollup, and recorded API fixtures for the awkward cases — a metric returned as a string, an SVG with no title — so the edge cases are exercised without a network call.

**Honest limits:** Field data cannot be measured from a lab run, and the tool says so rather than substituting a proxy. Lab vitals are captured with third-party trackers blocked, which makes them optimistic by construction: a page that looks slow here is slower in the wild, never faster. That caveat ships in the interface, on the cell, not in a footnote.

## Built with

Next.js, TypeScript, Playwright, SQLite, Drizzle ORM, Google Gemini (multimodal), Vitest, PageSpeed Insights API
