Two counts from the same repository.
find app -name page.tsx | wc -l → 1029 curl -s /sitemap.xml | grep -c '<loc>' → 991 Both are correct. In the Next.js App Router a page file and a public URL are not the same unit, and they disagree in both directions. Diffing the two sets took about five minutes and produced a better description of the codebase than any folder tree I could have drawn.
The arithmetic
- 1,029
page.tsxfiles - 8 of them have a dynamic segment, so 1,021 are a fixed route
- 991 URLs in the sitemap, all answering 200
The set difference, run properly, says this:
| files | lines | |
|---|---|---|
| Public content pages | 991 | 148,824 |
| The dynamic practice runner | 1 | - |
| Signed-in product, admin, auth, dev-only | 37 | 4,107 |
That is the whole business in one table. 97 percent of the page files in this repository exist to be found by somebody who has not signed up yet, and they carry 36 times the prose of everything behind the login.
The public 991, by family:
| URLs | family |
|---|---|
| 457 | /games/* provider hubs and per-test pages |
| 211 | /employers/* |
| 116 | /blogs/* |
| 65 | /cheating/* |
| 61 | /interview/* |
| 15 | /tests/* |
| 22 | /nl/* and /de/* |
| 10 | /assessment-centre-practice/* |
| 7 | /comparison/* |
| 5 | /application-tracker/* |
| 22 | top level, one page each |
The route that produces no URLs at all
app/games/[game-name]/page.tsx is one file. It serves every practice exercise on the site, 428 routes by the count in our own robots.txt, and contributes exactly zero entries to the sitemap.
That is deliberate. A practice attempt is not a document: there is nothing on that URL for a crawler to read, because the page is a runner that needs a session and a question set before it means anything. So those routes are excluded from the sitemap and blocked in robots.txt, where they are 856 of the 906 Disallow lines: 428 routes, listed twice because the file carries one rule set for * and an identical one for Bingbot.
Which means the single most-used route in the product is invisible to every tool that measures a site by its sitemap. If you audit coverage, performance or internal links from a sitemap, you are auditing the brochure.
Thirty-seven files behind the door
Grouped, the files that produce no public URL:
- 11 dashboard pages
- 7 admin pages
- 4 password-recovery steps
- login, sign-up and the sign-up success page
- an affiliate join page, an auth error page, a device-limit page, a maintenance page, an unsubscribed page
- 2 dev-only preview routes
The last two are the interesting ones, because they are shipped and unreachable:
export default async function GamePreviewPage({ params }) { if (process.env.NODE_ENV !== 'development') notFound(); ... } They exist so a screenshot script can drive a real exercise with no sign-in, which is a legitimate need and a terrible thing to leave reachable. The guard is one line at the top of the component, and every build that is not next dev answers 404. You can check that from outside: it is the only part of this post you can verify without reading the repo.
The disagreement runs the other way too
Three sitemap URLs have no matching folder path, which looked like a bug until I remembered route groups:
app/games/(benchmarks)/arctic-shores/practice-scores/page.tsx → /games/arctic-shores/practice-scores A folder in parentheses is organisational only. It does not appear in the URL, and the group here holds no layout, so it changes nothing at runtime at all.
What it changes is the repository. app/games/arctic-shores/ already exists, with 15 entries of its own. The route group lets a second directory tree serve pages under the same URL prefix without touching that first folder, which matters when pages under one provider are being written on a different branch from the provider's own folder. Two trees, one prefix, no collision, and a ls app/games that undercounts.
It is also why a naive file-to-URL comparison reports three missing pages and three orphan URLs that are in fact the same three pages.
Three layouts
For 1,029 pages there are exactly three layout.tsx files: the root, the dashboard, the admin area. The shared public header and footer are not a layout at all; they are components, imported by the page files that want them. That is more repetition than a layout, and it is the reason a page can opt out of the site chrome by not importing it, which several of the 37 do.
The inverse holds for loading.tsx: 13 of the 14 in the repo sit behind the login, where a slow query is normal. The 991 public pages have none between them, because they are static and there is nothing to wait for.
See it
Count the sitemap yourself:
curl -s https://cogniprep.app/sitemap.xml | grep -c '<loc>' Then pick the three things above out of it. cogniprep.app/games/arctic-shores/practice-scores answers 200 and is in that list, with no folder of that path in the repo. A game runner URL is not in the list at all, though it loads for a signed-in user. And cogniprep.app/dev/game-preview/balloon answers 404 in production, which is the dev-only guard doing its job.
The audit that produced this post is two sets and a subtraction. If you run an App Router site of any size, run it: walk app/ for page.tsx, fetch your own sitemap, and print both differences. The first difference tells you what you are hiding, and the second tells you which of your file conventions you have forgotten about.