PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/utils/recordDeepLink.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:13 UTC 3,202 bytes, 74 lines download raw bytes

1// Pure helpers for the ?record=ID deep link, split out of the window-bound
2// navigation code so they can be unit-tested without a browser (#422).
3//
4// router.js imports viewState.js with a ?v= suffix that Node's test loader can't
5// resolve, so the suite can't import router.js directly. Keeping these two pure
6// functions here — with no ?v= imports of their own — lets the behavioural tests
7// import them by bare relative path while the browser code imports them with the
8// usual ?v= cache-busting suffix. Browser call sites delegate here, so there is
9// one definition of the read, write, canonical-link, and share-target rules.
10
11// Read the record id from a URL query string ('?record=ID', 'record=ID', or a
12// full search with other params). Returns the id, or null when the param is
13// absent or present-but-empty — an empty id must not open a modal or pin a
14// phantom selection. Tolerates undefined/null search (URLSearchParams coerces
15// the empty string).
16export function parseRecordId(search) {
17  const params = new URLSearchParams(search || '');
18  return params.get('record') || null;
19}
20
21// Set or clear ?record= on a URLSearchParams: a truthy id sets it, any falsy id
22// (null on deselect, undefined, '') deletes it. Returns the same params object so
23// callers can chain. This is the single source of the write-effect branch that
24// the mount-time deep-link race used to get wrong.
25export function setRecordParam(searchParams, recordId) {
26  if (recordId) {
27    searchParams.set('record', recordId);
28  } else {
29    searchParams.delete('record');
30  }
31  return searchParams;
32}
33
34// Build the one shareable record URL used by every archive surface. Desktop
35// windows intentionally resolve to the standard archive record URL so the
36// optional shell never creates a second incompatible citation/share format.
37export function canonicalRecordUrl(locationHref, recordId) {
38  if (typeof locationHref !== 'string' || locationHref === '') {
39    throw new TypeError('canonicalRecordUrl requires a location URL');
40  }
41  if (typeof recordId !== 'string' || recordId === '') {
42    throw new TypeError('canonicalRecordUrl requires a record id');
43  }
44
45  const url = new URL(locationHref);
46  url.search = '';
47  url.hash = '';
48  setRecordParam(url.searchParams, recordId);
49  return url.toString();
50}
51
52// Article-like records use the generated archive metadata route. Social posts
53// use their original public URL so the platform's own record-specific preview
54// is available without generating tens of thousands of extra FTP files.
55export function shareRecordUrl(locationHref, record) {
56  if (!record || typeof record.id !== 'string' || record.id === '') {
57    throw new TypeError('shareRecordUrl requires a record with an id');
58  }
59
60  if (record.type === 'social' && typeof record.url === 'string') {
61    try {
62      const sourceUrl = new URL(record.url);
63      if (sourceUrl.protocol === 'http:' || sourceUrl.protocol === 'https:') {
64        return sourceUrl.toString();
65      }
66    } catch {
67      // Fall through to the safe archive URL.
68    }
69  }
70
71  return canonicalRecordUrl(locationHref, record.id);
72}
73
74export default { canonicalRecordUrl, parseRecordId, setRecordParam, shareRecordUrl };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.