1. Interaction
  2. Linking
  3. Links you can trust
  4. Links You Can Trust

Interaction · Linking 06

Links You Can Trust

A link is a promise about where a click will go, and the promise is only as good as the address behind it. Addresses can be dressed up to look like somewhere else, hidden behind short links, loaded with tracking data, or simply stop working. This page is about reading a link before following it, publishing links that stay honest and keep working, showing another site's page inside your own, and what changes when a script takes over the links.

The earlier pages in this section, from the element to links people can use, looked at links from the author's side. This one looks from both sides. A reader needs to know how a URL can mislead them. An author needs to know what makes a link trustworthy: an address that says where it goes, no surprise detours, no data about the reader riding along, and a destination that still exists in ten years. The URLs page covers the address itself, including URL security and cool URIs; this page builds on both rather than repeating them.

1. Reading a URL before following it

Most people judge a link by its text, and the text can say anything. The address underneath is the only thing that decides where a click goes, and within the address, one part matters far more than the rest: the host, and within the host, the registrable domain.

Read the host from the right

A host name is a list of labels separated by dots, and its authority runs from right to left. In www.cse134.site, site is the top-level domain, cse134.site is the name someone registered and paid for, and www is a subdomain that the owner of cse134.site created, as they can create any other. The registered part, one label plus its public suffix, is the registrable domain, and it is the answer to the question that matters: who controls this server?

The public suffix is not always one label. Anyone can register a name under co.uk or github.io, so the registrable domain of news.bbc.co.uk is bbc.co.uk, three labels. Browsers know which suffixes are public from the Public Suffix List, a shared file of several thousand entries, and use it for cookies, for the "site" in same-site rules and for what they emphasize in the address bar.

Everything to the left of the registrable domain is chosen by its owner. That is the whole of the subdomain trick: an attacker who owns account-check.example can create example.com.account-check.example, or login.yourbank.com.account-check.example, and the familiar name at the start is just a label. People read left to right and stop when they recognize something, so the trick works.

Which part of a URL decides where it goesTwo URLs split into labeled boxes. The first, https://example.com.account-check.example/login, has the scheme, then example.com marked as a subdomain chosen by the owner, then account-check.example highlighted as the registrable domain that decides who runs the server, then the path. An arrow under the host points right to left, labeled read from here. The second, https://example.com@login.example.net/verify, has example.com marked as userinfo, a username, followed by the at sign, then login.example.net highlighted as the real host.https://example.com.account-check.example/logina subdomain its owner choseregistrable domain: who runs itread the host from here, right to lefthttps://example.com@login.example.net/verifyuserinfo: a username, not a sitethe real hostIn both, the name a reader recognizes is the part that does not decide anything.
The familiar name comes first in both addresses, and decides nothing. The registrable domain, read from the right, says who runs the server.

Lookalikes and typosquats

The registrable domain itself can be the disguise. A lookalike domain differs from a real one by something easy to miss: a digit 1 for a lowercase l, rn for m, an added hyphen, or .co for .com. A typosquat is a domain registered to catch people who mistype the real one. Both are cheap to register and, with a free certificate, show the same padlock as the real site. The padlock means the connection is encrypted to whoever owns that name. It says nothing about who that is.

The userinfo trick

The URL syntax allows a username and password before the host, separated from it by @: https://user:password@host/. It is now mostly a way to put a misleading name at the front of an address. In https://bank.com@evil.example/, bank.com is a username, and the request goes to evil.example. Browsers have grown wary of it. They never send the userinfo in a Referer, they hide it in the address bar, and some show a warning before following such a link: Firefox asks whether you meant to sign in to evil.example as bank.com. In an email or a chat message, nothing hides it, and the first thing a reader sees is the name they trust.

Homographs and punycode

Domain names can be written in most of the world's scripts: 例え.jp and bücher.example are valid. Underneath, DNS still only carries ASCII, so each non-ASCII label is converted to a form called punycode and given the prefix xn--. bücher becomes xn--bcher-kva. The URL API does the conversion, as the browser does, so new URL('https://bücher.example/').hostname is 'xn--bcher-kva.example'.

The problem is that several scripts contain letters that look identical. Cyrillic has its own а, е, о, р and с, each a different character from the Latin letter it resembles. A domain spelled with them is a homograph: it looks exactly like the real name and is a different domain. In 2017 a researcher registered xn--80ak6aa92e.com, which is apple.com spelled entirely in Cyrillic, and showed that Chrome and Firefox displayed it as apple.com with a valid certificate. Chrome fixed it in version 58 that April, and browsers now have rules for when to show the Unicode form and when to show xn-- instead. Chrome shows punycode when a label mixes Latin with Cyrillic or Greek, or is written entirely in letters that could pass for Latin ones, as in that example. The rules differ between browsers, apps and email clients. A link in a message may show the Unicode form even when the address bar would not.

The practical rule: an xn-- in a host you expected to be plain English is a reason to stop. In the address bar, it is the browser telling you it does not trust how the name looks.

The text says one thing, the href another

The oldest trick needs no clever domain at all. A link's text and its href are independent, so the text can be a URL that is not the destination:

<!-- The reader sees one address; the browser goes to another --> <a href="https://login.example.net/verify">https://www.example.com/account</a>

Before following a link that matters, look at where it goes. On a computer, rest the pointer on it and read the address the browser shows at the bottom of the window. On a phone, press and hold the link: the menu that appears shows the address, and offers to copy it instead of opening it. The same works in most email clients.

The demo takes a URL apart the way the browser does, using the URL API, and points out the tricks in this section. Every preset uses reserved names such as .example and .test, which cannot be registered, so none of them leads anywhere.

The demo's registrable-domain line is a guess, because it does not load the Public Suffix List. That is the lesson: "is this the same site?" needs a list of thousands of entries, so leave it to the browser or a maintained library, not a regular expression.

Short links are everywhere: on posters, in text messages and posts, under QR codes, and inside links that platforms rewrite without asking. They combine almost every problem on this page in one address.

A short link is a URL whose server answers with a redirect and nothing else. The shortening service keeps a table from short codes to long addresses. When a request arrives for https://bit.ly/3abcXYZ, it looks up 3abcXYZ and answers with a 301 or 302 and a Location header, exactly the redirects described in What a Click Does. This site has its own demo addresses that work the same way:

HTTP/2 302 location: https://cse134.site/go/chain/2 cache-control: no-store

That redirect is not free. Before the browser can ask for the page the person wanted, it has to look up the shortener's host name in DNS, open a connection and negotiate TLS with it, send a request and wait for the answer. Only then does it start again with the real host: another DNS lookup, another connection, another request. On a phone on a slow network, each of those steps can take a few hundred milliseconds, and the person sees an empty tab the whole time. Before the First Byte follows the same steps for a single host.

Short links solve real problems:

  • Character limits. Text messages and some social platforms count characters, and a long address with a query string can use most of a post. Twitter's 140 characters made the first wave of shorteners popular.
  • Print and speech. A long address cannot be typed from a poster or read aloud on a podcast. cse134.site/go/syllabus can.
  • QR codes. A QR code is a URL a camera can read, and a shorter URL makes a simpler code that scans more reliably. Most printed codes also point at a short link because the print cannot change and the redirect behind it can.
  • Counting clicks. Every click passes through the shortener's server, so it can count them: when, from where, on which device, from which site. For many users that report is the product.

Every one of those benefits has a cost, and most of the costs fall on the person clicking.

  • The destination is hidden. Everything in section 1 depends on reading the address, and a short link has no address worth reading. bit.ly/3abcXYZ could lead to a university page or to a lookalike login form, and the reader cannot tell which without following it. That is why phishing campaigns use shorteners: they hide the registrable domain, the one part that would give the attack away.
  • The shortener sees every click. The service records the time, the IP address, the browser and the referring page of everyone who follows the link, and it is a third party to both the author and the reader. Its privacy policy, not yours, governs that record.
  • Redirect chains. Links pass through several services on the way to their destination. X wraps every link in a post in its own t.co address, and counts it as 23 characters whatever its length; LinkedIn uses lnkd.in; email newsletters rewrite every link through their click-tracking host; corporate mail scanners add another layer. A link shared through two or three of these can take four or five hops before it reaches the page, each a round trip, and each with its own record of the click.
  • The link dies with the service. A short link works only as long as its company keeps the lookup table and the server running. Shorteners close, get acquired, change their terms, or start charging for links that were free. When that happens, every short link it ever issued breaks at once, in every document, slide and printed poster that used one.
  • The destination can change after it is shared. The person who controls a short link can point it somewhere else at any time. That is the feature that makes printed QR codes practical, and it also means a link that was safe when it was checked, shared and approved can lead somewhere else tomorrow, if its account is taken over, or if its domain is sold.
  • Filters block them. Because shorteners hide destinations so well, spam filters and email security gateways treat them with suspicion, and some block shortener domains outright. A short link in a class announcement can send the whole message to spam.
A shared link's redirect chainFive boxes left to right: the browser, t.co, bit.ly, a campaign tracker at click.shop.example, and the page at shop.example. Arrows between them are labeled 301, 301, 302 and finally 200. Under each of the three middle hosts, a note says a new host means DNS, a connection and a round trip, and that the host logs the click. A bracket under the chain says the person waits for all four requests before the page starts to load.browserone tapt.coplatform wrapper301bit.lyshortener301click.shop…campaign tracker302shop.example200: the pageeach middle hop: a DNS lookup, a connection, a round tripand a log entry: time, IP address, browser, referring pagefour requests before the page can start to loadOnly the last box is the place the link was shared to show.
A link shared on a social platform, shortened and tagged for a campaign, is three redirects through three companies before the page.

The case of goo.gl

Google's own shortener shows how a link can die with its service, even when the service belongs to one of the largest companies on the web. Google launched goo.gl in 2009 and stopped accepting new links in 2019, while promising that existing links would keep redirecting. On 18 July 2024 it announced that they would not: after 25 August 2025, every goo.gl link would stop responding, and from 23 August 2024 some would first show a warning page. Those links had been published for fifteen years, in papers, books, documentation, slides and printed material.

After a year of objections, Google partly reversed the decision on 1 August 2025: only links that had shown no activity in late 2024 would be deactivated, and all others would keep working. Links made by Google's own apps, such as shared Maps locations, were never affected. The result is the worst kind of link rot, because it is invisible: a reader cannot tell from a goo.gl link in an old paper whether it is one of the survivors, and the decision about which links lived was made by someone else, on a measure of popularity the author never saw.

Expand a link before following it

A short link can be read before it is followed, by asking its server where it goes without going there. Several services offer a preview for this. Add a + to the end of a Bitly link, as in https://bit.ly/3abcXYZ+, and instead of redirecting, Bitly shows a page with the destination's address and title and the date the link was made. Not every shortener has a preview, and a preview is only as honest as the service.

The general tool is curl, which is installed on macOS, Windows and most Linux systems. With -I it sends a HEAD request, asking only for the headers, and it does not follow redirects unless told to, so it prints the first Location and stops. Add -L to follow every hop, printing each response along the way:

$ curl -I https://cse134.site/go/chain HTTP/2 302 location: https://cse134.site/go/chain/2 $ curl -IL https://cse134.site/go/chain HTTP/2 302 location: https://cse134.site/go/chain/2 HTTP/2 302 location: https://cse134.site/go/chain/3 HTTP/2 302 location: https://cse134.site/api/echo?via=chain HTTP/2 200 content-type: application/json; charset=utf-8

Neither command runs the destination page or shows it, and without -L curl never contacts the destination at all.

The demo does the same from a web page. The site's Worker follows a link's redirects one hop at a time and reports each one's status, Location and time. It is deliberately limited: it only follows this site's /go/ addresses and a handful of well-known shorteners, sends HEAD requests, never returns a page's content, and stops after five hops. When a hop points anywhere else, it reports the address and does not request it. A server that would fetch any address anyone typed would be an open proxy, a tool for probing other servers with this site's name on the requests. For any other link, use curl.

The fix for most of these problems, when you are the one publishing a link, is to make the short link yourself, on your own domain. A path like cse134.site/go/syllabus is as short as most shortener links. It shows the reader whose site it leads to. It tells no third party about the click. It lives exactly as long as your domain does, which is the same promise the rest of your site makes. And it can still be changed, by you, if the page behind it moves.

Most hosts make this a one-line job. Static hosts such as Cloudflare and Netlify read a _redirects file, one rule per line: the short path, the destination and the status. A server or Worker can answer the same way in a few lines of code, as this site's inspector shows with its /go/ addresses.

# short path destination status /go/syllabus /courses/cse134b/syllabus/ 301 /go/office-hours https://calendar.example.edu/cse134b 302 /go/survey https://forms.example.edu/cse134b-week3 302

Choose the status by how long the link is meant to last. A 301 says the move is permanent: browsers cache it and search engines transfer the old address's standing to the new one, so use it for links that should always lead to the same place, such as the syllabus. A 302 says "for now", so browsers do not cache it unless the response says to, and it is right for a link you expect to repoint, such as this week's survey. Keep the old short paths working when you change things; a short link printed on a handout is a promise too.

The same applies to QR codes. A QR code that points straight at a long address cannot be fixed when the page moves, and one that points at a third-party QR or shortener service stops working when that service does, or when its free tier ends, which is a common surprise with "dynamic QR code" generators. Point printed codes at a short path on your own domain, and redirect from there. The code stays valid as long as the domain does, and the destination stays in your hands.

3. Tracking parameters

Many links carry more than an address. Copy a link from a newsletter, an ad or a social app and it often ends in a query string like this:

https://shop.example/boots?utm_source=newsletter&utm_medium=email&utm_campaign=fall-sale&fbclid=IwAR2x…

The page is https://shop.example/boots. Everything after the ? is for the site's analytics. Parameters starting with utm_ (from Urchin, the analytics company Google bought and turned into Google Analytics) describe a campaign: where the link was placed, through which medium, as part of which campaign. They are the same for everyone who receives the link. Click identifiers are different: gclid (Google Ads), fbclid (Facebook), msclkid (Microsoft Advertising), ttclid (TikTok), mc_eid (Mailchimp, identifying the email recipient) and many others are unique values that tie this particular click to a particular ad impression or recipient, so that the advertiser can join a visit on one site to a person profiled on another.

A shared link with a click id says something about the person who shared it: that they came from an ad, or, with mc_eid, which email recipient they were. And everyone who follows it is counted as that original click, which corrupts the analytics too.

Browsers that strip them

Because click ids are used to track people across sites, browsers with tracking protection have started removing the best-known ones from addresses as a navigation happens:

  • Firefox added query parameter stripping in version 102 (2022). It removes a list of known click ids when Enhanced Tracking Protection is set to Strict, and since version 120 its context menu offers Copy Link Without Site Tracking in every mode.
  • Safari added Link Tracking Protection in Safari 17 (2023): known click ids are removed in Private Browsing, and from links opened from Mail and Messages.
  • Brave has stripped known click ids by default, on every navigation, since 2020.

All three target parameters that identify a person or a click, such as fbclid, gclid and mc_eid. None of them removes utm_ parameters by default, because those describe a campaign rather than a person, and removing them would break ordinary analytics without much privacy gain. Chrome strips none of them.

The reliable fix is to delete them yourself. Before pasting a link into a message or a page, look at the query string and remove anything that is not part of the address. Most pages work without any query string at all; a search results page or a product variant is the exception, so test the cleaned link once. A script can do the same with the URL API:

const TRACKING = /^(utm_\w+|gclid|fbclid|msclkid|ttclid|mc_eid|mc_cid|igshid)$/; function clean(address) { const url = new URL(address); for (const name of [...url.searchParams.keys()]) { if (TRACKING.test(name)) url.searchParams.delete(name); } return url.href; } clean('https://shop.example/boots?size=9&utm_source=newsletter&fbclid=IwAR2x'); // 'https://shop.example/boots?size=9'

For links you publish, the same thinking applies from the other side. Campaign tags on your own site's links are a reasonable way to learn which announcement worked, but they make the address longer and uglier, they leak into every copy that gets shared, and they say to every reader that they are being counted. Use them deliberately, mention them in your privacy notice, and never put anything identifying a person in a link's query string. What a Click Does showed what the Referer header sends; tracking parameters send whatever the author chooses, to whoever receives the link. The Privacy page puts both in the larger picture.

A link that worked when it was written stops working when its target moves or disappears. This is link rot, and its slower cousin, content drift, is a link that still works but leads to a page that no longer says what it said when it was cited. Both are measured, and the numbers are large.

  • In 2014, Jonathan Zittrain, Kendra Albert and Lawrence Lessig at Harvard checked the links in legal citations. More than 70% of the URLs in the Harvard Law Review and other law journals, and 50% of those in United States Supreme Court opinions, no longer led to the material originally cited.
  • In 2024, the Pew Research Center sampled pages from Common Crawl, a public archive of the web, and found that 38% of the pages that existed in 2013 were no longer available a decade later. About a quarter of all pages that existed at some point between 2013 and 2023 were gone. Of the news and government pages it checked, 23% and 21% contained at least one broken link, and 54% of the English Wikipedia pages it sampled had at least one broken link in their references.

The Cool URIs section of the URLs page covers the author's half of the fix: Tim Berners-Lee's argument that URLs do not change, people change them, and the habits that make an address last. When an address must change anyway, a 301 from the old address to the new one keeps every link anyone ever made to it working. This site does exactly that for every page it moved in its reorganization, as What a Click Does showed. A redirect costs one line of configuration and a round trip; a broken link costs every reader who follows it.

The Wayback Machine and archive links

For links to other people's pages, which you cannot keep alive, the answer is an archive. The Internet Archive's Wayback Machine has saved copies of the web since 1996, and anyone can ask it to save a page now, at web.archive.org/save. An archived copy has an address that includes the time it was saved, such as https://web.archive.org/web/20240101000000/https://example.com/, and a short form, https://web.archive.org/web/https://example.com/, that leads to the most recent copy.

When a link in something you write is the evidence for a claim, link to the live page and archive it at the same time, so a reader has the original if the live page changes or disappears. Wikipedia's bots replace dead references with archived copies, and the Harvard study's authors built Perma.cc for courts and journals to do the same. HTML has no attribute for an archived copy, so the convention is a second link beside the first:

<p> <a href="https://example.com/report-2024">The 2024 report</a> (<a href="https://web.archive.org/web/20240601000000/https://example.com/report-2024">archived copy, June 2024</a>) found that… </p>

For your own site, run a link checker on a schedule. It reports links that answer 404 or 410, and ones that now redirect to a home page, which is often a missing page in disguise.

5. Showing a page instead of linking to it

A link sends the reader to another document. An <iframe> brings the other document to the reader: it shows a complete, separate page inside a rectangle of the current one. Video players, maps, payment forms, code playgrounds and the <browser-window> demos on this site are all iframes. The framed page keeps its own URL, scripts and origin; the framing page decides only where it goes, how big it is, and what it may do.

<iframe src="https://maps.example/embed?place=ucsd" title="Map of the UC San Diego campus" width="600" height="400" loading="lazy" referrerpolicy="strict-origin-when-cross-origin"></iframe>
  • title names the frame for screen readers, which announce a frame and its name when the reader enters it. Without one, people hear "frame" and nothing more. It should say what is in the frame, not that it is a frame.
  • width and height, or a size in CSS, reserve the space before the page loads, as they do for images.
  • loading="lazy" waits to load the frame until it is near the viewport. A framed page is a whole page, often with megabytes of script, so this matters more for frames than for images. It is supported in every current browser.
  • referrerpolicy works as it does on links, and controls what the framed site learns about the page it is embedded in.

The sandbox attribute

A framed page from another origin cannot read the page around it: the same-origin policy keeps the two documents apart. It can still do a great deal on its own: run scripts, open pop-ups, submit forms, start downloads, show alert() dialogs, and, in some cases, navigate the whole tab to another address. The sandbox attribute takes all of that away. An empty sandbox applies every restriction at once, and each token added to it gives one capability back:

TokenGives back
(none)Nothing. No scripts, no forms, no pop-ups, no downloads, no top navigation, and the content gets a unique, opaque origin, so it has no cookies or storage.
allow-scriptsRunning JavaScript
allow-same-originThe content's real origin, with its cookies and storage
allow-formsSubmitting forms
allow-popups, allow-popups-to-escape-sandboxOpening new windows, and letting them run without the sandbox
allow-top-navigation-by-user-activationNavigating the whole tab, but only when the person clicked something
allow-top-navigationNavigating the whole tab at any time
allow-modals, allow-downloadsalert() and other dialogs; downloads

Give back only what the content needs. An embedded widget that only displays something needs allow-scripts at most; a payment form may need allow-forms and allow-popups too. The one combination to understand is allow-scripts allow-same-origin on content from your own origin. Scripts in the frame then run with the same origin as the page around them, which lets them reach into the parent document, find their own <iframe> element and remove its sandbox attribute. The sandbox is then decoration. On content from another origin, the pair is safe enough, since the same-origin policy still separates the two pages, which is why many embed codes ask for it. For untrusted HTML you show yourself, such as a preview of what a user wrote, use srcdoc with an empty sandbox, or sandbox="allow-scripts" without allow-same-origin:

<!-- Untrusted HTML, shown and nothing more --> <iframe sandbox srcdoc="&lt;p&gt;A comment someone posted&lt;/p&gt;" title="Comment preview"></iframe> <!-- Scripts run, in a unique origin with no cookies or storage --> <iframe sandbox="allow-scripts" src="/playground/run?id=42" title="Your code, running"></iframe> <!-- Do not: same-origin content can remove its own sandbox --> <iframe sandbox="allow-scripts allow-same-origin" src="/uploads/user-page.html" title="User page"></iframe>

The allow attribute

Powerful browser features, the camera, the microphone, location, full screen, payments and the clipboard among them, are governed by Permissions Policy. A frame from another origin does not get most of them by default. The allow attribute grants them, feature by feature, and the person is still asked for permission as usual:

<iframe src="https://meet.example/room/cse134b" title="Video call: CSE 134B office hours" allow="camera; microphone; fullscreen; display-capture"></iframe>

sandbox and allow are independent. One removes general abilities, the other grants specific features, and a frame can have both.

The framed page's side

The framing page controls its frames. The framed page controls whether it can be framed at all, and it should care. In a clickjacking attack, a page frames another site, makes the frame invisible, and lays it over something the person wants to click, such as a "play" button. The person's click lands in the invisible frame, on the real site, where they are signed in: a "delete account", "confirm payment" or "allow camera" button the attacker has positioned under the cursor.

Who controls what in an iframeOn the left, the embedding page at your-site.example contains an iframe element. Labels on the iframe list what the embedder controls: src, title, size, loading, referrerpolicy, sandbox and allow. An arrow leads right to the framed page at other.example, whose labels list what it controls: the Content-Security-Policy frame-ancestors directive and the older X-Frame-Options header, which decide whether it may be framed at all. A note across the bottom says the same-origin policy keeps the two documents apart.your-site.example<iframe>src, title, width and heightloading, referrerpolicysandbox: what it may doallow: which features it getsloadsother.exampledecides whether it may be framed:frame-ancestors 'self'(Content-Security-Policy)X-Frame-Options: DENY(the older header)Between them, the same-origin policy: neither page can read the other.
The embedder sets the frame's limits. The framed page decides whether anyone may frame it.

The defense is a response header on every page that should not be framed by other sites, which is most pages that do anything once a person is signed in. The current way is the frame-ancestors directive of Content Security Policy, which lists who may frame the page. The older X-Frame-Options header does the same with two useful values, DENY and SAMEORIGIN; browsers that support frame-ancestors ignore it when both are present, and sending both covers old browsers too:

Content-Security-Policy: frame-ancestors 'self' X-Frame-Options: SAMEORIGIN

These must be headers: frame-ancestors is ignored in a <meta> element, because by the time the HTML is read, the page is already framed. Security covers the rest of the headers a site should send.

6. Where JavaScript takes links next

Everything in this section has assumed that a link is followed by the browser. In a single-page application, it often is not. A client-side router listens for clicks on links, cancels the navigation with preventDefault(), fetches the data for the new view, updates the page in place and changes the address bar with the History API. Done badly, it throws away everything a real link provides.

A router has to keep, by hand, what the browser did for free:

  • Real href values. Every link is still an <a href> with the real address of the view, so it can be opened in a new tab, copied, bookmarked, shared, read by a search engine and followed when the script fails to load. The router enhances links; it does not replace them with <div>s and click handlers.
  • Real URLs. Every view has its own address, the server answers that address with a working page when it is requested directly, and reloading shows the same view.
  • The back button. Each navigation adds a history entry, and going back restores the previous view.
  • Modifier keys. A click with Ctrl, Cmd, Shift or the middle button is left alone, so the browser can open the new tab the person asked for.
  • Scroll position. A new view starts at the top; going back returns to where the person was.
  • Focus and announcement. After a real navigation, a screen reader announces the new page's title and starts reading from the top. After a script swaps the content, nothing happens unless the router moves focus to the new content, usually its heading, and updates document.title. Without that, a screen reader user activates a link and hears nothing at all.

The Navigation API was designed to make this easier. Instead of listening for clicks on links, a page listens for every navigation, whatever started it, and can choose to handle it with a script. The browser keeps the parts that routers used to get wrong: the address bar, history entries, the back button, the loading indicator, and scroll restoration. It reached every major browser in January 2026 (Chrome 102, Firefox 147, Safari 26.2), so older browsers still need a fallback, which real links already are:

// Browsers without the API keep ordinary links, which already work. if ('navigation' in window) navigation.addEventListener('navigate', (event) => { const url = new URL(event.destination.url); // Leave the browser in charge of anything this page should not handle. if (!event.canIntercept || event.hashChange || event.downloadRequest !== null) return; if (url.origin !== location.origin) return; event.intercept({ async handler() { const view = await render(url.pathname); // fetch and build the new view document.querySelector('main').replaceChildren(view); document.title = view.dataset.title; const heading = document.querySelector('main h1'); heading.tabIndex = -1; // a heading cannot take focus without it heading.focus(); }, }); });

Often, a router is not needed at all. Much of the reason for building one was the look of a navigation: the white flash between pages, and the lack of an animated transition. Cross-document view transitions give ordinary links an animated transition between two separate pages, with two lines of CSS on each, and the links stay ordinary links:

@view-transition { navigation: auto; }

Chromium browsers and Safari 18.2 and later support it; Firefox, at the time of writing, ignores the rule and navigates as it always has. That is the pattern this course has followed throughout: start with HTML that works, a real link to a real address, and let CSS and JavaScript make it better without taking anything away.

HTML is the start. A link is the oldest interaction on the web and still the most reliable one: it works without script, it works for every input method and every assistive technology, it can be bookmarked, shared, archived and cited, and it costs one element and one attribute. The Forms section that follows is about the other half of the conversation, when the reader stops following and starts sending something back.

7. Summary

ConceptKey points
Reading a URLThe registrable domain decides who runs the server; read the host from the right. Subdomains are the owner's choice. Lookalikes and typosquats swap characters. The padlock says the connection is encrypted, not who owns the name.
Userinfo and homographsIn https://bank.com@evil.example/, the host is evil.example. Non-ASCII hosts become punycode (xn--); browsers show it when a name looks like a lookalike. Hover or long-press to see a link's real address.
Short linksA server that answers with a 301 or 302. Useful for limits, print, speech, QR codes and counting. Costs a round trip per hop, hides the destination, tells a third party about every click, can be repointed, gets filtered, and dies with the service (goo.gl, 2024–2025).
Expanding a linkA preview such as Bitly's +; curl -I for one hop, curl -IL for all of them; the inspector demo for /go/ addresses and known shorteners.
Publishing short linksYour own domain and short paths, from a _redirects file or server code. 301 for links meant to last, 302 for links you will repoint. Printed QR codes point at your own domain.
Tracking parametersutm_* tags a campaign; click ids (gclid, fbclid, msclkid, mc_eid) tag a person or a click. Firefox, Safari and Brave strip known click ids in some modes; none strips utm_* by default. Remove them before sharing.
Link rotMeasured at 50% of Supreme Court opinion links (2014) and 38% of 2013 web pages gone by 2023 (Pew, 2024). Fix your own with 301s; cite others with an archived copy beside the live link; check outgoing links on a schedule.
iframeShows another document in place. Always a title; loading="lazy"; referrerpolicy. sandbox removes abilities, its tokens give them back; never allow-scripts allow-same-origin on same-origin untrusted content. allow delegates features such as the camera.
Being framedClickjacking hides a framed page under a decoy. Refuse framing with Content-Security-Policy: frame-ancestors (headers only), plus X-Frame-Options for old browsers.
Links and JavaScriptA router keeps real hrefs and URLs, the back button, modifier keys, scroll, focus and an announcement. The Navigation API leaves more to the browser; view transitions animate ordinary links. HTML is the start.