A directory built from researching Google listings inherits Google's names, including the keyword-stuffed ones. On 4 August PawScapes cleaned them up: 99 renames, 94 slug changes, and 9 junk or duplicate listings archived.
A slug change is a URL change, and other pages linked to those URLs. So the cleanup had an aftermath: a 94-pair rewrite of internal links across stored content, run in D1, followed by a recursive SQL sweep to prove nothing still pointed at an old URL. The sweep found zero broken links, and the whole exercise would have to be repeated after every future rename.
Store the target, not the address
highseam 1.7.0's link field holds either an external URL or a reference to a document by id:
export interface LinkValue {
/** External destination (http(s), mailto:, tel:, or site-relative /path). */
url?: string;
/** Internal destination, stored by id. */
doc?: { collection: string; id: string; /** filled by populate at depth ≥ 1 */ resolved?: Doc };
newTab?: boolean;
}An internal link never stores a path, so renaming the target changes nothing in the link. The URL is worked out when the page renders. One field, one picker: choose a document, or paste a URL.
PawScapes adopted it on 14 August, the day it shipped, starting with the links editors set by hand: hero calls to action, CTA strip buttons and quote attributions. The card, timeline and callout blocks added in September use it too.
{ name: "cta_href", type: "link", relationTo: ["venues", "pages", "plans", "guides", "news"] },Old values were plain strings. The field lifts a string to { url } the next time the document saves, so nothing had to be migrated up front.
The site decides what a URL looks like
highseam knows which document a link targets; only the site knows where that document lives. linkHref() takes a map from collection to URL builder, and PawScapes keeps it in one file:
const ROUTES: Record<string, (d: Record<string, unknown>) => string> = {
venues: (d) => venuePath(String(d.type) as VenueType, String(d.slug)),
diary: (d) => `/diary/${String(d.slug)}`,
guides: (d) => `/guides/${String(d.slug)}`,
plans: (d) => `/plans/${String(d.slug)}`,
news: (d) => `/news/${String(d.slug)}`,
pages: (d) => `/${String(d.slug)}`,
};A venue's URL depends on its type (/parks/…, /pubs/…), so the builder reads the type too. When venues were re-typed in the cleanup (a "beach" that was really holiday accommodation became a stay), links to them would follow with no edit.
When a link can't be resolved, because the target was archived or deleted, linkHref() returns null and the site renders the label without an anchor. Never a dead #. Links to archived venues go quiet by design.
What it doesn't cover
The link field covers links people set in the admin. The guides the pipeline writes are HTML, and the links inside them are still plain hrefs. After 13 August, when PawScapes started redirecting moved URLs post-launch, the order of operations became fixed: correct the source, rewrite internal links, and only then add the 301, so the site never links to a redirect. For pipeline HTML, that rewrite is still SQL.
Link fields also make "what links here?" answerable: highseam's usage scan (findUsage) counts link references, so finding everything that points at a page before archiving it is one call. The admin already shows those backlinks on media documents.
