From 73fcc526ead975e770fdceaa4f989ab06703ae01 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 4 Jun 2026 15:12:37 +0200 Subject: [PATCH 01/12] perf: reduce Speed Index, fix font-display, clean dead CSS - Change Syne font-display from swap to optional (h1/h2 headings). swap caused FOUT: the LCP element repainted when Syne loaded. With optional and the existing Syne 700 preload in root.tsx, the font renders in the initial paint window on most connections with no swap repaint. - Remove dead Syne 600 @font-face declarations. No CSS rule assigns font-weight 600 to any Syne element (h1/h2 use 700). No preload existed for them either. - Shorten hero fadeUp animation: duration 0.6s to 0.35s, delays halved, from-opacity raised from 0 to 0.7. Above-fold content is no longer fully invisible on initial paint, directly improving Speed Index. - Scope will-change to first three firefly elements. Previously applied to all 8, violating the <=3 simultaneous will-change rule in PERFORMANCE.md. - Remove dead firefly CSS rules for :nth-child(9-12). Hero.tsx renders 8 spans; rules 9-12 were never matched. Update mobile hide comment to reflect the real count (7-8, not 7-12). - Replace hardcoded "offon.dev" string in ConsentBanner.tsx and Footer.tsx with the SITE_NAME constant from src/data/constants.ts. Signed-off-by: Sinduri Guntupalli --- src/components/ConsentBanner.tsx | 3 ++- src/components/Footer.tsx | 4 +-- src/index.css | 45 ++++++++++---------------------- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/components/ConsentBanner.tsx b/src/components/ConsentBanner.tsx index 15f7ae110..f8e0625df 100644 --- a/src/components/ConsentBanner.tsx +++ b/src/components/ConsentBanner.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, type JSX } from "react"; import { Link } from "react-router"; import { Cookie } from "lucide-react"; import { useConsent } from "@/hooks/useConsent"; +import { SITE_NAME } from "@/data/constants"; export function ConsentBanner(): JSX.Element | null { const { consent, grant, deny, reset } = useConsent(); @@ -53,7 +54,7 @@ export function ConsentBanner(): JSX.Element | null {

- We use Google Analytics to understand how visitors use offon.dev. No data is sent to + We use Google Analytics to understand how visitors use {SITE_NAME}. No data is sent to Google until you accept. You can change your preference at any time. See our{" "} { {/* Brand */}

- offon.dev + {SITE_NAME}

{BRAND_SHORT_DESCRIPTION} diff --git a/src/index.css b/src/index.css index fbba36768..625b260e9 100644 --- a/src/index.css +++ b/src/index.css @@ -94,28 +94,12 @@ font-display: optional; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -@font-face { - font-family: 'Syne'; - src: url('/fonts/syne-latin-ext-600-normal.woff2') format('woff2'); - font-weight: 600; - font-style: normal; - font-display: swap; - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -@font-face { - font-family: 'Syne'; - src: url('/fonts/syne-latin-600-normal.woff2') format('woff2'); - font-weight: 600; - font-style: normal; - font-display: swap; - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} @font-face { font-family: 'Syne'; src: url('/fonts/syne-latin-ext-700-normal.woff2') format('woff2'); font-weight: 700; font-style: normal; - font-display: swap; + font-display: optional; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } @font-face { @@ -123,7 +107,7 @@ src: url('/fonts/syne-latin-700-normal.woff2') format('woff2'); font-weight: 700; font-style: normal; - font-display: swap; + font-display: optional; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -385,8 +369,8 @@ html { /* ─── Animations ──────────────────────────────────────────── */ @keyframes fadeUp { - from { opacity: 0; transform: translateY(14px); } - to { opacity: 1; transform: translateY(0); } + from { opacity: 0.7; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } @keyframes marquee { @@ -421,10 +405,10 @@ html { 100% { transform: translate(0, 0); opacity: 0; } } -.animate-fade-up { animation: fadeUp 0.6s ease-out both; } -.animate-fade-up-delay-1 { animation: fadeUp 0.6s ease-out 0.1s both; } -.animate-fade-up-delay-2 { animation: fadeUp 0.6s ease-out 0.2s both; } -.animate-fade-up-delay-3 { animation: fadeUp 0.6s ease-out 0.3s both; } +.animate-fade-up { animation: fadeUp 0.35s ease-out both; } +.animate-fade-up-delay-1 { animation: fadeUp 0.35s ease-out 0.05s both; } +.animate-fade-up-delay-2 { animation: fadeUp 0.35s ease-out 0.10s both; } +.animate-fade-up-delay-3 { animation: fadeUp 0.35s ease-out 0.15s both; } .animate-marquee { animation: marquee 30s linear infinite; } @media (prefers-reduced-motion: reduce) { @@ -456,6 +440,11 @@ html { 0 0 5px 2px hsl(var(--primary) / 0.55), 0 0 10px 3px hsl(var(--primary) / 0.2); animation: fireflyFloat linear infinite; +} +/* will-change creates a GPU compositing layer; cap at 3 to stay within PERFORMANCE.md limit */ +.firefly:nth-child(1), +.firefly:nth-child(2), +.firefly:nth-child(3) { will-change: transform, opacity; } /* vary each dot's position + timing */ @@ -467,13 +456,7 @@ html { .firefly:nth-child(6) { top: 18%; left: 55%; animation-duration: 8s; animation-delay: 1.8s; } .firefly:nth-child(7) { top: 82%; right: 8%; animation-duration: 6.5s; animation-delay: 4s; } .firefly:nth-child(8) { top: 50%; left: 72%; animation-duration: 7s; animation-delay: 2.6s; } -/* four new: fill top strip, mid-left gap, lower-right gap */ -.firefly:nth-child(9) { top: 7%; left: 38%; animation-duration: 6.5s; animation-delay: 0.9s; } -.firefly:nth-child(10) { top: 6%; right: 20%; animation-duration: 7s; animation-delay: 3.5s; } -.firefly:nth-child(11) { top: 46%; left: 6%; animation-duration: 8s; animation-delay: 1.6s; } -.firefly:nth-child(12) { top: 68%; right: 14%; animation-duration: 5.5s; animation-delay: 2.3s; } - -/* On mobile hide fireflies 7–12; display:none also stops their animations */ +/* On mobile hide fireflies 7–8; display:none also stops their animations */ @media (max-width: 639px) { .firefly:nth-child(n+7) { display: none; } } From e2d621ac0afbe638be69f1013525e5b407d0bc76 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 4 Jun 2026 15:28:27 +0200 Subject: [PATCH 02/12] perf/a11y/refactor: size-adjust fallback, motion guard, dedup ALL_LEVEL_SUMMARIES size-adjust fallback for Syne (font-display: optional): - Add 'Syne Fallback' @font-face using local('Arial') with metrics measured via canvas.measureText() on the live site: size-adjust 114.62%, ascent-override 81.14%, descent-override 23.56%. When the optional load window is missed, h1/h2 render in a metric-adjusted Arial that keeps character widths and line heights close to Syne so the fallback looks intentional rather than broken. - Insert 'Syne Fallback' into --font-heading and the h1/h2 @layer base rule. Motion guard alignment (ACCESSIBILITY.md + PERFORMANCE.md): - Move .animate-fade-up*, .animate-marquee, and .firefly animations inside @media (prefers-reduced-motion: no-preference) instead of defining them unconditionally and suppressing with a reduce override. Both approaches are functionally equivalent but the no-preference guard is the pattern the project docs prescribe. Remove the now-redundant reduce block. Deduplicate ALL_LEVEL_SUMMARIES (filter-utils): - Export ALL_LEVEL_SUMMARIES from src/data/adventures/filter-utils.ts. Challenges.tsx had an identical module-level flatMap that reproduced the same projection already done by getLevelSummariesByFilters. Remove the local ALL_LEVELS constant and import the shared export instead. Signed-off-by: Sinduri Guntupalli --- src/data/adventures/filter-utils.ts | 9 ++++++ src/index.css | 46 ++++++++++++++--------------- src/pages/Challenges.tsx | 15 ++-------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/data/adventures/filter-utils.ts b/src/data/adventures/filter-utils.ts index 246e8eeaa..707154d8a 100644 --- a/src/data/adventures/filter-utils.ts +++ b/src/data/adventures/filter-utils.ts @@ -1,6 +1,15 @@ import { ADVENTURE_SUMMARIES } from "./summaries"; import type { RelatedLevelSummary } from "./types"; +export const ALL_LEVEL_SUMMARIES: RelatedLevelSummary[] = ADVENTURE_SUMMARIES.flatMap((a) => + a.levels.map((level) => ({ + level, + adventureId: a.id, + adventureTitle: a.title, + ...(a.isLive ? { isLive: true as const } : {}), + })) +); + /** Returns level summaries matching all selected tags (AND) and/or a difficulty. */ export const getLevelSummariesByFilters = ( tags: string[], diff --git a/src/index.css b/src/index.css index 625b260e9..175961950 100644 --- a/src/index.css +++ b/src/index.css @@ -110,6 +110,18 @@ font-display: optional; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } +/* Metric-adjusted fallback for Syne 700. Used when font-display: optional misses the load + window. size-adjust/ascent/descent keep h1/h2 layout close to Syne so the fallback + render looks intentional rather than broken. Values measured via canvas.measureText() + against Arial at 100px on the live site. */ +@font-face { + font-family: 'Syne Fallback'; + src: local('Arial'); + size-adjust: 114.62%; + ascent-override: 81.14%; + descent-override: 23.56%; + line-gap-override: 0%; +} @import "tailwindcss"; @@ -118,7 +130,7 @@ @theme { --font-sans: Inter, sans-serif; --font-mono: "JetBrains Mono", monospace; - --font-heading: Syne, sans-serif; + --font-heading: Syne, 'Syne Fallback', sans-serif; --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); @@ -349,7 +361,7 @@ html { } h1, h2 { - font-family: 'Syne', sans-serif; + font-family: 'Syne', 'Syne Fallback', sans-serif; font-weight: 700; color: hsl(var(--primary)); } @@ -405,26 +417,12 @@ html { 100% { transform: translate(0, 0); opacity: 0; } } -.animate-fade-up { animation: fadeUp 0.35s ease-out both; } -.animate-fade-up-delay-1 { animation: fadeUp 0.35s ease-out 0.05s both; } -.animate-fade-up-delay-2 { animation: fadeUp 0.35s ease-out 0.10s both; } -.animate-fade-up-delay-3 { animation: fadeUp 0.35s ease-out 0.15s both; } -.animate-marquee { animation: marquee 30s linear infinite; } - -@media (prefers-reduced-motion: reduce) { - .animate-fade-up, - .animate-fade-up-delay-1, - .animate-fade-up-delay-2, - .animate-fade-up-delay-3, - .animate-marquee { - animation: none; - } - /* Firefly particles run a continuous 13-keyframe animation with positional - transforms. Vestibular disorder and motion-sensitive users are affected - by persistent movement even from decorative elements. */ - .firefly { - animation: none; - } +@media (prefers-reduced-motion: no-preference) { + .animate-fade-up { animation: fadeUp 0.35s ease-out both; } + .animate-fade-up-delay-1 { animation: fadeUp 0.35s ease-out 0.05s both; } + .animate-fade-up-delay-2 { animation: fadeUp 0.35s ease-out 0.10s both; } + .animate-fade-up-delay-3 { animation: fadeUp 0.35s ease-out 0.15s both; } + .animate-marquee { animation: marquee 30s linear infinite; } } /* ─── Firefly particles ───────────────────────────────────── */ @@ -439,7 +437,9 @@ html { 0 0 2px 1px hsl(var(--primary)), 0 0 5px 2px hsl(var(--primary) / 0.55), 0 0 10px 3px hsl(var(--primary) / 0.2); - animation: fireflyFloat linear infinite; +} +@media (prefers-reduced-motion: no-preference) { + .firefly { animation: fireflyFloat linear infinite; } } /* will-change creates a GPU compositing layer; cap at 3 to stay within PERFORMANCE.md limit */ .firefly:nth-child(1), diff --git a/src/pages/Challenges.tsx b/src/pages/Challenges.tsx index 756aa7c32..8ebd97590 100644 --- a/src/pages/Challenges.tsx +++ b/src/pages/Challenges.tsx @@ -11,7 +11,7 @@ import { StarterNudge } from "@/components/StarterNudge"; import { ChallengeFilters, type Difficulty } from "@/components/ChallengeFilters"; import { slugToTag, tagToSlug } from "@/data/adventures"; import { ADVENTURE_SUMMARIES, SUMMARY_TAGS } from "@/data/adventures/summaries"; -import { getLevelSummariesByFilters } from "@/data/adventures/filter-utils"; +import { getLevelSummariesByFilters, ALL_LEVEL_SUMMARIES } from "@/data/adventures/filter-utils"; import { SITE_URL, BRAND_NAME } from "@/data/constants"; import { buildPageMeta } from "@/lib/meta"; @@ -42,15 +42,6 @@ export const meta: MetaFunction = ({ params }) => { }); }; -const ALL_LEVELS = ADVENTURE_SUMMARIES.flatMap((adventure) => - adventure.levels.map((level) => ({ - level, - adventureId: adventure.id, - adventureTitle: adventure.title, - ...(adventure.isLive ? { isLive: true as const } : {}), - })) -); - const Challenges = (): JSX.Element => { const { tag: tagSlug } = useParams<{ tag?: string }>(); const initialTag = tagSlug ? slugToTag(tagSlug) ?? null : null; @@ -60,7 +51,7 @@ const Challenges = (): JSX.Element => { const [hasFiltered, setHasFiltered] = useState(false); const isFiltered = activeTopics.length > 0 || activeDifficulty !== null; - const filteredLevels = isFiltered ? getLevelSummariesByFilters(activeTopics, activeDifficulty) : ALL_LEVELS; + const filteredLevels = isFiltered ? getLevelSummariesByFilters(activeTopics, activeDifficulty) : ALL_LEVEL_SUMMARIES; const handleDifficultyChange = (diff: Difficulty | null): void => { setHasFiltered(true); @@ -128,7 +119,7 @@ const Challenges = (): JSX.Element => {

All Adventures - · {ADVENTURE_SUMMARIES.length} {ADVENTURE_SUMMARIES.length === 1 ? "adventure" : "adventures"}, {ALL_LEVELS.length} {ALL_LEVELS.length === 1 ? "challenge" : "challenges"} + · {ADVENTURE_SUMMARIES.length} {ADVENTURE_SUMMARIES.length === 1 ? "adventure" : "adventures"}, {ALL_LEVEL_SUMMARIES.length} {ALL_LEVEL_SUMMARIES.length === 1 ? "challenge" : "challenges"}

From 836b6aea5f2a15a6cb7d8d43145bad3ed6fa8729 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 4 Jun 2026 15:34:44 +0200 Subject: [PATCH 03/12] docs/test: update styleguide and add ALL_LEVEL_SUMMARIES tests - styleguide.md animations table: correct fadeUp values to match the current implementation (opacity 0.7 start, 8px translate, 0.35s duration, halved delays). Add note that all animation classes live inside @media (prefers-reduced-motion: no-preference). - styleguide.md firefly entry: correct duration range (5.5-8.5s, not 6.5-11s), document the will-change cap at three particles. - styleguide.md fonts table: remove Syne weight 600 -- the @font-face declaration was removed; only weight 700 is declared and used. - filterUtils.test.ts: add three tests covering ALL_LEVEL_SUMMARIES (count, shape, and equality with getLevelSummariesByFilters([], null)). Signed-off-by: Sinduri Guntupalli --- src/test/filterUtils.test.ts | 22 +++++++++++++++++++++- styleguide.md | 14 ++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/test/filterUtils.test.ts b/src/test/filterUtils.test.ts index 640332cd7..d26a32dc5 100644 --- a/src/test/filterUtils.test.ts +++ b/src/test/filterUtils.test.ts @@ -1,11 +1,31 @@ import { describe, it, expect } from "vitest"; -import { getLevelSummariesByFilters } from "@/data/adventures/filter-utils"; +import { getLevelSummariesByFilters, ALL_LEVEL_SUMMARIES } from "@/data/adventures/filter-utils"; import { ADVENTURE_SUMMARIES } from "@/data/adventures/summaries"; const allLevels = ADVENTURE_SUMMARIES.flatMap((a) => a.levels); const allDifficulties = Array.from(new Set(allLevels.map((l) => l.difficulty))); const firstTag = Array.from(new Set(ADVENTURE_SUMMARIES.flatMap((a) => a.tags))).sort()[0]; +describe("ALL_LEVEL_SUMMARIES", () => { + it("contains every level across all adventures", () => { + const allLevelCount = ADVENTURE_SUMMARIES.flatMap((a) => a.levels).length; + expect(ALL_LEVEL_SUMMARIES.length).toBe(allLevelCount); + }); + + it("each entry has adventureId, adventureTitle, and level", () => { + ALL_LEVEL_SUMMARIES.forEach((item) => { + expect(item.adventureId).toBeTruthy(); + expect(item.adventureTitle).toBeTruthy(); + expect(item.level).toBeTruthy(); + expect(item.level.id).toBeTruthy(); + }); + }); + + it("matches getLevelSummariesByFilters([], null)", () => { + expect(ALL_LEVEL_SUMMARIES).toEqual(getLevelSummariesByFilters([], null)); + }); +}); + describe("getLevelSummariesByFilters", () => { it("returns all levels across all adventures when no filters are active", () => { const result = getLevelSummariesByFilters([], null); diff --git a/styleguide.md b/styleguide.md index e4c396e0e..aa862cf5f 100644 --- a/styleguide.md +++ b/styleguide.md @@ -26,7 +26,7 @@ All brand copy constants live in `src/data/constants.ts`. Use them instead of ha | Role | Family | Key weights | Format | |---|---|---|---| -| Headings / display (`font-heading`) | Syne | 600, 700 | WOFF2 only (`public/fonts/syne-*.woff2`) | +| Headings / display (`font-heading`) | Syne | 700 | WOFF2 only (`public/fonts/syne-*.woff2`) | | Body & UI (`font-sans`) | Inter | 400, 500, 600 primary (700 available) | WOFF2 only (`public/fonts/inter-*.woff2`) | | Code / mono (`font-mono`, `code`, `pre`) | JetBrains Mono | 400 primary (500, 600 available) | WOFF2 only (`public/fonts/jetbrains-mono-*.woff2`) | @@ -285,17 +285,19 @@ Light mode: no background texture. ## Animations +All animation classes are defined inside `@media (prefers-reduced-motion: no-preference)`. Under `prefers-reduced-motion: reduce` the animation property is absent, so elements render at their natural styles (fully visible, no transform). + | Class | Keyframe | Duration | |---|---|---| -| `.animate-fade-up` | fadeUp (fade from opacity 0 + slide up 14px) | 0.6s ease-out | -| `.animate-fade-up-delay-1` | fadeUp | 0.6s, 0.1s delay | -| `.animate-fade-up-delay-2` | fadeUp | 0.6s, 0.2s delay | -| `.animate-fade-up-delay-3` | fadeUp | 0.6s, 0.3s delay | +| `.animate-fade-up` | fadeUp (fade from opacity 0.7 + slide up 8px) | 0.35s ease-out | +| `.animate-fade-up-delay-1` | fadeUp | 0.35s, 0.05s delay | +| `.animate-fade-up-delay-2` | fadeUp | 0.35s, 0.10s delay | +| `.animate-fade-up-delay-3` | fadeUp | 0.35s, 0.15s delay | | `.animate-marquee` | horizontal scroll left | 30s linear infinite | ### Firefly particles -`.firefly` - 2×2 px dot with `box-shadow` glow in `--primary` color, animated with `fireflyFloat` (8 particles, varying `animation-duration` 6.5–11 s and `animation-delay`). In light mode, `.light .firefly` reduces to 1.5×1.5 px and uses `--firefly-color` (`41 100% 45%`, slightly darker amber) for contrast against the light background. +`.firefly` - 2×2 px dot with `box-shadow` glow in `--primary` color, animated with `fireflyFloat` inside `@media (prefers-reduced-motion: no-preference)` (8 particles, varying `animation-duration` 5.5–8.5 s and `animation-delay`). `will-change: transform, opacity` is applied only to the first three particles to stay within the ≤3 simultaneous limit. In light mode, `.light .firefly` reduces to 1.5×1.5 px and uses `--firefly-color` (`41 100% 45%`, slightly darker amber) for contrast against the light background. --- From c029e97c9cc2c62bf55fde3bb3dd273ffb01765c Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 4 Jun 2026 15:50:28 +0200 Subject: [PATCH 04/12] feat: redirect /adventures to /challenges, update footer and hero copy The /challenges page is the canonical listing. /adventures now redirects there so existing links and breadcrumbs in AdventureDetail and ChallengeDetail continue to work without 404s. - Add src/pages/redirects/ChallengesRedirect.tsx (client-side redirect to /challenges, following the HandbookRedirect pattern) - routes.ts: replace Adventures.tsx with ChallengesRedirect for the /adventures route; keep /adventures/:id and /adventures/:id/levels/:levelId unchanged since those are real content pages - Footer: replace Adventures link with Challenges pointing to /challenges - Challenges hero description: clarify the adventure and challenge structure with a community angle - sitemap.xml: remove /adventures/ listing entry (redirect routes are not indexed per project rules) - react-router.config.ts: remove /adventures from prerender array - e2e/smoke.spec.ts, seo.test.ts, prerender.test.ts: remove /adventures listing route entries; the individual adventure pages are untouched - footer.test.tsx: update Adventures assertion to Challenges - README.md: mark /adventures as a redirect in the routes table Signed-off-by: Sinduri Guntupalli --- README.md | 2 +- e2e/smoke.spec.ts | 1 - public/sitemap.xml | 2 +- react-router.config.ts | 2 +- src/components/Footer.tsx | 2 +- src/pages/Challenges.tsx | 2 +- src/pages/redirects/ChallengesRedirect.tsx | 9 +++++++++ src/routes.ts | 2 +- src/test/footer.test.tsx | 4 ++-- src/test/prerender.test.ts | 5 +---- src/test/seo.test.ts | 1 - 11 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 src/pages/redirects/ChallengesRedirect.tsx diff --git a/README.md b/README.md index dd596543b..fabe2079b 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Adventures are authored as YAML at `src/data/adventures//adventure.yaml` and | Path | Page | Purpose | |---|---|---| | `/` | `Index.tsx` | Home page | -| `/adventures` | `Adventures.tsx` | All adventures listing | +| `/adventures` | redirects to `/challenges` | Legacy alias for the challenges listing | | `/adventures/:id` | `AdventureDetail.tsx` | Adventure landing | | `/adventures/:id/levels/:levelId` | `ChallengeDetail.tsx` | Individual challenge | | `/contribute` | `Contribute.tsx` | How to contribute (technical and non-technical ways) | diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index b84c239a4..b5714fba4 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -14,7 +14,6 @@ const ROUTES: RouteSpec[] = [ { path: "/privacy", title: /Privacy Policy/ }, { path: "/accessibility", title: /Accessibility Statement/ }, { path: "/404", title: /Page Not Found/ }, - { path: "/adventures", title: /Adventures - Hands-on open source challenges/ }, // GENERATED:adventures { path: "/adventures/lex-imperfecta", title: /Lex Imperfecta/ }, { path: "/adventures/lex-imperfecta/levels/beginner", title: /The Twelve Tables/ }, diff --git a/public/sitemap.xml b/public/sitemap.xml index 0fc7c8bf2..f32b7d3ed 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,7 +1,7 @@ https://offon.dev/2026-06-01weekly1.0 - https://offon.dev/adventures/2026-06-01weekly0.9 + https://offon.dev/contribute/2026-06-02monthly0.8 https://offon.dev/sponsors/2026-06-01monthly0.7 https://offon.dev/about/2026-06-01monthly0.7 diff --git a/react-router.config.ts b/react-router.config.ts index f343b8ba9..774c30581 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -13,7 +13,7 @@ export default { }, prerender: [ "/", - "/adventures", + "/404", "/contribute", "/sponsors", diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index b8ed7d249..9fa8f258f 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -31,7 +31,7 @@ export const Footer = (): JSX.Element => { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e76662025..fd4b5c147 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -42,8 +42,8 @@ const NavLinks = ({ homeActive, onNavigate }: NavLinksProps): JSX.Element => ( > Home - Challenges - About + Challenges + About ( > Community - Contribute - Handbook - Sponsors + Contribute + Handbook + Sponsors ); diff --git a/src/components/OtherLevelsCard.tsx b/src/components/OtherLevelsCard.tsx index b7e674352..2079e9bfd 100644 --- a/src/components/OtherLevelsCard.tsx +++ b/src/components/OtherLevelsCard.tsx @@ -47,7 +47,7 @@ export const OtherLevelsCard = ({ {otherLevels.map((level) => (
  • diff --git a/src/components/SponsorStrip.tsx b/src/components/SponsorStrip.tsx index e41256589..4dafa540f 100644 --- a/src/components/SponsorStrip.tsx +++ b/src/components/SponsorStrip.tsx @@ -10,7 +10,7 @@ export const SponsorStrip = (): JSX.Element => { Sponsor challenges, swag, or licenses and connect with the next generation of open source contributors.

    Become a Sponsor