Playwright Route Testing for Next.js: How I Test a 280-Page Site
Throughout the past month, I’ve been building a free guitar-learning site with interactive tools and blogs among other things. At present, it has hundreds of routes, and I needed something to test the pages before deploying.
So I added a small Playwright workflow. It checks every route in the sitemap, opens some pages in a browser, and runs after the production build to catch any issues.
I think I’d need more than 7 tests to claim it catches all issues, but it’s quite useful for obvious deployment issues.
Why I decided to add Playwright
Generated 22 documents in .contentlayer
✓ Creating an optimized production build
✓ Compiled successfully
✓ Collecting page data
✓ Generating static pages (284/284)
Build error occurred
Error: ENOENT: no such file or directory, rename
'C:\Users\pc\Code\tonerune\.next\export\newsletter\2026-q3.html'
-> 'C:\Users\pc\Code\tonerune\.next\server\pages\newsletter\2026-q3.html'I was greeted by this interesting build failure. Hmm.. the route was generating both original slug and lowercase version.
The dynamic route was also deliberately generating both the original slug and a lowercase version:
/newsletter/2026-Q3
/newsletter/2026-q3I don’t really need duplicates, but fine, right? But, those names mean the same thing on Windows. Hence the error.
I just had to rename the newsletter filename to lowercase and have one canonical lowercase route. Easy.
But, how do I check what routes failed to load everytime I add a new feature? A page could work in dev while a generated slug or path fails in prod. Our team is lean; we don’t have a QA engineer.
“Is the site working?” is several different questions
| Question | Useful check |
|---|---|
| Can the project compile? | Prod build |
| Do the published routes respond? | Sitemap route test |
| Does browser render critical pages? | Playwright smoke test |
| Does a button open the expected panel? | Browser interaction assertion |
My current Playwright setup does what I need. Playwright is a browser automation and end-to-end testing framework. It can use Chromium, Firefox, and WebKit through one testing API, and its runner includes features like locators, projects, traces, screenshots, and fixtures. [1]
I don’t really care about edge-cases here; I simply want it to check routes in sitemap and check a few pages to know they work fine.
My route check looks like:
import { test, expect } from '@playwright/test'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const sitemapPath = join(process.cwd(), 'public', 'sitemap.xml')
const sitemapXml = readFileSync(sitemapPath, 'utf8')
const routes = [...sitemapXml.matchAll(/<loc>(https?:\/\/[^<]+ )<\/loc>/g)].map(
([, value]) => new URL(value).pathname
)
test.describe('sitemap routes', () => {
for (const route of routes) {
test(`${route} responds successfully`, async ({ request }) => {
const response = await request.get(route)
expect(response.ok()).toBeTruthy()
})
}
})I think it’s a good starting point for now.
Working scripts look like:
{
"scripts": {
"test": "jest",
"test:routes": "playwright test tests/sitemap-routes.spec.ts",
"test:smoke": "playwright test tests/critical-pages.spec.ts",
"test:predeploy": "npm run build && playwright test"
}
}npm test: unit tests and component level checks
npm run test:routes runs sitemap route check
npm run test:smoke runs checks for selected pages
npm run test:predeploy runs prod build and launches Playwright suite
I mostly run the smoke tests, then predeploy before commits.
My tests look like this:
import { test, expect } from '@playwright/test'
test('capo chart renders properly', async ({ page }) => {
await page.goto('/tools/guitar-capo-chart')
await expect(
page.getByRole('heading', { name: /guitar capo chart/i })
).toBeVisible()
await expect(
page.getByRole('button', { name: /sharps/i })
).toBeVisible()
await expect(
page.getByRole('button', { name: /flats/i })
).toBeVisible()
})Why.
I think it’s important to ask why even use Playwright. I don’t have Docker in the current project because I decided against it.
The codebase works perfectly at present, but over the years, I’m sure it will grow to a thousand URLs, some dynamically rendered, many with contentlayer or something similar; so having playwright works as a sanity check.
Generated 22 documents in .contentlayer
✓ Generating static pages (283/283)
✓ Sitemap generated with 219 URLs
7 Playwright tests passedI use a minimal workflow.
But, Playwright can capture screenshots and traces where test fails, run projects at different viewport sizes, and more. It can also interact with social platforms, so it’s very powerful.
My current workflow
I usually run the prod build, have Playwright test the sitemap routes and smoke tests, fix issues if any, and push.
That’s:
npm run test:predeployIt’s boring, so it works. I also have unit tests with jest so I don’t really need to test everything, just knowing it renders is good enough for now.
Takeaway
When building projects, it’s a fantastic idea to separate concerns:
- dev server
- prod build
- public routes
- browser interactions
- speed
- features being accurate and useful (with Google’s HCU update, this is more important than ever).
If you have a project that’s getting big and need to test routes, Playwright may be a good idea.
Footnotes
- Playwright documentation is a good place to start.