PRACTICAL GUIDE / Playwright test XML sitemap
Test XML Sitemaps with Playwright
Use Playwright test XML sitemap checks to validate status, content type, canonical host, duplicate URLs, discoverability, and protocol rules in CI.
In this guide11 sections
- What Should a Playwright XML Sitemap Test Verify?
- How Is the Sitemap Generated in This Repository?
- Why Is a 200 Response Not Enough?
- A Numbered Workflow for Testing an XML Sitemap
- Code Example: Parse and Validate Every loc Element
- How Do You Compare Sitemap Coverage with Public Routes?
- How Do You Detect Duplicate and Noncanonical URLs?
- Protocol Checks vs Discoverability Checks
- How Should Dynamic Profiles and CI Be Handled?
- Frequently Asked Questions About Playwright Sitemap Tests
- Can Playwright parse XML sitemap files?
- Should a sitemap test assert the exact number of URLs?
- How do I find duplicate URLs in a sitemap?
- Should sitemap URLs use one canonical hostname?
- How do I test routes that depend on a database?
- Is a valid sitemap proof that every URL will be indexed?
- Next Steps: Add Sitemap Policy to CI
What you will learn
- What Should a Playwright XML Sitemap Test Verify?
- How Is the Sitemap Generated in This Repository?
- Why Is a 200 Response Not Enough?
- A Numbered Workflow for Testing an XML Sitemap
Playwright test XML sitemap checks should request /sitemap.xml, require a successful XML response, parse every <loc>, enforce absolute URLs on the expected canonical host, reject duplicates, and compare the result with the public route inventory. A status of 200 is only the first check. An HTML error document, an empty urlset, a preview hostname, a stale article route, or a private application URL can all arrive with a successful response and still make the sitemap wrong.
Playwright is useful here because one test can combine response evidence with repository policy. APIRequestContext retrieves the generated file without opening a visible page. Browser evaluation can parse XML with DOMParser. The test can then compare protocol structure, URL identity, and expected content coverage in one controlled workflow.
What Should a Playwright XML Sitemap Test Verify?
A complete test has three layers. The transport layer checks status and content type. The protocol layer checks XML parsing, urlset, namespace, required url entries, and loc values. The product layer checks which routes must appear, which routes must never appear, and which hostname represents the site.
The sitemaps protocol requires an XML urlset using the sitemap namespace, with a url parent and required loc child for each entry. Values must be escaped and the file must use UTF-8. The protocol also defines optional lastmod, changefreq, and priority elements. Tests should not make optional values mandatory unless the application contract includes them.
The Google sitemap guidance recommends absolute URLs and including the URLs a site wants represented in search. That makes canonical identity and inventory selection part of the test design. It does not turn sitemap inclusion into an indexing guarantee.
Check the namespace through the parsed root's namespaceURI, not only through a text search for the namespace string. A document can contain the right text in the wrong place. Select url and loc in a way that respects namespaces, and reject parser-error documents before extracting values. Also verify that each selected url has one nonempty loc; a global list of locations can hide a malformed entry with no required child.
Escaping belongs to parsing evidence. An ampersand in a query value must be represented correctly in XML, while the parsed text should become the intended URL. Do not pre-replace entities before DOMParser sees them. If parsing fails, report the serializer defect. If parsing succeeds but new URL() rejects the resulting text, report URL construction separately.
This scope differs from general Playwright API testing, where endpoint payloads, authentication, and workflow setup are central. A sitemap is a public discovery artifact with a specific XML contract and URL policy.
How Is the Sitemap Generated in This Repository?
src/app/sitemap.ts exports the default async sitemap() function. It assembles staticPages, blogPages, and roadmapPages, then attempts to add public user profiles. Every blog URL comes from getAllPosts(), so adding a valid Markdown article automatically adds its route. Roadmaps come from getAllRoadmaps().
The database query selects up to 1000 usernames for users who are not banned. If that query fails, the catch branch returns static, blog, and roadmap pages without profiles. A sound test must respect this contract instead of pretending dynamic profile membership is always available.
The exact implementation names matter during review. src/app/sitemap.ts calls the optional query result publicProfiles. In e2e/public.spec.ts, the response text is sitemapBody, and the matched article locations are publicGuideUrls. These symbols connect each proposed assertion to the existing production and test evidence.
The static array currently names home, blog, battles, reels, roadmaps, leaderboards, privacy, and terms routes with declared frequency and priority values. Blog entries use post.updatedAt for lastModified, weekly frequency, and priority 0.7. Roadmap entries use their update date, monthly frequency, and priority 0.8. Profile entries encode usernames, use daily frequency, and carry priority 0.4. Those details are testable where they express deliberate product policy, but URL presence remains the highest-value baseline.
The catch branch is not an empty-sitemap fallback. It guarantees that three deterministic groups survive an unavailable database: static pages, all registered blog posts, and all registered roadmaps. Write a direct unit or integration check for sitemap() if the database failure can be injected safely. At the browser endpoint, require the deterministic invariant and avoid inferring whether the database succeeded merely from a profile's absence.
| Route group | Source | Deterministic in content-only CI | Database dependency | Test policy |
|---|---|---|---|---|
| Static pages | literal staticPages array | Yes | None | require known critical routes |
| Blog pages | getAllPosts() | Yes | None | compare with Markdown inventory |
| Roadmaps | getAllRoadmaps() | Yes | None | compare with roadmap inventory |
| Public profiles | users query | No | Yes | validate shape under seeded data |
The installed Next.js sitemap file convention confirms that sitemap.ts returns MetadataRoute.Sitemap entries and Next.js serializes them as /sitemap.xml. The current framework docs also describe the file as a cached special route unless request-time behavior changes that status.
Why Is a 200 Response Not Enough?
Status says the server handled the request, not that the body is the intended document. A fallback route can return an HTML page with 200. A proxy can replace the body. An application can generate valid XML containing development hosts. The route list can be empty because content registration failed. Optional database work can throw before the intended fallback is reached.
Content type is also evidence, but not a substitute for parsing. Require a response type consistent with XML, then parse the body. If the type is wrong but the body parses, the server contract is still wrong. If the type is correct but parsing fails, the document contract is wrong.
Inspect the final response URL when routing infrastructure can redirect /sitemap.xml. A redirect to a login page, locale route, or generic document may still end with status 200. Verify that the response represents the intended public endpoint before interpreting its body. Keep redirect policy explicit: following one configured canonical-host redirect can be valid in a deployed smoke check, while redirecting to an HTML application shell is not.
An empty but well-formed urlset is another successful transport failure. Require a nonzero deterministic core and, where the content inventory is available, an exact blog set. A minimum count without membership can pass after replacing real routes with unintended ones. Membership without a prohibited-route check can pass while private pages leak into the file.
Avoid using one large text snapshot. URL ordering may change without semantic impact, and modification dates change legitimately. Convert parsed entries into a data structure and assert rules. Preserve the raw response as a failure attachment when diagnosis needs the exact serialized XML.
The download content validation guide applies the same principle to files: validate identity and content, not only transfer success. For sitemap responses, Playwright APIResponse supplies status, headers, URL, and text methods needed at the transport boundary.
A Numbered Workflow for Testing an XML Sitemap
Use the following sequence:
- Request
/sitemap.xmlthrough the Playwrightrequestfixture. - Require an OK response and record the final response URL.
- Assert an XML-compatible content type before interpreting the body.
- Read the body once and parse it with an XML parser.
- Require one
urlsetroot in the sitemap namespace. - Extract every direct
url > loctext value. - Reject empty values, duplicates, relative URLs, fragments, and disallowed hosts.
- Compare deterministic blog and roadmap inventories with the extracted URL set.
- Require critical public routes and reject private application routes.
- Report optional profile entries separately from deterministic core coverage.
Each step produces a smaller failure class. A response failure points to routing or server execution. A parser failure points to serialization. A namespace failure points to protocol output. A host failure points to environment configuration. A missing route points to registration or inventory assembly.
Do not silently normalize case, trailing slashes, or percent encoding before duplicate checks. Exact duplicate detection should inspect what was published. A second policy pass can parse each URL and compare normalized components where the product has an explicit equivalence rule.
Keep raw and parsed forms together during validation. The raw text is needed to report exact duplicates and serialization differences. The parsed URL supplies protocol, hostname, path, query, and fragment fields. If canonical policy treats two spellings as equivalent, report that as a semantic duplicate in a second result rather than replacing the raw duplicate result. This distinction makes fixes obvious: serializer duplication differs from two route generators producing alternate forms.
Order checks last unless ordering is part of product behavior. The sitemap protocol does not make the sequence a discoverability guarantee. Sorting entries inside a test before set comparison avoids failures caused by harmless collection order, while direct per-entry checks still validate each item's fields. Never sort the source text and call it XML validation.
Code Example: Parse and Validate Every loc Element
Node test environments do not always expose DOMParser, while a Playwright page does. The example passes the response text into page evaluation and returns plain strings.
import { expect, test } from "@playwright/test";
test("sitemap follows the XML protocol and canonical host policy", async ({
page,
request,
}) => {
const response = await request.get("/sitemap.xml");
expect(response.ok()).toBe(true);
expect(response.headers()["content-type"]).toContain("xml");
const xml = await response.text();
const parsed = await page.evaluate((source) => {
const document = new DOMParser().parseFromString(source, "application/xml");
const parserError = document.querySelector("parsererror")?.textContent;
const root = document.documentElement;
const entries = Array.from(root.children)
.filter(
(node) =>
node.localName === "url" && node.namespaceURI === root.namespaceURI,
)
.map((urlNode, index) => {
const directLocations = Array.from(urlNode.children).filter(
(node) =>
node.localName === "loc" &&
node.namespaceURI === root.namespaceURI,
);
return {
index,
locationCount: directLocations.length,
location: directLocations[0]?.textContent?.trim() ?? "",
};
});
return {
parserError,
rootName: root.localName,
namespace: root.namespaceURI,
entries,
};
}, xml);
expect(parsed.parserError).toBeUndefined();
expect(parsed.rootName).toBe("urlset");
expect(parsed.namespace).toBe("http://www.sitemaps.org/schemas/sitemap/0.9");
expect(parsed.entries.length).toBeGreaterThan(0);
for (const entry of parsed.entries) {
expect(
entry.locationCount,
`sitemap url entry ${entry.index} must contain exactly one direct loc`,
).toBe(1);
expect(
entry.location,
`sitemap url entry ${entry.index} has an empty loc`,
).not.toBe("");
}
const locations = parsed.entries.map((entry) => entry.location);
const duplicates = locations.filter(
(value, index, all) => all.indexOf(value) !== index,
);
expect(duplicates, `duplicate sitemap URLs: ${duplicates.join(", ")}`).toEqual([]);
for (const value of locations) {
const url = new URL(value);
expect(url.origin).toBe("https://qabattle.com");
expect(url.port).toBe("");
expect(url.hash).toBe("");
}
});This parser checks structure before URL policy. It walks each direct url child, requires exactly one nonempty direct loc, and then enforces the exact HTTPS origin with no port. It does not use a regular expression to parse XML. A narrow expression can still be useful for a regression count against known generated output, as e2e/public.spec.ts currently does for blog <loc> elements, but structural parsing should own protocol claims.
How Do You Compare Sitemap Coverage with Public Routes?
The strongest coverage check derives expected routes from the same public content inventory available to the test, but through an independently readable boundary. In this repository, e2e/public.spec.ts counts Markdown files in content/blog as expectedGuideCount. It then counts serialized blog <loc> elements and compares totals.
Count comparison is useful but incomplete. Two defects can cancel each other: one missing article and one unintended route still produce the expected total. Add set membership. Build expected article URLs from filenames, compare every expected URL with the parsed set, and identify unexpected blog URLs.
Derive a slug from the filename only because src/lib/blog.ts enforces the same filename-to-frontmatter relationship when loading posts. If the repository later supports drafts, nested content, or unpublished status, the expected inventory must use that publishing contract instead of every Markdown file. Tests should represent public registration rules, not an incidental directory listing that happens to work today.
Compare route families independently. A missing blog URL should not be hidden by an extra profile, and a roadmap discrepancy should not change the expected article count. Group parsed paths into static, blog, roadmap, profile, and unknown buckets, then report missing and unexpected values in each. Unknown public paths deserve review because they may be a new intended group or accidental leakage.
import fs from "node:fs";
import path from "node:path";
import { expect } from "@playwright/test";
const expectedBlogUrls = fs
.readdirSync(path.join(process.cwd(), "content/blog"))
.filter((file) => file.endsWith(".md"))
.map((file) => `https://qabattle.com/blog/${file.replace(/\.md$/, "")}`);
export function expectBlogCoverage(actualLocations: string[]) {
const actual = new Set(actualLocations);
const missing = expectedBlogUrls.filter((url) => !actual.has(url));
const unexpected = actualLocations.filter(
(url) => url.includes("/blog/") && !expectedBlogUrls.includes(url),
);
expect(missing, `missing blog URLs: ${missing.join(", ")}`).toEqual([]);
expect(unexpected, `unexpected blog URLs: ${unexpected.join(", ")}`).toEqual([]);
}Keep explicit policy assertions beside inventory comparison. The existing public test requires several representative guides and rejects /app/battles/. Those checks communicate intent even if a future refactor changes how expected sets are built.
Use TypeScript environment configuration to keep the canonical site host explicit. Do not build expected public URLs from PLAYWRIGHT_BASE_URL, because that value may be localhost or a preview deployment.
How Do You Detect Duplicate and Noncanonical URLs?
Duplicate checks should report actual repeated values. A Set length assertion says duplication exists but can leave the engineer searching through a large file. Group values by exact string and print entries whose count exceeds one.
Canonical checks need a written URL policy. For this repository, generated public routes use SITE_URL. Assert that parsed URLs use the expected production host, have no fragment, and belong to intended public path families. Query strings are not automatically wrong, but each allowed query form should have a reason. The current generated route groups do not construct query strings.
Cross-host entries are protocol concerns as well as product concerns. Reject preview domains, localhost, alternate www forms not used by canonical policy, and unexpected subdomains. A TLS-focused suite can inspect response security separately; API TLS details with Playwright covers that boundary. The sitemap test only needs enough protocol checking to reject a nonproduction public URL.
Test lastmod by meaning, not by freshness against the current clock. For blog pages, src/app/sitemap.ts uses post.updatedAt. An exact article-specific test can compare the serialized date with frontmatter. Do not assert that every entry changed recently, because old unchanged content can have an old valid modification date.
Protocol Checks vs Discoverability Checks
The test report should identify whether a failure violates the shared sitemap format or this application's route policy.
| Check | Layer | Evidence | Failure owner |
|---|---|---|---|
| XML parses | Protocol | no parser error | serializer or framework integration |
urlset namespace | Protocol | exact namespace URI | sitemap output |
absolute loc | Protocol and policy | new URL succeeds | route generation |
| expected host | Product policy | protocol and hostname match | site configuration |
| no duplicates | Product quality | exact values unique | route inventory |
| every article present | Discoverability | expected set is subset | blog registration |
| private route absent | Indexability policy | forbidden set has no match | sitemap selection |
| fallback core remains | Resilience | static groups still present | database boundary |
Generic REST API status code guidance helps with transport meaning, while Postman API testing offers another client workflow. Neither replaces XML and route-set assertions. Likewise, JSON Schema validation applies to JSON contracts, not sitemap XML.
If repeated sitemap assertions appear across projects, package them as custom Playwright matchers. Return clear diagnostics such as missing routes, duplicate values, and invalid hosts. A matcher that only returns true or false discards the evidence needed for CI triage.
How Should Dynamic Profiles and CI Be Handled?
The repository intentionally keeps sitemap generation available when the profile query fails. Tests should preserve that behavior. In a database-free environment, require static, blog, and roadmap routes. In a seeded integration environment, add known public profiles and verify their encoded paths. Do not require an arbitrary production profile total.
CI should run deterministic sitemap checks on every relevant change: content registration, site URL configuration, framework upgrades, and sitemap code. A deployed smoke test can request the public sitemap and repeat host plus critical-route checks. This catches deployment configuration differences without making external indexing part of the build.
When local services are required, Playwright webServer orchestration explains controlled startup and readiness. CI containers and browser caches addresses stable runner setup. Keep those operational concerns outside sitemap assertions so a failure says whether the artifact or the environment is wrong.
Preserve the raw XML on failure, but avoid storing unrelated user data longer than needed. Public profile URLs can still reveal identifiers. The test report should show only the entries needed to explain a policy violation.
Use controlled seed users for the profile branch. Include one ordinary username, one value that requires URL encoding, and one banned user that must not appear. Assert the known entries and exclusion without asserting a production total. Because the query applies a limit, the suite should not make claims about profiles beyond its seed dataset or interpret the limit as a complete external inventory guarantee.
Test database failure at the closest controllable boundary. A test that deliberately breaks a shared database during a parallel browser run can disrupt unrelated cases. Prefer an isolated function test with an injected or mocked query seam when available, then let the endpoint test enforce the fallback's observable core routes in its normal environment. Keep the failure simulation deterministic and local to the worker.
CI artifacts should include response status, content type, final URL, parser error, and concise route differences. Store the complete XML only when repository policy allows the listed public identifiers in artifacts. For a deployed smoke check, print a bounded sample around failures rather than every profile location. The useful evidence is which invariant failed, not the size of the log.
When a content file is added, the registration path should update the sitemap without a hand-edited index. A missing new article therefore points to frontmatter parsing, content discovery, or build freshness. An unexpected removed article can indicate a stale deployment. These are different from protocol failures and should appear under a coverage label in the report.
Frequently Asked Questions About Playwright Sitemap Tests
Can Playwright parse XML sitemap files?
Yes. Retrieve the file with request.get, then use a standards-aware parser. A browser page provides DOMParser, and Node packages are another option when already approved by the project. Parse errors, root identity, namespace, and loc extraction should be separate assertions.
Should a sitemap test assert the exact number of URLs?
Pin counts for deterministic inventories and supplement them with set comparison. Avoid one unexplained global count when database-backed entries vary. Exact membership gives better diagnostics and prevents a missing route from being hidden by one unintended route.
How do I find duplicate URLs in a sitemap?
Compare exact location values before normalization, group repeated strings, and include them in the assertion message. Then perform URL parsing and canonical policy checks. This keeps duplicate serialization and semantic URL equivalence as separate issues.
Should sitemap URLs use one canonical hostname?
The sitemap should follow the site's chosen public identity and the applicable same-host protocol rules. Assert the production host directly. Local server and preview origins are test infrastructure, not public canonical values, so they should not leak into generated locations.
How do I test routes that depend on a database?
Split deterministic core coverage from controlled dynamic coverage. Verify fallback route groups without a database. Verify known dynamic profiles with seed data in a separate environment. Do not convert an optional, changing collection into a fixed fabricated count.
Is a valid sitemap proof that every URL will be indexed?
No. It gives crawlers discovery information and preferred URLs. Search systems make separate crawl and index decisions. A Playwright test proves file correctness and application policy, which is valuable release evidence without claiming control over an external system.
Next Steps: Add Sitemap Policy to CI
Begin by replacing body-only checks with XML parsing, then retain the repository's explicit representative inclusions and private-route exclusion. Add exact membership comparison for Markdown articles. Keep profile assertions in seeded integration coverage and preserve the database-failure contract.
Finally, run a small deployed smoke test against the production artifact. Assert XML structure, canonical host, critical route presence, and duplicate absence. This gives the team a direct answer when sitemap generation changes: whether the public discovery file is readable, policy-correct, and complete for deterministic content.
Keep the failure output organized by layer. Report transport status and content type first, XML parser failures second, URL policy failures third, and inventory differences last. Include bounded lists of missing, unexpected, duplicate, and invalid locations. That order lets a reviewer distinguish a broken endpoint from a valid document with incomplete route coverage without reading the full response.
Revisit the checks whenever src/app/sitemap.ts, blog discovery, public route groups, or the configured site identity changes. A new deterministic route should enter the expected set with its owning feature. A new database-backed route should receive seeded coverage and a stated fallback rule. The suite then remains an executable description of what QABattle intentionally publishes.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official sitemaps.org reference
sitemaps.org
Primary documentation selected and verified for the claims in this guide.
- 03Official developers.google.com reference
developers.google.com
Primary documentation selected and verified for the claims in this guide.
- 04Official nextjs.org reference
nextjs.org
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Can Playwright parse XML sitemap files?
Yes. Request the sitemap with Playwright, read the response text, and parse it with a real XML parser such as DOMParser in a browser page. Keep response checks and XML checks separate. Regular expressions can count a narrow known pattern, but they should not be the primary parser for XML structure.
Should a sitemap test assert the exact number of URLs?
Assert an exact count only for a deterministic route group whose source inventory is available to the test. Dynamic profiles or database-backed routes may need a minimum or set-membership assertion instead. The strongest check compares each known content item with its expected URL rather than pinning one unexplained total.
How do I find duplicate URLs in a sitemap?
Extract every loc text value, preserve its exact serialized form, and compare the array length with a Set built from those values. Report the repeated URLs, not only the count difference. Parse each value with URL afterward so duplicate detection and URL-policy validation remain distinct diagnostic steps.
Should sitemap URLs use one canonical hostname?
A standard sitemap file normally lists URLs from one host, and the application should publish the hostname chosen by its canonical policy. Assert the expected protocol and host explicitly. Do not derive the expected production host from a local Playwright base URL, because localhost is an execution origin rather than public identity.
How do I test routes that depend on a database?
Separate the deterministic core from optional database results. Verify static, blog, and roadmap routes even when the data source is unavailable, then test dynamic entries in an environment with controlled seed data. Avoid inventing a fixed profile count when the repository contract only promises a bounded query and a fallback.
Is a valid sitemap proof that every URL will be indexed?
No. A sitemap helps discovery and expresses preferred URLs, but search engines still crawl, evaluate, and select pages independently. Your test can prove valid XML, allowed URL policy, and intended inventory coverage. It cannot promise crawl timing, index inclusion, ranking, or a particular search presentation.
RELATED GUIDES
Continue the learning route
GUIDE 01
Playwright API Testing: Validate APIs Inside Your E2E Suite
Playwright API testing guide covering request contexts, setup flows, assertions, authentication, fixtures, UI strategy, and CI-ready checks.
GUIDE 02
Assert API TLS Security Details with Playwright
Learn Playwright API response TLS security details with working code, failure cases, debugging steps, and CI evidence for reliable QA automation.
GUIDE 03
Validate Download Names, Content, and Failures in Playwright
Capture Playwright downloads without races, inspect suggested names and bytes, verify business content, handle failures, and keep parallel tests isolated.
GUIDE 04
Custom Playwright Matchers: Extend and Merge expect Safely
Create retry-aware Playwright custom matchers, merge expect modules without collisions, and preserve useful negation, timeout, and failure output.
GUIDE 05
Orchestrate Multiple Local Services with Playwright webServer
Configure Playwright multiple webServer processes with explicit readiness, stable ports, local reuse, named logs, environment isolation, and graceful shutdown.
GUIDE 06
Type-Safe Environment Configuration for Playwright Test
Create type-safe Playwright environment configuration with validated inputs, explicit base URLs, protected secrets, and predictable CI commands.