Check redirects, canonical annotations and sitemaps together
Eleven actual HTTP requests and forty assertions expose direct routes, incorrect language targets and unsuitable sitemap entries.

A redirect, a canonical annotation and a sitemap answer different questions. The redirect determines the next HTTP destination, a canonical annotation expresses a preferred representation and the sitemap identifies addresses intended for discovery. A reliable check compares those statements. We build a small fictional site with German and English service pages, collect real responses and deliberately introduce contradictions. The result is a technical contract check, not a forecast of Google's indexing decisions or search positions.
Choose the preferred address before inspecting implementation
In the teaching case, the German service page should exist at /leistungen. The older address /alt should redirect directly to it. The English version has its own address, /en/services, and both language versions should remain independently available. A fictional draft exists as an HTML response but is outside the intended public selection. This short agreement gives us explicit expectations before we start collecting status codes, head elements or XML entries from the server.
Without those expectations, a check can establish reachability but cannot reliably decide whether the destination is appropriate. Redirecting to a homepage can work technically while making a requested service hard to find. A canonical annotation can also be syntactically valid and point to the wrong content. We therefore keep intended mappings separate from observed responses. The report compares them instead of automatically treating the existing implementation as the desired outcome merely because it already returns something.
Keep the roles of three signals distinct
A server redirect is an HTTP response carrying a destination which a client can follow. A canonical annotation describes a preferred address without itself redirecting the current document. A sitemap entry replaces neither behaviour. Google's canonicalisation documentation describes redirects and canonical annotations as strong signals and sitemap inclusion as a weaker signal. An expressed preference remains different from the address Google actually selects. Agreement between technical signals is useful, but it does not grant control over an external selection process.
Our acceptance contract intentionally covers one simple consistent case. The old path reaches the German target, that target declares one absolute self-referencing canonical and the sitemap lists both current language versions. It lists neither the old path nor the draft. This is the example's chosen contract, not a universal requirement that every site must have this exact structure. Passing the check also does not force a search engine to index either artificial page or display it for any particular query.
| Signal | Our checking question | Intended teaching case |
|---|---|---|
| HTTP redirect | Where does the old path actually lead? | /alt goes directly to /leistungen using 308 |
| Canonical in the HTML head | Which address does the target declare? | One absolute self-reference |
| Sitemap | Which current pages are listed? | The German and English target pages |
| Indexing directive | Does noindex contradict the intended selection? | The draft stays outside the sitemap |

Capture the first response without following automatically
A browser can reach the destination so quickly that the first redirect is easy to miss. We therefore collect the teaching responses with automatic redirect following disabled initially. Status and Location are stored separately, and the next known address is represented by another observation. That approach preserves evidence of whether the transition really contains one step or includes an intermediate address. Looking only at the final address bar would conceal precisely the difference which the acceptance contract asks us to inspect.
The response semantics are described in RFC 9110. We choose 308 for the permanently replaced path. Other application flows can legitimately need other statuses. In particular, a GET-page check should not silently become evidence about form POST behaviour. This experiment collects ordinary GET responses and claims no complete verification of every method supported by a production system. A real application handling writes would need method-specific cases and their intended destinations.
Expose direct routes, chains and loops
The additional fictional address /kette first redirects to /alt and only then reaches the service page. The final content is available, but the route differs from the agreed direct transition. Our checker reports a warning with two redirect hops. We do not claim a measured timing penalty. The concrete concern is an additional maintenance dependency: changing the intermediate address later could alter the entire route even when the original old address and final page remain unchanged.
Two other fictional addresses point at one another. The checker follows the supplied observations and remembers every visited URL. Seeing one again identifies a loop. A configurable hop bound also prevents indefinite traversal of an ever-longer recorded chain. Both are rules of our own checking tool. They do not assert how many redirects a particular search-engine process follows in every situation. A useful report preserves the actual recorded steps so that a person can understand what triggered the finding.
Read the canonical from the collected document head
The HTML responses are parsed with a real Edge browser's DOMParser. We collect canonical link elements from the head of the captured document rather than inferring them from its visible address. The simple contract expects exactly one absolute annotation pointing at the preferred URL itself. Two different annotations therefore produce a concrete inconsistency. A missing annotation is also reported as a deviation from this particular agreement. The code does not pretend that a successful 200 response already proves that the head contains the expected declaration.
Google can support relative canonical references, while its documentation recommends absolute addresses. Our contract deliberately adopts the stricter choice. A finding must therefore remain recognisable as a project rule instead of becoming a false statement that every relative reference is technically invalid in every context. Separating general platform behaviour from a chosen acceptance condition makes the report more useful when investigating an existing system. The goal is to explain the deviation, not manufacture a larger-sounding error category.
Check the canonical destination as well
An annotation containing a well-formed URL does not establish that its destination behaves correctly. That destination might redirect, return an error or be absent from the observation set. Our checker therefore requires a known 200 response for the canonical target. A controlled negative case changes the service page's annotation to the old redirecting path. Its declared preference now disagrees with the final route. The report identifies this contradiction specifically instead of approving the link merely because it is a valid absolute address.
The program does not automatically decide whether two pages are meaningfully equivalent. A status or identifier cannot prove that two service descriptions convey the same content. A real duplicate-content assessment must examine the actual material. We do not hide that responsibility behind an invented similarity score. The teaching case only verifies the agreed mapping of already selected target pages against their observed technical statements. A broader content decision remains an editorial and information-architecture task with its own evidence.
Avoid accidentally consolidating language versions
The English teaching page has its own self-referencing canonical. In one negative case, we change only that recorded annotation so it points to the German page. The checker detects the deviation from the intended English destination. This matters because both language versions were explicitly intended to remain separate. An English page is not simply a technical duplicate because it explains the same service in another language. The actual language and content plan determines the relationship the implementation is meant to preserve.
A complete multilingual review would additionally inspect the intended hreflang relationships and return links. Those annotations do not replace a coherent canonical choice. This small checker does not evaluate them, so its report does not describe an entire multilingual site as fully accepted. For a real deployment, we would add the expected set of language versions, reciprocal references and their reachability as a separate responsibility, with negative cases which distinguish missing links from a deliberately absent translation.
Treat the sitemap as a checkable selection
The valid teaching sitemap contains exactly two entries, both representing successful current target pages. The Sitemap protocol describes XML structure, namespace and encoding requirements. We parse the actual XML response with the browser and read loc elements in the expected namespace. Successful XML parsing alone does not establish that the selected addresses are appropriate. We therefore compare the resulting list with the intended set of pages instead of stopping at a syntax-valid message.
A second artificial sitemap additionally lists the old redirecting path, a draft and an error address, while repeating an existing entry. The checker can distinguish these findings. The result is a list of specific tasks rather than a generic bad-sitemap label. An editor can remove the draft from the public selection while a developer investigates the source of the obsolete path. The report does not claim to perform either repair automatically, and the original observations remain available for comparison.
Keep noindex separate from canonical preference
Google's robots-meta documentation describes indexing directives including noindex. The fictional draft has that directive and is outside our intended public sitemap. A separate negative case places noindex on the service page which should belong to the preferred selection. The checker reports the contradiction between intended inventory and observed directive. It makes no claim that Google has actually discovered, crawled or processed this particular artificial page running only on our local test origin.
Blocked fetching and a successfully read noindex instruction are also distinct situations. Our tool does not read robots.txt and reports no completed test of its rules. It does not verify the access controls of a protected area either. An actual draft requiring protection from unauthorised readers needs real access control. An indexing directive is not authentication, and keeping an address outside a sitemap does not guarantee that someone who knows it cannot request the page directly.
Preserve the boundary of the observation set
The module below processes observations which have already been collected. It makes no network requests itself. This separation between collection and evaluation is intentional. If a destination is absent from the supplied list, the module does not invent a response status for it. It reports the missing evidence. In a real review, the team would decide whether that particular address should be collected next and whether doing so lies within the authorised scope of the checking exercise.
The origin boundary is explicit too. The teaching contract expects URLs from the same origin without credentials or fragments. An external redirect is reported as a deviation instead of being followed blindly. This is not a claim that all external redirects are wrong. It prevents our bounded local comparison from silently expanding into unrestricted requests to arbitrary destinations. A real collection tool additionally needs appropriate network and permission boundaries; the pure evaluation module does not implement those responsibilities by implication.
Read the executed checking module
Input consists of the origin, page observations, sitemap selection, preferred target addresses and intended alias mappings. Each page record keeps status, redirect destination, canonical annotations and robots values separate. Output contains issues, warnings and inspectable routes. A chain warning is consequently not confused with a route which failed to reach its intended endpoint. The team can decide how to handle warnings against its actual requirements without losing the underlying distinction in one undifferentiated red or green result.
The program deliberately implements only the described teaching contract. It does not evaluate arbitrary HTTP Link headers, PDF canonicals, client-side redirects or search-engine choices. It does not estimate content quality from response status either. Those limits make a finding useful: the reader knows which input and rule produced it. The complete module was actually executed against the artificial observations described below, so the example connects a readable implementation with concrete outputs rather than presenting unrun pseudocode as a proven tool.
export function auditSignals({origin,pages,sitemap,preferred,aliases,maxHops=5}){
const base=new URL(origin);if(!['http:','https:'].includes(base.protocol)||!Number.isInteger(maxHops)||maxHops<1||maxHops>20)throw new TypeError('Prüfkonfiguration ungültig.');
const issues=[],warnings=[],routes=[];
const url=value=>{const u=new URL(value,base);if(u.origin!==base.origin||u.username||u.password||u.hash)throw new TypeError('Nur eindeutige URLs derselben Herkunft ohne Fragment zulässig.');return u.href;};
const records=new Map;for(const p of pages){const key=url(p.url);if(records.has(key))throw new TypeError('Doppelte Beobachtung.');records.set(key,p);}
const expected=new Set(preferred.map(url));
const issue=(code,target)=>issues.push({code,url:target});
for(const target of expected){const p=records.get(target);if(!p){issue('missing_observation',target);continue;}if(p.status!==200)issue('preferred_not_200',target);if(!p.html)issue('preferred_not_html',target);if((p.robots??[]).some(x=>['noindex','none'].includes(x.toLowerCase())))issue('preferred_noindex',target);
const canon=p.canonicals??[];if(canon.length!==1){issue('canonical_count',target);continue;}
let canonical;try{if(!/^https?:\/\//i.test(canon[0]))throw new TypeError();canonical=url(canon[0]);}catch{issue('canonical_not_absolute_local',target);continue;}
if(canonical!==target)issue('canonical_not_self',target);if(records.get(canonical)?.status!==200)issue('canonical_target_not_200',target);
}
const seen=new Set;for(const value of sitemap){const target=url(value);if(seen.has(target))issue('sitemap_duplicate',target);seen.add(target);if(!expected.has(target))issue('sitemap_unexpected',target);if(records.get(target)?.status!==200)issue('sitemap_not_200',target);if((records.get(target)?.robots??[]).some(x=>['noindex','none'].includes(x.toLowerCase())))issue('sitemap_noindex',target);}
for(const target of expected)if(!seen.has(target))issue('sitemap_missing',target);
for(const [source,wanted]of Object.entries(aliases)){let current=url(source),end=url(wanted),hops=0;const visited=new Set,steps=[];let failed=false;
while(true){if(visited.has(current)){issue('redirect_loop',current);failed=true;break;}visited.add(current);const p=records.get(current);if(!p){issue('redirect_missing_observation',current);failed=true;break;}steps.push({url:current,status:p.status});if(![301,302,303,307,308].includes(p.status))break;if(hops>=maxHops){issue('redirect_limit',current);failed=true;break;}if(!p.location){issue('redirect_location_missing',current);failed=true;break;}if(![301,308].includes(p.status))warnings.push({code:'temporary_alias',url:current});try{current=url(new URL(p.location,current).href);}catch{issue('redirect_external_or_invalid',current);failed=true;break;}hops++;}
if(!failed&&(current!==end||records.get(current)?.status!==200))issue('redirect_wrong_final',url(source));if(hops>1)warnings.push({code:'redirect_chain',url:url(source),hops});routes.push({source:url(source),wanted:end,final:current,hops,steps,failed});
}
return{passed:issues.length===0,issues,warnings,routes};
}
const origin='https://beispiel.invalid';
const result=auditSignals({
origin,
preferred:['/leistungen','/en/services'],
aliases:{'/alt':'/leistungen'},
sitemap:[origin+'/leistungen',origin+'/en/services'],
pages:[
{url:origin+'/alt',status:308,location:'/leistungen'},
{url:origin+'/leistungen',status:200,html:true,canonicals:[origin+'/leistungen'],robots:[]},
{url:origin+'/en/services',status:200,html:true,canonicals:[origin+'/en/services'],robots:[]}
]
});
console.log(JSON.stringify(result));
Understand what the forty assertions establish
The experiment started our own HTTP server on an available loopback address. We actually fetched nine teaching addresses and two sitemap versions. HTML and XML were then processed with DOMParser in Edge 157.0.4341.42. The evaluator ran with Node.js 24.22.1. Forty assertions passed. In the valid case, the old path's recorded route contained 308 followed by 200, and the good sitemap contained the two intended target entries. These observations belong to a controlled fixture rather than a scan of a real customer domain.
The negative cases deliberately modify copies of those observations. They include a missing canonical, two annotations, a relative destination, an incorrect language mapping and a canonical target which redirects. Additional cases check noindex, missing sitemap entries, a loop, the hop limit and an external Location. These record modifications are not presented as extra real page requests. The report distinguishes eleven actual HTTP fetches from the controlled evaluation cases performed afterwards, preserving the meaning of both kinds of evidence.
| Observation or negative case | Own finding | Appropriate next task |
|---|---|---|
| /alt reaches /leistungen directly | One hop, target 200 | Compare with the intended replacement |
| /kette reaches the target through /alt | Two-hop warning | Review the intermediate dependency |
| English canonical points at the German page | Deviation from its intended self-reference | Resolve the language mapping |
| Draft appears in the bad sitemap | Unexpected URL marked noindex | Correct the selection source |
| Redirects form a cycle | Loop | Choose the intended final destination |
Use modification dates meaningfully
An optional lastmod value should describe a genuine relevant change. Google's sitemap-building guidance explains that it ignores priority and changefreq and uses lastmod when the value is consistently meaningful and accurate. Our small teaching sitemap omits those optional properties. We do not manufacture a precise-looking modification history which the experiment does not possess. A syntactically valid date would add little if nobody can explain what event it actually represents.
A real editorial workflow should define which changes justify a new modification time and where that time can be obtained reliably. Inserting today's date whenever the sitemap is generated initially describes generation, not necessarily a change to the linked page. The check should therefore examine the meaning of the value rather than count its presence as a quality indicator. Significant content updates and a regenerated list are different events, even when they happen to occur on the same day.
Handle parameters and old links deliberately
A real historical address may contain additional query values. Some describe an important selection, while others only record campaign provenance. A blanket rule which removes every query parameter could lose meaningful state. Our teaching fixture intentionally has no such variants. An extension should first collect concrete old addresses and expected destinations, then decide what each parameter means. That is more reliable than adding an attractive-looking normalisation rule before anyone has established which information must survive the transition.
Internal links should also use the intended current target. If a service overview still links to the old address, the redirect remains necessary even after both page bodies have been updated. Technical transition and editorial link maintenance are therefore separate tasks. A successful check of the old route does not establish that every navigation item and content link already points directly at the preferred destination. This small module contains no comprehensive link crawler and does not report completion of that broader work.
Record observations as a dated snapshot
Collected responses belong to a particular run. A subsequent server change can make the same address respond differently. The report should retain collection time and underlying observations. In our controlled fixture, the real responses are stored in a result file. Negative cases work on copies rather than silently altering the records described as the original observations. That separation allows a later reader to distinguish what was actually served from what was deliberately changed to challenge the evaluator.
The same distinction helps a real transition: keep the earlier state, intended change and renewed verification separate. When a finding is unexpected, the team can compare a specific initial response with its later replacement. A green overall indicator without stored routes would be less informative. Retaining evidence does not turn a small probe into a complete assessment of every possible address, however. Scope and collection time remain part of the result, alongside the rule which classified the observation.
Connect the report to actual publication
A local fixture does not replace checking the deployed system. Domain rules, an upstream service or older configuration can produce a different first response. After a real change, the affected old and new addresses should therefore be read externally again. For multiple languages, that includes every intended version. Collection remains within authorised scope and does not modify external content or search-engine accounts. A useful final report identifies the deployed responses it actually checked instead of treating an isolated demonstration as production evidence.
A successful technical contract check is documented as exactly that. Whether and when a search engine selects a preferred address remains unproven. A later Search Console observation would be different evidence with its own collection time and meaning. Keeping these layers distinct makes client communication more reliable. The team can explain what it delivered and verified without offering a seemingly technical guarantee about an external indexing or ranking process which the checking module neither observes nor controls.
Checklist for a consistent transition
- Define preferred addresses and intended aliases before collecting responses.
- Record the first HTTP status and every Location separately.
- Distinguish direct routes, chains and loops.
- Read canonicals from actual responses and check their destinations.
- Map language versions according to the content plan.
- Compare sitemap selection with current successful target pages.
- Keep drafts, noindex and real access protection separate.
- Report unknown destinations as missing evidence instead of invented statuses.
- Distinguish controlled negative cases from actual HTTP fetches.
- Verify the deployed route again after publication.
The signals fit together when they support the same intended mapping while retaining their different roles. An old path leads to an appropriate target, the target states a coherent preference and the sitemap lists the current addresses which the team actually intends to expose. That inspectable agreement is the purpose of this example. It gives a reviewer concrete evidence and a clear correction path without turning a local technical check into an indexing or ranking promise.








