Unrecognized feature: 'interest-cohort' on GitHub Pages
Where the interest-cohort warning came from, why the meta-tag fix does nothing, and the base-path change that actually brings a blank GitHub Pages app back.
TL;DR#
The message is a warning, not an error, and it does not stop your page from loading. From April 2021, GitHub Pages sent Permissions-Policy: interest-cohort=() on every github.io site to opt them out of Google's FLoC experiment, and Chrome builds that did not know the interest-cohort name logged "Unrecognized feature". The page still loaded. Nothing in your project caused it, and the popular "add a meta tag" answer does nothing, because Chromium ignores Permissions-Policy in a meta tag. If your page is blank after deploying, the usual cause, which the third answer on the question points to, is a build deployed to https://<USERNAME>.github.io/<REPO>/ that loads its scripts from the domain root. Set homepage in package.json (Create React App) or base in vite.config.js (Vite), rebuild and redeploy.
The console line from the Stack Overflow question, asked in October 2021 by someone whose React page worked on localhost and who ran into errors after hosting it on GitHub with npm run deploy, reads "Error with Permissions-Policy header: Unrecognized feature: 'interest-cohort'." It was the first warning in the console, and the one that worried the asker most. The highest-voted answer says you can ignore it, and that is right. The third answer looks past it, at the network requests for the CSS and JavaScript files.
Where the warning comes from#
Two pieces of Chromium build the message. The prefix is set when Blink parses the response headers of a document, in security_context_init.cc, which creates a logger with the text "Error with Permissions-Policy header: ". The rest comes from the policy parser, permissions_policy_parser.cc: when a feature name in the header is not in the browser's list of known features, it logs "Unrecognized feature: " followed by the quoted name, and skips that entry. The word "Error" in the prefix is misleading. Chrome reports it at warning level, drops the one unknown entry and carries on.
The header itself came from GitHub. The GitHub changelog entry of 27 April 2021 says: "All GitHub Pages sites served from the github.io domain will now have a Permissions-Policy: interest-cohort=() header set." It adds: "Pages sites using a custom domain will not be impacted." The empty allowlist () means "this feature is allowed for no one", an opt-out of FLoC, the interest-cohort experiment Chrome was trialling at the time.
Why was the name unknown to the asker's browser? In Chromium, a policy feature that depends on a runtime feature only exists when that runtime feature is enabled (the comment at the top of the features list states it: "the feature is available if any of the runtime features are enabled"). In Chromium's source on the day before the question (main at 17 October 2021), the interest-cohort policy depended on a runtime feature named InterestCohortFeaturePolicy, and the runtime features list gave that feature the status "experimental", which the same file defines as features that are "not on by default in stable". Then the commit that removed FLoC from Chromium, dated 5 February 2022 and titled "Remove FLoC code", deleted the policy name altogether. Six weeks later, the commit that added the Topics API (17 March 2022) brought the name back as BrowsingTopicsBackwardCompatible, which depends on the Topics API runtime feature. Chromium's feature enum describes it as the interest-cohort "permissions policy that controls the use of Topics API". The current features list still carries that alias.
So whether a given Chrome printed the warning depended on its version and its flags, and it never said anything about the page you deployed.
What a current Chrome does with the header#
To see the behaviour today rather than infer it, a small Node server on 127.0.0.1 served the same HTML page with different Permissions-Policy values, and a headless Chrome read each one through the DevTools Protocol (22 October 2026). The browser reported its version as Chrome 155. A made-up feature name is the positive control: it must produce the warning, otherwise the capture proves nothing. An excerpt of the output:
Browser: Chrome/155.0.8059.86
--- /bogus header: bogus-feature=() meta: (none)
console [warning] Error with Permissions-Policy header: Unrecognized feature: 'bogus-feature'.
--- /cohort header: interest-cohort=() meta: (none)
featurePolicy: {"geolocation":true,"interestCohortKnown":true,"browsingTopicsKnown":true}The control produced the exact message, at warning level. The header from GitHub produced no console line at all, and document.featurePolicy.features() listed interest-cohort as a known feature. On this build, the warning in the question no longer appears even when the header is present. (The same run logged a 404 for /favicon.ico on the control page; the test server only served the listed paths.)
GitHub Pages no longer sends the header#
The second half of the story is on GitHub's side. A GET on four github.io sites on 22 October 2026, filtered to the status line, the server and any Permissions-Policy line, returned no such header:
$ curl -s -D - -o /dev/null https://octocat.github.io/ | grep -i -E '^HTTP|^server|^date|permissions-policy'
HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 22 Oct 2026 21:55:57 GMT
$ curl -s -D - -o /dev/null https://google.github.io/styleguide/ | grep -i -E '^HTTP|^server|^date|permissions-policy'
HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 22 Oct 2026 21:55:58 GMTThe other two sites (primer.github.io and microsoft.github.io/monaco-editor/) gave the same result. When GitHub stopped sending it is not stated in the changelog entry that announced it, so this page does not guess a date. The practical consequence is simple: on a github.io site today, the interest-cohort warning has nothing to come from.
Why the meta-tag answer changes nothing#
The second most popular answer suggests adding this to index.html:
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">Even when the warning was live, this could not remove it: the warning comes from the response header, and a meta tag cannot delete a header the server sends. It also has no effect of its own. Chromium's http_equiv.cc handles a fixed list of http-equiv values (content-security-policy, refresh, default-style, origin-trial and a few others), and Permissions-Policy is not on it. The same headless run shows it with a feature whose effect is easy to read, geolocation:
--- /geo-header header: geolocation=() meta: (none)
featurePolicy: {"geolocation":false,"interestCohortKnown":true,"browsingTopicsKnown":true}
--- /geo-meta header: (none) meta: geolocation=()
featurePolicy: {"geolocation":true,"interestCohortKnown":true,"browsingTopicsKnown":true}Sent as a header, geolocation=() turns geolocation off for the page. Sent as a meta tag, the same value leaves it on. If you added that meta tag to silence the warning, you can delete it.
Still seeing "Unrecognized feature"? Find who sends the header#
Chrome prints this warning for any name it does not know, so it still turns up with other names, for example when a header copied from a boilerplate lists features that the browser has never shipped or has since removed. The fix is always on the server, never in your bundle. First find which response carries it:
curl -s -D - -o /dev/null https://www.example.eu/ | grep -i permissions-policyIn Chrome DevTools, the Network panel shows the same thing: select the document request and read the Response Headers. If the header is there, it was added by whatever serves the page: your framework config, a hosting _headers file, a reverse proxy or a CDN rule. On a Next.js app, the usual place is the headers option in next.config.js, where a block such as this one is the thing to edit:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
],
},
]
},
}Remove the name Chrome reports as unrecognised, keep the ones you actually need, redeploy, and run the curl again. If the name is valid in another browser and you want to keep it there, the warning is harmless and can stay; Chrome only skips that entry.
The real fix for the blank GitHub Pages app#
If your page works on localhost but is blank on GitHub Pages, the usual cause, which the third answer points to, is the public path. A project site lives under a sub-path (https://<USERNAME>.github.io/<REPO>/), but both Create React App and Vite build for the domain root by default, so the generated index.html asks for its JavaScript and CSS at /static/... or /assets/..., which on github.io resolves to the root of the user site, USERNAME.github.io, not to your project. The scripts 404, React never mounts, and the page stays empty.
Create React App (with the gh-pages package and an npm run deploy script, as in its guide). The CRA deployment guide says: "By default, Create React App produces a build assuming your app is hosted at the server root." Its GitHub Pages section marks the homepage field as the step that must not be skipped, and explains that "Create React App uses the homepage field to determine the root URL in the built HTML file":
{
"homepage": "https://<USERNAME>.github.io/<REPO>",
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}
}Vite. The Vite static deploy guide says that for https://<USERNAME>.github.io/<REPO>/ you set base to '/<REPO>/':
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: '/my-app/',
})Rebuild and redeploy after either change; the old build in the gh-pages branch still has the root paths. A user site (USERNAME.github.io itself) or a custom domain serves from the root, so it needs neither change. The build output behind this, and why the same absolute paths also break a dist/index.html opened from disk, is shown step by step in Tailwind CSS not working after a Vite build, whose section on deploying under a sub-path covers GitHub Pages project sites.
Verify the fix#
Open the deployed site with DevTools on the Network tab and reload. Every script and stylesheet request should go to /REPO/... and return 200. The interest-cohort line, if your browser still shows one for some other site, can be ignored; what matters is that no request for your bundle returns 404 and that the console shows no uncaught error from your own code.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
The error that tends to come next#
Once the paths are right, the next deploy can bring a different failure: a tab opened before the redeploy asks for a hashed chunk that the new build deleted, and the console shows "Failed to fetch dynamically imported module". That case, and the no-cache plus reload-once fix for it, is in Failed to fetch dynamically imported module. A different static-hosting path problem shows up on Firebase: Firebase Hosting: every route except index.html returns 404 explains why a static Next.js export serves only the home page until clean URLs are turned on.
In short, the Permissions-Policy line was a side note from GitHub's 2021 FLoC opt-out, met by Chrome builds that did not know the name. Neither GitHub's current responses nor Chrome 155 produce it, and nothing you add to index.html changes it. A blank project site is a path problem, and homepage or base is the change that fixes it.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix "changing an uncontrolled input to be controlled"
Why an empty state object makes React treat a text field as uncontrolled, what the React 19 wording looks like, and the react-hook-form variant of the same warning.
MUI DatePicker readonly in Cypress CI: fix the variant
A wider Cypress viewport does not guarantee the desktop picker. Diagnose the media query first, then test the UI your users actually receive.
Fix Functions cannot be passed directly to Client Components
Read the caret line to find the exact prop, then keep that function on the client side of the boundary: four reproduced shapes on Next.js 16.4.0, and why "use server" rarely fixes it.


