Website engineering¶
← Maintainer handbook · Architecture · Testing · User-facing directory guide
The Next.js site is a read-only presentation layer. site/src/lib/ owns server queries and pure helpers; client components own browser interaction. Keep strict TypeScript, semantic controls, Tailwind utilities, minimal global CSS, and safe external URLs. The publication map separates server reads, generated downloads, and browser-local state.
Read-only database contract¶
getDirectoryData opens a short-lived node:sqlite connection with readOnly: true and uses an explicit read transaction to select open rows and the latest successful collection timestamp from one SQLite snapshot. SQL orders the rows by descending first-seen time and ID text; presentation and API helpers apply microsecond-aware first-seen sorting with numeric-ID ties. A later failed run cannot imply fresher data.
React request caching shares the query between metadata and the page. Null-prototype SQLite rows become plain objects before crossing the Server Component boundary. Every listing URL must be canonical HTTPS LinkedIn and match its numeric ID.
In versioned mode, OPPORTUNITIES_RELEASE_ROOT/current resolves once per server operation to a release under releases/. Invalid/missing pointers fail closed rather than using legacy paths. The selected release must remain available until readers drain. Separate requests can span a cutover; a page followed by a download is not a pinned transaction.
The website never collects, classifies, migrates, generates exports, or writes SQLite.
The listing API reuses the same query; fixed download routes serve three Python-generated files.
RSS (/feed.xml) and Atom (/atom.xml) routes also reuse the read-only open-row query, apply the shared microsecond-aware first-seen sort with numeric-ID ties before limiting to 50 matches, and accept exact type, country, and category filters. Their query parser rejects unknown or repeated keys, output is XML-escaped, and GET/HEAD responses support conditional caching; there is no subscription state or notification service. See the user guide for URLs and filter examples. Configuration owns runtime variables; Automation owns release publication.
Production status¶
opportunity-status-data.ts pins one publication, reads both SQLite aggregates in one statement, and closes the read-only connection before reading exports. Status and fixed-download readers use nonblocking opens to reject nonregular files without waiting for pipe writers. Status additionally bounds regular-file sizes and streamed bytes, strictly decodes metadata, and hashes the actual JSON download. Four concurrent reads per server process are permitted; excess requests receive a sanitized 503 without queueing or cached results.
opportunity-status.ts checks count/hash agreement and timestamps before projecting the public allowlist. Only successful searches advance collection time, including successful searches in a partial batch. Release identifiers contain digits and a hyphen; invalid versioned pointers never fall back to legacy state. Preserve runtime-file tracing directives to keep operator data out of builds.
The public status contract owns fields, byte limits, HTTP methods, cache policy, and error responses. Status does not verify every SQLite field or CSV contents, contact GitHub or LinkedIn, or replace publication validation. Testing owns fixture isolation and standalone-server checks; Automation owns comparison with reviewed state.
Local state contract¶
local-opportunity-state.ts owns pure parsing, pruning, toggling, and visit-baseline decisions. use-local-opportunities.ts owns browser storage and React synchronization. opportunities-directory-state version 1 contains lastVisitAt, optional nullable previousVisitAt, and saved, hidden, applied arrays of numeric job-ID strings. No listing metadata, notes, résumés, or application details are stored.
- Hydration loads storage only in the browser, in a layout effect before the next paint; SSR cannot depend on visitor state. The header reserves space for the saved/applied/hidden summary and optional new-opportunity message at every viewport width, without showing guessed counts while storage loads.
- Parsing deduplicates and prunes IDs not present in current open data. Unsupported/corrupt records reset safely.
- A visit resumes across reloads/tabs until 30 minutes without a load or local action. Older v1 records without
previousVisitAtupgrade locally. First visits have no previous baseline. - New means immutable first-seen time strictly after that baseline. Hidden rows never appear in All, Saved, Applied, or New.
- Local view choice is not serialized into a public URL. Public filters and sorting still apply inside each local list.
- Open tabs synchronize local changes and clearing through storage events. Each action rereads stored IDs before toggling, preserving sequential edits made elsewhere. Simultaneous writes are still last-writer-wins; local storage is not a transactional collaboration service.
- Storage failures keep consecutive actions usable in memory. Clearing site data removes the lists; there is no backend copy.
Browser lists are explicitly supported; server-stored applications, authentication, uploaded content, or canonical write APIs are not. See the canonical privacy notice, and preserve these boundaries in network-level browser tests.
URL and presentation state¶
| Parameter | Directory meaning |
|---|---|
q, company, category, type |
Search and exact filter selections |
country |
Canonical country label, supported code, or alias |
first-seen |
24-hours, 7-days, 30-days, relative to directory request time |
sort |
Company, role, location, first-seen; -asc / -desc |
page-size |
10, 20, 30, 50, 100 |
page |
Positive one-based page, constrained to displayed results |
opportunity-country.ts resolves country names, codes, and supported city-only or remote values through the collector's shared geography vocabulary. A filter selects one country; multi-location roles can match each supported country independently. This normalizes presentation without reclassifying listings. Turbopack resolves development imports from the repository root; production retains website-rooted standalone output.
Defaults are omitted. Search typing replaces history; other controls push history. Filter/sort/page-size changes reset to page one. Reset clears public filters and page, preserving sort, page size, and unrelated parameters. Local-list changes constrain the displayed page without rewriting the public URL.
The header's open roles badge always counts the complete open dataset, independent of public filters, local lists, or hidden IDs. The result count beside pagination describes the current filtered/local view instead.
Pagination links have real hrefs. Without JavaScript, crawlers can follow the unfiltered default view through its pages. Unknown browser parameters are ignored/constrained; the API instead rejects invalid input. Shared filtering helpers must not erase this intentional distinction or the API's snapshot-relative recency contract.
Accessibility¶
Maintain labeled search/filter controls, semantic table headers, aria-sort, pressed states, meaningful external link text, status announcements, and visible focus. Keyboard activation must preserve focus after local actions; when a row disappears, move focus to the relevant list control. Escape dismisses More actions and restores its trigger; outside focus/pointer or scrolling dismisses the floating menu. Mobile users must be able to reach actions through table scrolling without document-level overflow.
Automated axe scans cover normal, filtered, empty, dark, and open-menu states. They do not replace keyboard or assistive-technology review. Testing owns commands and thresholds, not the user guide.
Search and social metadata¶
The unfiltered default / and valid /?page=N views self-canonicalize and are indexable. Search/filter/alternate sort or page-size/out-of-range views use noindex, follow and / as canonical. The site emits title/description, Open Graph, a generated 1200×630 image, large-card metadata, robots, sitemap, manifest, and escaped JSON-LD.
JSON-LD describes the WebSite and Dataset, Europe, cycle 2027, MIT license, maintainer, three-day collection schedule, latest successful collection and both download distributions. Collection frequency is not a production-publication guarantee; reviewed deployment remains separate. The sitemap omits a change-frequency hint because publication has no fixed cadence. It does not claim complete individual JobPosting markup. Serialization must escape < so hostile listing-like text cannot end the script element.
SITE_URL is a validated origin; production requires exact https://techopportunities.eu. Robots and sitemap resolve it at request time, even if built with localhost. The former hostname redirects with permanent 308 while preserving path/query; Deployment owns routing.
Browser integrations and security headers¶
next.config.ts supplies production CSP, HSTS, content-type, referrer, framing, cross-origin, and permissions headers. src/proxy.ts replaces caller-supplied nonce/CSP headers with a fresh nonce for framework, theme, analytics, and JSON-LD scripts. The script policy uses strict-dynamic and blocks untrusted inline scripts, event handlers, and eval. Inline styles support the theme and floating action menu. Dynamically rendered HTML must not be cached across different nonces. Production browser tests exercise script blocking and hydration; Docker smoke tests verify the response policy.
Umami loads only in production with canonical SITE_URL: script origin https://cloud.umami.is, event origin https://gateway.umami.is. Local tests/demo use loopback and block external browser requests. Local-list IDs and visit timestamps must not enter analytics payloads. Theme storage uses the separate opportunities-theme key.
Before adding analytics, advertising, error tracking, authentication, forms, or another browser integration, document visitor data flow, retention, consent/disclosure, and client-visible variables; review Security and update Privacy. Never put secrets in NEXT_PUBLIC_* values.