feat: Labor Hub /jobs/ — All Jobs wall + per-job detail pages#4751
feat: Labor Hub /jobs/ — All Jobs wall + per-job detail pages#4751aseckin wants to merge 3 commits into
Conversation
Adds two new consumer-facing routes under the Labor Hub: - /labor-hub/jobs/ — Forecast Wall of 15 occupations with year toggle and hover-only news ticker - /labor-hub/jobs/[slug]/ — statically generated job detail page with breadcrumb, year-stat tiles, forecast chart, Jump To carousel, Felten/MNA/AOE exposure metrics, Curated Insights (tiered: data.ts override → top comments on the job's post → keyword-matched sibling comments), wages + economy-wide hours bento, share card, and Hub CTA Share card PNG and OG meta image are generated through the existing screenshot-service pattern at /og/labor-hub/jobs/[slug]/. No new packages, no changes to the existing Labor Hub dashboard. Per-page JSON-LD (Dataset for job pages, ItemList for All Jobs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 of the consumer-friendly job pages, closing prototype-parity gaps and the curated-insights UX feedback: - JetBrains Mono wired through next/font and Tailwind; salmon-900 added to the palette so tile text matches the Claude Design output pixel-for-pixel - Forecast Wall tiles: prototype-exact font sizes / padding / hover overlay; ticker now slides by default with horizontal mask gradients and expands to a 3-line fade-bottom excerpt on hover; tile padding reserves space so the title is never overlapped by the ticker - Hub CTA card simplified to match the surrounding page sections (white bg, no colored border) - Jump To strip: arrows snap-to-edge on click (gradient-carousel clamps to 0 / maxScroll when within half a step) and use bright light-bg buttons in dark mode; first snap point is now exactly scrollLeft=0 after dropping the list's px-2 padding so the left arrow hides correctly - Exposure metrics get a question-circle icon next to each HIGH/MED/ LOW chip with a hover tooltip - Wages / Hours mini-cards centered at md+, bigger numeric values - Bento becomes a 3-step MobileCarousel (Embla) on mobile, grid on desktop - Curated Insights: tiered fallback honours per-job excluded_comment_ids; markdown is stripped down to plain prose (tables, images, HTML entities, escapes); empty-after-strip comments are filtered out; comment-sourced items wrap in an anchor to /questions/<post_id>/#comment-<id> that opens in a new tab, with a visible underline + darker username on hover - All anchors in the new pages explicitly opt out of the global globals.css underline rule Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| // Drop blockquote and bullet prefixes at start of line | ||
| s = s.replace(/^\s*[>*\-+]\s*/gm, ""); | ||
| // Strip HTML tags (e.g. <p>, <strong>) | ||
| s = s.replace(/<\/?[^>]+>/g, ""); |
| s = s.replace(/^[\s\-=*]{3,}$/gm, ""); | ||
| s = s.replace(/[*_`#]+/g, ""); | ||
| s = s.replace(/^\s*[>*\-+]\s*/gm, ""); | ||
| s = s.replace(/<\/?[^>]+>/g, ""); |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (4)
front_end/messages/zh.json (1)
2242-2242: ⚡ Quick winConsider replacing mathematical Unicode character with standard ASCII.
The mathematical bold capital X "𝕏" (U+1D54F) may not render correctly on all systems and fonts. For better i18n compatibility and accessibility, consider using:
- Standard ASCII "X"
- Regular emoji if branding is important
- Or just remove the symbol since the text already says "在 X 分享" (Share on X)
♻️ Proposed fix for better compatibility
- "laborHubJobsShareTweet": "𝕏 在 X 分享" + "laborHubJobsShareTweet": "X 在 X 分享"Or simply:
- "laborHubJobsShareTweet": "𝕏 在 X 分享" + "laborHubJobsShareTweet": "在 X 分享"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/messages/zh.json` at line 2242, The string value for the message key "laborHubJobsShareTweet" uses the mathematical bold capital X character (𝕏) which can render inconsistently; update the value to use a standard ASCII "X" (or an appropriate emoji or remove the symbol) so it reads e.g. "X 在 X 分享" or "在 X 分享" and ensure you modify the "laborHubJobsShareTweet" entry accordingly in the JSON.front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts (1)
39-41: 💤 Low valueType assertion assumes post structure.
The type assertion
as QuestionWithNumericForecasts[] | undefinedassumes thegroup_of_questions.questionsstructure matches the expected type. If the runtime structure differs, this could cause errors ingetValueForLabel.Consider adding runtime validation or using a type guard to ensure type safety. However, if the post structure is well-known and validated elsewhere, the current approach is pragmatic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts around lines 39 - 41, The code currently asserts post.group_of_questions?.questions as QuestionWithNumericForecasts[] | undefined which can hide runtime shape mismatches; add a runtime type guard (e.g., isQuestionWithNumericForecastsArray) and use it to validate post.group_of_questions?.questions before treating it as QuestionWithNumericForecasts[] (or fall back to undefined/empty array), then only call getValueForLabel with validated questions; reference the variables/values questions, post.group_of_questions?.questions and the helper getValueForLabel when locating where to add the check.front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.ts (1)
42-70: 💤 Low valueConsider more robust HTML tag stripping.
The regex
/<\/?[^>]+>/gon Line 59 won't match malformed tags like<script(without closing>). While React's default escaping mitigates XSS risk, using a proper HTML parser or a more defensive approach would be more robust for security-sensitive contexts.For the current use case (extracting plain text for display in React components), the risk is low.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts around lines 42 - 70, stripBody's HTML stripping uses the fragile regex /<\/?[^>]+>/g which can miss malformed tags like "<script " — replace that step with a proper HTML-to-text approach: in the browser use DOMParser or create a temporary element (e.g., document.createElement and element.innerHTML = s; textContent) to reliably remove tags and handle malformed markup, and provide a safe fallback for non-browser environments (e.g., a more defensive regex that strips any "<" to next ">" or trims trailing "<" fragments). Update the stripBody function to call this parser-based sanitizer instead of the current regex so HTML tags (including malformed ones) are robustly removed before decoding entities and extracting the first paragraph.front_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts (1)
11-42: ⚡ Quick winDuplicated sanitization logic across files.
The
decodeEntitiesandstripfunctions are duplicated fromfetch_job_insights.ts. Consider extracting these into a shared utility module to maintain consistency and reduce duplication.The same security considerations apply here: double-decoding and incomplete HTML sanitization are low-risk given React's default escaping, but could be improved for defense in depth.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts around lines 11 - 42, Extract the duplicated decodeEntities and strip functions into a single shared utility module (e.g., sanitize or textUtils), export them, then replace the local implementations in this file by importing those exported functions; specifically remove the local decodeEntities and strip definitions and import decodeEntities and strip where they are used to ensure one canonical implementation (matching the existing implementation used in the other helper), run tests / lint and verify behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@front_end/messages/cs.json`:
- Line 2208: Update the value for the JSON key "laborHubJobsHeroLead" in
front_end/messages/cs.json by correcting the Czech typo: replace "Předpovědači"
with "Předpovídači" so the hero lead reads correctly; ensure you preserve the
rest of the string and surrounding punctuation/escaping.
In `@front_end/src/app/`(main)/labor-hub/jobs/components/exposure_metrics.tsx:
- Around line 117-131: Tooltip trigger is currently a non-focusable <span>,
making it inaccessible to keyboard users; replace it with a keyboard-focusable
<button type="button">. Locate the Tooltip usage (Tooltip component wrapping the
trigger) and replace the span element that has aria-label and className with a
<button type="button"> keeping the same aria-label, className, and child
FontAwesomeIcon; ensure no form submission by including type="button" and
preserve visual styling and tooltip props (tooltipContent, showDelayMs,
placement) so keyboard users can focus and activate the tooltip.
In `@front_end/src/app/`(main)/labor-hub/jobs/components/job_nav_strip.tsx:
- Around line 55-64: In job_nav_strip.tsx, the active Link (the job pill that
uses isActive and tone(item.value2035)) isn't exposed to assistive tech; add an
aria-current attribute to the Link element so screen readers announce the
current item (e.g., set aria-current="page" when isActive, otherwise omit or set
undefined) to the Link that points to `/labor-hub/jobs/${item.slug}/`.
In `@front_end/src/app/`(main)/labor-hub/jobs/components/jobs_wall.tsx:
- Around line 141-152: This control is a segmented toggle but currently uses
partial tab semantics; change the container from role="tablist" to role="group"
(or remove role) and on each button remove role="tab" and aria-selected and
instead add aria-pressed={y === year} so the buttons use toggle semantics; keep
the aria-label={t("laborHubJobsYearToggleLabel")} on the container and keep
onClick={() => setYear(y)} and key={y} as-is (references: WALL_YEARS, year,
setYear).
In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts:
- Around line 28-40: The decodeEntities function currently decodes numeric
entities then named entities which can double-decode sequences like "&lt;"
into "<"; update decodeEntities so named entities (&, <, >, ",
&`#39`;, ', ) are replaced first and numeric/hex replacements (&`#x`...;
and &#...;) are applied afterwards, or alternatively add a clear docstring on
decodeEntities stating it must only be used for plain-text contexts and must not
be used with dangerouslySetInnerHTML; modify the implementation in
decodeEntities accordingly to prevent unintended double-decoding.
In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts:
- Around line 17-18: Normalize the incoming year parameter by validating it
against the canonical set of allowed wall years before any use in URL
construction or download filename generation: introduce a constant list (e.g.,
knownWallYears) and replace direct use of req.nextUrl.searchParams.get("year")
(the local variable year) with a normalized value that is coerced to a string
from the allowed list (falling back to "2035" or the closest valid year) and use
that normalizedYear for all subsequent URL and filename building (including the
download flag handling and wherever the year is interpolated later in this
module).
- Around line 37-44: The POST to screenshotEndpoint using fetch (the const r =
await fetch(...) call) has no timeout; wrap the request with an AbortController,
pass controller.signal into fetch, and start a setTimeout that calls
controller.abort() after a configured timeout (e.g., SCREENSHOT_SERVICE_TIMEOUT
or a sensible default). Clear the timeout on successful response, and handle the
abort case by catching the thrown error and returning/throwing a clear
timeout/error response from this route handler so upstream slowness cannot hang
the request.
- Around line 23-26: Guard the construction of screenshotEndpoint by validating
process.env.SCREENSHOT_SERVICE_API_URL before calling new URL; specifically,
check that SCREENSHOT_SERVICE_API_URL is defined and is a valid URL (e.g.,
attempt to construct a URL inside the existing try/catch or validate with a
small helper) and only then build screenshotEndpoint, otherwise throw or return
a structured JSON error so the existing error handling catches malformed/missing
env values; update the code that creates screenshotEndpoint (the new URL(...)
call) to live inside that validation block and reference
SCREENSHOT_SERVICE_API_URL and screenshotEndpoint accordingly.
---
Nitpick comments:
In `@front_end/messages/zh.json`:
- Line 2242: The string value for the message key "laborHubJobsShareTweet" uses
the mathematical bold capital X character (𝕏) which can render inconsistently;
update the value to use a standard ASCII "X" (or an appropriate emoji or remove
the symbol) so it reads e.g. "X 在 X 分享" or "在 X 分享" and ensure you modify the
"laborHubJobsShareTweet" entry accordingly in the JSON.
In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_job_insights.ts:
- Around line 42-70: stripBody's HTML stripping uses the fragile regex
/<\/?[^>]+>/g which can miss malformed tags like "<script " — replace that step
with a proper HTML-to-text approach: in the browser use DOMParser or create a
temporary element (e.g., document.createElement and element.innerHTML = s;
textContent) to reliably remove tags and handle malformed markup, and provide a
safe fallback for non-browser environments (e.g., a more defensive regex that
strips any "<" to next ">" or trims trailing "<" fragments). Update the
stripBody function to call this parser-based sanitizer instead of the current
regex so HTML tags (including malformed ones) are robustly removed before
decoding entities and extracting the first paragraph.
In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_tile_tickers.ts:
- Around line 11-42: Extract the duplicated decodeEntities and strip functions
into a single shared utility module (e.g., sanitize or textUtils), export them,
then replace the local implementations in this file by importing those exported
functions; specifically remove the local decodeEntities and strip definitions
and import decodeEntities and strip where they are used to ensure one canonical
implementation (matching the existing implementation used in the other helper),
run tests / lint and verify behavior is unchanged.
In `@front_end/src/app/`(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.ts:
- Around line 39-41: The code currently asserts
post.group_of_questions?.questions as QuestionWithNumericForecasts[] | undefined
which can hide runtime shape mismatches; add a runtime type guard (e.g.,
isQuestionWithNumericForecastsArray) and use it to validate
post.group_of_questions?.questions before treating it as
QuestionWithNumericForecasts[] (or fall back to undefined/empty array), then
only call getValueForLabel with validated questions; reference the
variables/values questions, post.group_of_questions?.questions and the helper
getValueForLabel when locating where to add the check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d8ca2956-cc8c-40d2-afd1-7fc7b588a830
📒 Files selected for processing (32)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/src/app/(main)/labor-hub/data.tsfront_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsxfront_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsxfront_end/src/app/(main)/labor-hub/jobs/components/curated_insights.tsxfront_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsxfront_end/src/app/(main)/labor-hub/jobs/components/hub_cta_card.tsxfront_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsxfront_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsxfront_end/src/app/(main)/labor-hub/jobs/components/share_card.tsxfront_end/src/app/(main)/labor-hub/jobs/components/share_card_preview.tsxfront_end/src/app/(main)/labor-hub/jobs/components/wage_hours_cards.tsxfront_end/src/app/(main)/labor-hub/jobs/components/year_stats.tsxfront_end/src/app/(main)/labor-hub/jobs/helpers/build_comment_url.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/exposure_thresholds.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wage_and_hours.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_wall_data.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/wall_types.tsfront_end/src/app/(main)/labor-hub/jobs/page.tsxfront_end/src/app/og/labor-hub/jobs/[slug]/page.tsxfront_end/src/app/og/labor-hub/jobs/[slug]/route/route.tsfront_end/src/components/gradient-carousel.tsxfront_end/src/constants/colors.tsfront_end/src/utils/fonts.tsfront_end/tailwind.config.ts
🚀 Preview EnvironmentYour preview environment is ready!
Details
ℹ️ Preview Environment InfoIsolation:
Limitations:
Cleanup:
|
…ity fixes Round 3 of the consumer-friendly job pages. Layout & visuals - Job Detail hero and Jump-To strip share one container with a horizontal divider, matching the prototype - All Jobs hero and tile wall now share one container with a divider too - "survive AI?" in the hero h1 picks up the prototype's blue-600 accent via t.rich - Mobile breadcrumb drops the 3rd crumb (job name) so it doesn't wrap - Mobile wall is now a 3-col uniform grid, no size variation, no tickers - Bento on mobile: 5-tile grid (Felten, MNA, AOE, Wage, Hours); Curated Insights moves to its own full-width section below - Wage / Hours cards center contents on md+, left-align on mobile, consistent fonts across all 5 tiles - Share card preview switches to container queries (cqw units) so it scales proportionally on narrow viewports instead of squeezing - HIGH chip text in dark mode bumped to salmon-900-dark for readability - Chart card height reduced ~32px Jump-To strip - Prototype-matched chip style: uniform blue-100 / blue-900 active, no per-job color - Mobile: arrows + "Jump to:" label hidden, gradient fade tightened - Scroll position persisted across visits via sessionStorage; active pill is auto-scrolled into view via requestAnimationFrame after restore - aria-current on the active pill; data-active-pill attribute for the ensure-visible lookup - Arrows use items-center + w-9 h-9 so the FontAwesome icon centers correctly; light/dark bg flips via blue-900 / blue-900-dark - gradient-carousel: new optional gradientWidthClass prop (defaults to current w-[152px]); arrow snap-to-edge in scrollByAmount so clicking prev near scrollLeft 0 lands exactly at 0 (canPrev threshold bumped to > 1 for sub-pixel forgiveness) Chart - Reverted to MultiQuestionLineChart for typography + hover behavior - New historicalLabelText / forecastLabelText props on the underlying chart let the section labels be overridden; Job Detail uses "BASELINE" - Line + scatter color picked from 2035 direction (mc2/mc3/mc1) via getSeriesOptions - MultiQuestionLineChartSkeleton accepts the same height prop (and a showTitlePlaceholder flag) so the loading state matches the rendered size exactly — no page jump on hydration A11y & reliability (from the GHAS / CodeQL pass) - Tooltip trigger is now a <button type="button"> with a visible focus ring instead of a non-focusable <span> - Year toggle is role="group" + aria-pressed (was tablist/aria-selected with no associated panels) - decodeEntities (insights + tickers) is now a single-pass replacement so "&lt;" decodes once to "<" instead of double-decoding to "<"; docstring notes plain-text-only usage - OG route /og/labor-hub/jobs/[slug]/route hardened: ?year= clamped to WALL_YEARS allowlist, SCREENSHOT_SERVICE_API_URL validated before use (structured 500 on missing/invalid), fetch wrapped in an AbortController with a 15-second timeout (504 on abort) - Czech translation fix: "Předpovědač" → "Předpovídač" across the three cs.json strings that contained it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
front_end/src/app/og/labor-hub/jobs/[slug]/route/route.ts (2)
71-74:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid returning raw upstream error bodies to clients.
Passing upstream text through directly can leak internal service details. Return a stable generic error payload instead.
Proposed fix
if (!r.ok) { - const text = await r.text(); - return NextResponse.json({ error: text }, { status: r.status }); + return NextResponse.json( + { error: "screenshot service failed" }, + { status: r.status >= 400 && r.status < 600 ? r.status : 502 } + ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts around lines 71 - 74, The handler currently returns raw upstream response bodies (using r.text()) which can leak internal details; instead, read the upstream body for server-side logging (e.g., console.error or your logger) and return a fixed, generic JSON error payload to the client via NextResponse.json while preserving the HTTP status (use r.status). Update the branch that checks r.ok to stop forwarding raw text, log the detailed text internally, and return a stable message such as { error: "Upstream service error" } with NextResponse.json and status r.status.
62-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFail fast when the screenshot API key is missing.
This currently sends
api_key: ""upstream, which turns misconfiguration into avoidable external failures instead of a clear local 500.Proposed fix
+ const apiKey = process.env.SCREENSHOT_SERVICE_API_KEY; + if (!apiKey) { + return NextResponse.json( + { error: "screenshot service API key not configured" }, + { status: 500 } + ); + } + try { const r = await fetch(screenshotEndpoint, { method: "POST", headers: { "Content-Type": "application/json", - api_key: process.env.SCREENSHOT_SERVICE_API_KEY || "", + api_key: apiKey, },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts around lines 62 - 65, The code currently sends api_key: "" when process.env.SCREENSHOT_SERVICE_API_KEY is missing; update the route handler (the GET request handler in route.ts) to validate process.env.SCREENSHOT_SERVICE_API_KEY before making the upstream call and fail fast with a 500/explicit error if it's falsy. Specifically, check SCREENSHOT_SERVICE_API_KEY at the start of the handler and return an error response (or throw) instead of continuing; then build the headers object with api_key set to the validated value so you never send an empty string upstream.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@front_end/src/app/og/labor-hub/jobs/`[slug]/route/route.ts:
- Around line 71-74: The handler currently returns raw upstream response bodies
(using r.text()) which can leak internal details; instead, read the upstream
body for server-side logging (e.g., console.error or your logger) and return a
fixed, generic JSON error payload to the client via NextResponse.json while
preserving the HTTP status (use r.status). Update the branch that checks r.ok to
stop forwarding raw text, log the detailed text internally, and return a stable
message such as { error: "Upstream service error" } with NextResponse.json and
status r.status.
- Around line 62-65: The code currently sends api_key: "" when
process.env.SCREENSHOT_SERVICE_API_KEY is missing; update the route handler (the
GET request handler in route.ts) to validate
process.env.SCREENSHOT_SERVICE_API_KEY before making the upstream call and fail
fast with a 500/explicit error if it's falsy. Specifically, check
SCREENSHOT_SERVICE_API_KEY at the start of the handler and return an error
response (or throw) instead of continuing; then build the headers object with
api_key set to the validated value so you never send an empty string upstream.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 65939d16-41d4-4d9c-ac1f-d6e56d0f4653
📒 Files selected for processing (20)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsxfront_end/src/app/(main)/labor-hub/components/question_cards/multi_question_line_chart.tsxfront_end/src/app/(main)/labor-hub/jobs/[slug]/page.tsxfront_end/src/app/(main)/labor-hub/jobs/components/bento_layout.tsxfront_end/src/app/(main)/labor-hub/jobs/components/exposure_metrics.tsxfront_end/src/app/(main)/labor-hub/jobs/components/job_nav_strip.tsxfront_end/src/app/(main)/labor-hub/jobs/components/jobs_wall.tsxfront_end/src/app/(main)/labor-hub/jobs/components/share_card_preview.tsxfront_end/src/app/(main)/labor-hub/jobs/components/wage_hours_cards.tsxfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_job_insights.tsfront_end/src/app/(main)/labor-hub/jobs/helpers/fetch_tile_tickers.tsfront_end/src/app/(main)/labor-hub/jobs/page.tsxfront_end/src/app/og/labor-hub/jobs/[slug]/route/route.tsfront_end/src/components/gradient-carousel.tsx
✅ Files skipped from review due to trivial changes (2)
- front_end/messages/pt.json
- front_end/messages/es.json
Summary
Adds two consumer-facing routes under the Labor Hub as SEO-friendly entry points to the same forecast data, derived from the Claude Design "Fast Food" prototype:
/labor-hub/jobs/— Forecast Wall: a sized-by-impact tile grid of all 15 tracked occupations with a year toggle (2027 / 2030 / 2035) and a hover-only news-ticker preview on each tile/labor-hub/jobs/[slug]/— Job Detail page: breadcrumb hero with employment-forecast chart, Jump To carousel across all 15 jobs, Felten / MNA / AOE exposure metrics with HIGH/MED/LOW chips + tooltips, Curated Insights (data.ts override → top comments → keyword-matched comments fallback) with click-through to the source comment, Wages + economy-wide Hours bento, share card (1.91:1) with year toggle, Save Image PNG + Share on XData: 15 occupations get a
slugfield ondata.ts; optionalwage_post_id,curated_insights,keyword_aliases, andexcluded_comment_idsavailable per job for future curation without code changes.Reuse, no new packages:
MultiQuestionLineChartfor the hero chart,gradient-carouselfor Jump To,MobileCarouselfor the mobile bento, existingSCREENSHOT_SERVICE_API_URLfor the Share Card OG image. JetBrains Mono added vianext/font/google;salmon-900added to the palette.SEO: per-page
generateMetadata+generateStaticParams(15 prebuilt detail pages), OpenGraph + Twitter card, JSON-LD (Dataset per job, ItemList on All Jobs).i18n: ~30 new
laborHubJobs*keys translated into all 6 locales; job names left in English.The existing Labor Hub dashboard is untouched, and there is intentionally no link from
/labor-hub/to/labor-hub/jobs/in this first pass — discoverability will be handled separately.Test plan
cd front_end && bun run buildsucceeds,.next/server/app/(main)/labor-hub/jobs/page.jsand[slug]/page.jsare generated/labor-hub/jobs/renders the 15-tile wall; year toggle re-sorts sizes/labor-hub/jobs/software-developers/renders with Felten chip = HIGH, tooltip on?icon, Curated Insights populated, Wages + Hours bento, Share card with Save Image / Share on Xview-source:on either route shows<title>, meta description, OG tags, Twitter card, and<script type="application/ld+json">🤖 Generated with Claude Code
Summary by CodeRabbit