capo.js: checking that the order of your head elements is right
Get your <head> in order
At a glance
- What is it?
- capo.js is a script and a browser extension that analyses the order of elements in a page's head and flags the ones that are out of the order the browser would prefer, because head ordering affects perceived performance. It runs in the console, as a Node library, and as a WordPress plugin, and it is Apache-2.0 licensed with the newest release at v2.3.1 from 2026-09-19.
- Who is it for?
- capo.js fits a performance-minded frontend team that wants the head ordering of a page checked automatically rather than by eye, and it fits a Node pipeline that needs to assert on the head of a server-rendered page. It does not fit a team that wants the head rewritten for them in a static site, since the extension reports rather than rearranges, and the WordPress plugin is the separate project that does the rearranging.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 34 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Head order is a performance question, not a style question
capo.js exists because of one claim, and the claim is about perceived performance rather than aesthetics. How you order elements in the `<head>` can have an effect on the perceived performance of the page, and this script helps you identify which elements are out of order.
The lineage is stated in the readme. It is inspired by Harry Roberts's work on ct.css and by Vitaly Friedman's Nordic.js 2022 presentation, and the two of them are the people whose arguments about resource loading priorities this project encodes.
The mechanism is a weight per element type, and the output is a comparison. The script logs two info groups to the console, the actual order of the head and the optimal order. In the collapsed summary view you can see at a glance whether there are any high impact elements out of order, and each weight has a corresponding colour, red being the highest and blue or grey the lowest, with the exact mapping in `src/lib/rules.js`.
That weight table is the actual content of the project. Everything else is the machinery to collect the elements, look them up in the table, and report the difference. The examples in the readme show the tool run against the New York Times, docs.github.io, web.dev and stackoverflow.com, which is a good way to see what a violation looks like on real sites rather than on a synthetic one.
The detailed view expands the actual or the optimal order, which is the part that tells you what to change.
The project is Apache-2.0 licensed, the default branch is `main`, and the last push was on 2026-09-19.
Four ways to run it, and they answer different questions
The project ships the same analysis through four channels, and choosing between them is mostly about where the head is being built.
The first is the browser extension, for Chrome and Firefox, and it is the newest addition. You install it, browse to a page, and explore the console logs. The extension is the path of least resistance for auditing a site you do not control.
The second is the console script, injected manually. The readme explains that you can use local overrides in DevTools to inject the capo.js script into the document so that it runs before anything else, for example as the first child of `<body>`. Harry Roberts has a video showing how. The readme is honest about the drawbacks, noting that the inline script might be blocked by CSP, and it also suggests using something like Cloudflare workers to inject the script into the HTML stream, working around CSP by parsing out the correct `nonce` and adding it to the inline script. That suggestion comes with a parenthetical note that it is not tested, with an invitation to share examples.
The third is the programmatic API, which is the version 2 of the project, and it is the one that changed shape in the major version bump.
The fourth is the WordPress plugin, which is a separate project, capo-wp, and it is the only one that rewrites rather than reports. It automatically reorganises your WordPress site's head tags on the server via output buffering, with a deterministic stable sort that preserves CSS cascade and script execution order, full page cache compatibility, and a Site Health integration under Tools and Site Health.
The distinction between the reporting tools and the rewriting plugin is the one to hold on to. capo.js tells you what is wrong, and the WordPress plugin fixes it, and the readme keeps them clearly separate with a link to the plugin documentation.
The v2 API is an adapter over an abstraction, and the adapter is the point
The programmatic API in v2 is built around a single idea, and understanding it explains the whole design.
import { analyzeHead } from '@rviscomi/capo.js';
import { BrowserAdapter } from '@rviscomi/capo.js/adapters';
const head = /* your head element */;
const result = analyzeHead(head, new BrowserAdapter());`analyzeHead` takes a head element and an adapter. The adapter is the abstraction, and `BrowserAdapter` is the implementation for browser DOM elements. The list of adapters includes `AdapterInterface` as the base interface for custom adapters and `validateAdapter` to validate an adapter implementation, and the documentation points at a custom adapters page for building your own.
Why an adapter at all? Because the head is not always a DOM element. In Node.js, in a test, or in a build script, you have an HTML string or a parsed tree rather than a live DOM, and the analysis needs to reach into the element to ask questions like whether it is a meta tag or a preconnect. The adapter is how the core stays independent of the environment it is running in.
That design is also what makes the library testable, and the repository has an `examples/custom-adapter-test.js` file showing the pattern.
The core functions are `analyzeHead(head, adapter)` for detailed results, `analyzeHeadWithOrdering(head, adapter)` for results with ordering violations, `checkOrdering(elements)` to check an array of elements for violations, and `getWeightCategory(weight)` to get the category name for a weight value. The rules API exposes `ElementWeights` as a constant object mapping element types to weight values, `getWeight(element, adapter)` and `getHeadWeights(head, adapter)`, plus individual detector functions like `isMeta()`, `isTitle()` and `isPreconnect()`. The validation API has `VALID_HEAD_ELEMENTS`, `isValidElement`, `hasValidationWarning`, `getValidationWarnings` and `getCustomValidations`.
Subpath exports, a CommonJS build and a dual-format package
The packaging in `package.json` is the part of this project that shows real care about consumers.
The package is scoped, `@rviscomi/capo.js`, at version 2.3.1, published with public access, and it is an ES module package with `type: module` that also ships a CommonJS build at `dist/index.cjs`. The `main` field points at the CommonJS file and the `module` field at `src/index.js`, and the `exports` map gives separate `types`, `import` and `require` conditions for every entry point, which is the correct way to ship types for both module systems.
The subpath exports are the other half of it, and the readme explains the reason: import only what you need for smaller bundle sizes. The available paths are the root for the core analyzer, `./adapters` and `./adapters/browser`, `./rules` for the rules API, `./validation` for the validation API, and `./lib/io` and `./lib/options`.
// Import just the core analyzer
import { analyzeHead, checkOrdering } from "@rviscomi/capo.js";
// Import just adapters
import { BrowserAdapter } from "@rviscomi/capo.js/adapters";The `files` list is also deliberate, including `src` and `dist` but excluding `dist/*.zip` and the `dist/chrome` and `dist/firefox` directories. That is how the browser extension builds stay out of the npm package, which is the kind of detail that separates a package published by a careful maintainer from one published by a script.
The migration guide from v1.x records the three key changes, and they are all consequences of the same decision. All analysis functions now require an adapter parameter, there are new subpath exports for granular imports, and there is enhanced TypeScript support via JSDoc. The adapter requirement is the breaking change, and the TypeScript support through JSDoc is the answer to the fact that the project is JavaScript with a `tsconfig.json` and generated declaration files rather than a TypeScript rewrite.
One accuracy caveat, and it changes how you should use the tool
There is a single sentence in the readme that determines whether the tool gives you the right answer, and it is easy to miss in a long readme.
For applications that add lots of dynamic content to the head on the client, it would be more accurate to look at the server-rendered head instead.
That is a real methodological point. A client-rendered application injects its meta tags, preconnects and stylesheets after the initial HTML, so the head the browser ends up with is not the head the server sent. A tool that inspects the live DOM at load time sees the post-JavaScript state, and the ordering advice it gives is about a head that the browser already assembled, which is not the head whose order you control.
So the practical rule is: capo.js is most accurate against a server-rendered page, and least accurate against a heavily client-rendered single page application. The programmatic API in Node is the answer to the second case, because you can feed it the server-rendered HTML before any script runs, which is exactly the state the advice should be based on.
That is also why the Node API exists at all, and why it went through a breaking redesign in v2. The extension and the console script are for quick audits, and the Node API is for the case where you want to check the head of a page as the server sent it, in a test or in a build step.
The docs site is at rviscomi.github.io/capo.js, with separate developer documentation for custom adapters and user documentation for the extension and the WordPress plugin.
Where capo.js is the wrong tool
The first case is a team that wants the head rewritten for them in a static site. The extension and the script report; they do not rearrange. The WordPress plugin does rearrange, but it is a WordPress plugin for a WordPress site, and the readme is clear that it is a separate project. For a static site generator with a templating layer, the head order is set in a template, and the fix is a template edit, informed by what capo.js tells you.
The second case is a team with no performance problem in the head. The tool is a diagnostic for a specific issue. If your critical rendering path is blocked by something other than head ordering, a detailed report of misordered elements will not move the number, and the colour-coded console output becomes noise.
The third case is a stack where the head is assembled by a framework you do not control. Some meta frameworks and CMS platforms inject their own tags at a fixed point, and there is nothing the ordering advice can do about tags that the platform appends after yours.
The fourth is a team that needs a hard guarantee rather than a report. capo.js is a linter for the head, and to use it as a gate you need the programmatic API in a test or a CI step, which is the right use and is a different piece of work from installing the extension.
One more practical point. The weights are opinionated, and they are the opinion of a specific set of people about what browsers prioritise. The readme points at the source file, `src/lib/rules.js`, for the exact mapping, and that is the right thing to read before you turn a warning into a build failure, because the mapping encodes assumptions about network conditions and about how different browsers prioritise resource loads. A rule that is right for a site with a slow connection may be wrong for one served from a CDN near the user, and the table is where that judgement is encoded.
Editorial conclusion
capo.js fits a performance-minded frontend team that wants the head ordering of a page checked automatically rather than by eye, and it fits a Node pipeline that needs to assert on the head of a server-rendered page. It does not fit a team that wants the head rewritten for them in a static site, since the extension reports rather than rearranges, and the WordPress plugin is the separate project that does the rearranging. Before you adopt it, read the note about client-rendered head content, because the tool warns that for applications adding lots of dynamic content to the head on the client it is more accurate to look at the server-rendered head instead.
Frequently asked questions
How do I use capo.js in a browser?
Install the Capo browser extension for Chrome or Firefox and explore the console logs, or inject the script manually through DevTools local overrides as the first child of `<body>`, which the readme notes may be blocked by CSP. The script logs two info groups, the actual head order and the optimal order.
How do I use capo.js programmatically in Node.js?
Install `@rviscomi/capo.js`, then import `analyzeHead` from the package and `BrowserAdapter` from `@rviscomi/capo.js/adapters`, and call `analyzeHead(head, new BrowserAdapter())`. The same functions are available from CommonJS with `require`, and the package ships subpath exports for the core, adapters, rules and validation APIs.
What changed in capo.js v2?
All analysis functions now require an adapter parameter, new subpath exports were added for granular imports, and TypeScript support was enhanced through JSDoc. The migration guide is in the repository docs.
Does capo.js fix the head order or only report it?
It reports. The readme points to a separate WordPress plugin, capo-wp, which automatically reorganises the head tags on the server via output buffering, using a deterministic stable sort that preserves CSS cascade and script execution order.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rviscomi-capo-js)Was this useful?