Website overhaul (incl. staging-review fixes)#212
Merged
Conversation
5 tasks
There was a problem hiding this comment.
Pull request overview
This PR advances the website overhaul and applies the latest staging-review fixes across UI theming, docs/SEO affordances, navigation, search, and build/deploy tooling. It also introduces an external-link-aware Markdoc renderer and improves changelog/search UX for browsing and discovery.
Changes:
- Add docs-sync + Docker multi-stage static build flow (with nginx runtime) and extend the
llms-full.txtgenerator to include production guides. - Improve search UX by adding per-result snippets and update changelog navigation to group releases under collapsible version-line sections.
- Update multiple UI components (Hero CTA, callouts/summary contrast, footer/roadmap/team/examples styling) and route link behavior via
AutoLink.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/sync-docs.sh | New rsync-based docs sync script to pull Markdoc pages from the framework repo into this site prior to builds. |
| utils/generate-llms-full.mjs | Include docs/guides in the concatenated llms-full.txt output. |
| src/markdoc/search.mjs | Add snippet generation to search results for improved result previews. |
| src/markdoc/nodes.js | Override Markdoc link rendering to route through the new AutoLink component. |
| src/lib/navigation.js | Update the GoFr CLI navigation description copy. |
| src/components/Search.jsx | Render the new snippet field beneath search results and highlight query text. |
| src/components/HowTo.jsx | Add “schema-only” mode logic intended to emit JSON-LD without visible UI. |
| src/components/Hero.jsx | Replace tertiary CTA with an AI-agent-oriented link to /llms-full.txt. |
| src/components/Footer.jsx | Update “Production Guides” footer link destination. |
| src/components/EcosystemRecognition.jsx | Normalize recognition card accents to a single sky/slate palette. |
| src/components/Callout.jsx | Improve dark-mode contrast for bold text within callouts. |
| src/components/AutoLink.jsx | New component to open external links in a new tab and keep internal links on next/link. |
| src/components/Answer.jsx | Rename “Answer” to “Summary” and improve bold contrast. |
| src/app/team/page.jsx | Adjust top contributors layout to a centered 3-card grid. |
| src/app/roadmap/page.jsx | Point “Suggest a feature” CTA to the feature request template URL. |
| src/app/examples/page.jsx | Simplify tag chip colors to a smaller, consistent palette. |
| src/app/changelog/page.jsx | Improve header emoji stripping, map “Improvements” to enhancements, and group the right-rail by version-line. |
| Dockerfile | Replace prior Dockerfile with a two-stage build (node builder → nginx runtime) for static export. |
| .dockerignore | Add dockerignore entries to reduce build context size. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+8
to
+18
| function isExternal(href) { | ||
| if (!href) return false | ||
| if (href.startsWith('/') || href.startsWith('#')) return false | ||
| if (href.startsWith('mailto:') || href.startsWith('tel:')) return false | ||
| try { | ||
| const url = new URL(href, 'https://gofr.dev') | ||
| const host = url.host.replace(/^www\./, '') | ||
| return host !== 'gofr.dev' && host !== 'staging.gofr.dev' | ||
| } catch { | ||
| return false | ||
| } |
Comment on lines
+21
to
+33
| export function AutoLink({ href, children, ...rest }) { | ||
| if (isExternal(href)) { | ||
| return ( | ||
| <a href={href} target="_blank" rel="noopener noreferrer" {...rest}> | ||
| {children} | ||
| </a> | ||
| ) | ||
| } | ||
| return ( | ||
| <Link href={href || '#'} {...rest}> | ||
| {children} | ||
| </Link> | ||
| ) |
Comment on lines
+179
to
+183
| {result.snippet && ( | ||
| <p className="mt-1 line-clamp-2 text-xs text-slate-400 dark:text-slate-500"> | ||
| <HighlightQuery text={result.snippet} query={query} /> | ||
| </p> | ||
| )} |
Comment on lines
+184
to
+213
| function buildSnippet(content, query, maxLen) { | ||
| if (maxLen === undefined) maxLen = 140 | ||
| if (!content || !query) return undefined | ||
| let lower = content.toLowerCase() | ||
| let q = query.toLowerCase() | ||
| let idx = lower.indexOf(q) | ||
| let matchLen = q.length | ||
| if (idx === -1) { | ||
| // Try matching any single word from a multi-word query | ||
| let words = q.split(/\\s+/).filter(function (w) { return w.length > 1 }) | ||
| for (let i = 0; i < words.length; i++) { | ||
| let wIdx = lower.indexOf(words[i]) | ||
| if (wIdx !== -1) { | ||
| idx = wIdx | ||
| matchLen = words[i].length | ||
| break | ||
| } | ||
| } | ||
| } | ||
| if (idx === -1) return undefined | ||
| let half = Math.floor((maxLen - matchLen) / 2) | ||
| let start = Math.max(0, idx - half) | ||
| let end = Math.min(content.length, idx + matchLen + half) | ||
| let head = start > 0 ? '\u2026 ' : '' | ||
| let tail = end < content.length ? ' \u2026' : '' | ||
| let s = content.slice(start, end) | ||
| if (start > 0) s = s.replace(/^\\S*\\s/, '') | ||
| if (end < content.length) s = s.replace(/\\s\\S*$/, '') | ||
| return head + s + tail | ||
| } |
Comment on lines
+6
to
+44
| @@ -19,6 +30,19 @@ export function HowTo({ name, description, steps = [], children }) { | |||
| } | |||
| : null | |||
|
|
|||
| const hasVisibleContent = | |||
| Boolean(name) || Boolean(description) || Boolean(children) | |||
|
|
|||
| if (!hasVisibleContent) { | |||
| // Schema-only: emit JSON-LD with no surrounding chrome. | |||
| return jsonLd ? ( | |||
| <script | |||
| type="application/ld+json" | |||
| dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} | |||
| /> | |||
| ) : null | |||
| } | |||
Comment on lines
+431
to
+434
| <details | ||
| key={major} | ||
| open={major === openMajor} | ||
| className="group -ml-px" |
Comment on lines
+390
to
+411
| // Compute a major-line key like "v1.56.x" from a tag like "v1.56.4". | ||
| // Tags that don't parse fall into "other" so the rail still has a | ||
| // bucket to put them in. | ||
| function majorKey(tag) { | ||
| const m = tag && /^(v\d+\.\d+)/.exec(tag) | ||
| return m ? `${m[1]}.x` : 'other' | ||
| } | ||
|
|
||
| // Group releases by major-line. Order is preserved from the source | ||
| // array (newest first), so the first group is the most recent major. | ||
| function groupByMajor(items) { | ||
| const groups = new Map() | ||
| for (const r of items) { | ||
| const key = majorKey(r.tag) | ||
| if (!groups.has(key)) groups.set(key, []) | ||
| groups.get(key).push(r) | ||
| } | ||
| return [...groups.entries()] | ||
| } | ||
|
|
||
| // Right rail — releases grouped by major-version line inside native | ||
| // <details>/<summary> blocks. The major containing the active tag (or |
| dst="$WEBSITE_ROOT/src/app/$route/" | ||
| if [[ -d "$src" ]]; then | ||
| mkdir -p "$dst" | ||
| rsync -a --exclude='layout.jsx' "$src" "$dst" |
Comment on lines
+41
to
+42
| # `yarn refresh-data` on the host before docker build. | ||
| RUN yarn next build |
…e, agent-first CTA, search snippets, schema-only HowTo, and proper Dockerfile
Round-up of fixes from the staging.gofr.dev walkthrough:
Theming / legibility
- Answer / Callout: add prose-strong overrides so bold text inside callouts
stays high-contrast on dark backgrounds (was nearly invisible on /comparison)
- Answer: rename the "Answer" label to "Summary" — pages aren't asking
questions, so the Q&A framing was misleading
- EcosystemRecognition: collapse rose / sky / amber tints to a single sky
accent + slate-neutral meta pills; differentiation comes from the actual
TW / CNCF / JB labels, not from hue
- examples chips: replace the 16-color rainbow with a sky / emerald / slate
3-bucket palette aligned with the logo
Hero CTA
- Replace the personal-Calendly "block our calendar" tertiary CTA with an
AI-agent handoff: "Building with an AI coding agent? Hand it our LLM
guide →" pointing at /llms-full.txt. The site already ships
llms.txt / llms-full.txt; the new framing matches how agents are
actually onboarded onto a framework
Changelog
- Group the right-side rail by major version with collapsible <details>
groups; the active major is open by default, older majors collapse
- Dim entries outside the active major (slate-600 vs slate-500) so the
current release stands out
- Strip emoji prefixes via \p{Extended_Pictographic} (the previous regex
missed multi-codepoint sequences and let "💡 Improvements" through as
"� improvements")
- Map "Improvements" headers to the enhancements chip palette so the chip
is styled instead of falling through to the slate default
Search palette
- Add a content snippet under the title in search results — 140-char
window centered on the matched term, with the matched substring
highlighted via the existing HighlightQuery component
External links
- New AutoLink component wired into Markdoc's link-node renderer:
external URLs (host !== gofr.dev / staging.gofr.dev) get target="_blank"
rel="noopener noreferrer"; internal links continue to use next/link for
prefetching. mailto: / tel: are treated as internal (no new tab).
Footer / Roadmap / Team
- Footer "Production Guides" link points to /docs/guides/dockerizing-gofr-services
(was deploying-to-kubernetes — wrong as the entry point)
- Roadmap "Suggest a feature" CTA points at the feature_request.md template
directly, not the GitHub /issues/new/choose chooser
- Top contributors: 3 cards instead of 5; grid changed from
lg:grid-cols-5 max-w-4xl to grid-cols-1 sm:grid-cols-3 max-w-3xl so the
3-card row centers cleanly
Navigation
- Revert the GoFr CLI navigation bloat: References shows a single
"GoFr CLI" entry (matching Context / Configs / Testing weight) instead
of four flat siblings under it. Per-command pages stay reachable via
direct URL and the overview's in-page command index
HowTo / SEO
- HowTo component gains a schema-only mode: passing only `steps` (without
`name`/`description`/`children`) emits the JSON-LD HowTo block silently,
so guides can drop a self-closing {% howto steps=[...] /%} for AEO
without changing the visible page
- generate-llms-full.mjs: add the production guides section so the LLM-
ready index covers the dockerizing / k8s / helm / cloud-deployment
guides agents are most likely to need
Build
- Replace the half-finished Dockerfile with a real two-stage build:
builder runs yarn install + yarn next build (output: 'export' produces
./out), runtime is nginx:1.27-alpine serving /usr/share/nginx/html
on port 3000 via the existing nginix.conf
- New utils/sync-docs.sh — idempotent rsync that pulls
gofr-dev/gofr/docs/{quick-start,advanced-guide,datasources,guides,
references,faq,learn,why-gofr,migrate,comparison}/ into the
corresponding src/app/ paths before next build (skips layout.jsx so
React layout wrappers stay intact)
- Add .dockerignore for a tighter build context
Verified end-to-end via the new Dockerfile against localhost:3001 — all
sentinel routes return 200 and render real content (no 404 fallback).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded { gofr.dev, staging.gofr.dev } pair with a single
suffix match against gofr.dev so any current or future subdomain (the
existing staging.gofr.dev, plus future console.gofr.dev / playground
subdomains) is treated as internal — clicking from one gofr.dev
property to another stays in the same tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
aryanmehrotra
force-pushed
the
feature/website-overhaul
branch
from
May 5, 2026 11:10
3cbfa5d to
407e74d
Compare
…ight Two combined bugs broke ↑/↓/Enter in the ⌘K command palette: 1. Items were filtered + URL-deduped at render time inside SearchResults, but Algolia tracks active-item state against the *original* collection it was given by getItems(). Pressing ↑/↓ updated activeItemId pointing at items that had been filtered out before render, and Enter triggered onSelect for an invisible item — visible symptom was "buttons do nothing". 2. The <li> for each result had no active-state styling, so even when activeItemId was correct, the user saw no visual cue that an item was focused. Fix: - Move filter + URL-dedupe into the source's getItems(), so what the autocomplete state holds matches what's rendered. SearchResults now just maps and renders. - Add aria-selected:bg-slate-100 dark:aria-selected:bg-slate-700/30 to each <li>. Algolia's getItemProps emits aria-selected="true" on the focused item; Tailwind's aria-selected variant picks it up. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… snippet
Searching short queries that match a section heading (e.g. "dat" →
"Datasources") was returning seven visually-identical rows because:
1. Each result rendered its `title` as the headline, but for section-
level matches across the seven datasource pages the section title
("Datasources") is the same on every page. The actual page name
("MongoDB", "Redis", etc.) lived in `pageTitle` but wasn't shown
prominently.
2. The hierarchy line (driven by navigation lookup) often collapsed to
the same word as the title, adding zero information.
3. Snippets were suppressed for title-matched queries — exactly the
case where two results share a title and the snippet is the only
field that disambiguates them.
Fix:
- SearchResult now derives a real page name (`result.pageTitle ||
result.title`) and renders that as the headline. For section-level
matches, the section name is appended as `· Section`.
- Adds a URL-derived path breadcrumb (`/docs/datasources/mongodb` →
"Datasources › Mongodb") as the secondary line. Always present, so
two results from different pages always look different.
- search.mjs: drop the `titleOnly → snippet=undefined` gate. Always
build a snippet from page content; falls back to undefined only when
the query genuinely never appears in the body.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pages without a frontmatter `title:` were getting `pageTitle = "Untitled"`
written into the search index, which my recent search-result rewrite
then promoted to the headline — so `/faq`, `/migrate/from-aspnet-core`,
`/migrate/from-chi`, etc. all rendered as "Untitled · Datasources" in
the ⌘K palette. Useless.
Resolve the page title at index-build time with three fallbacks:
1. Frontmatter `title:` (preferred)
2. First body heading (h1/h2 from the AST walk)
3. URL-derived slug, prettified ("from-aspnet-core" → "From Aspnet Core")
The literal "Untitled" string is now gone from search.mjs entirely. The
runtime `|| 'Untitled'` fallback is replaced with `|| url` as a purely
defensive guard — sections[0][0] is always populated by the loader.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Snippets in the ⌘K palette were leaking raw markdown source — link
syntax `[Text](url)`, heading markers `##`, bold markers `**` — because
the index builder was appending the raw markdown body alongside the
AST-extracted clean text:
fullRawText = allRawText.join(' ')…
fullRawText += ' ' + md.replace(/^---[\s\S]*?---/, '').trim() ← here
A reader searching "configs" on /learn was getting a snippet like
"…and [Configuration Reference](/docs/references/configs). ## Track C
— Building for production **Estimated time: 2–3…" — implementation
details, not content.
Drop the raw-markdown append. The AST walk (text + code + code_block
nodes, recursively) gives clean visible text only: link nodes
contribute the part before the parens, heading/strong/emphasis nodes
contribute their inner text. Snippets are now plain readable English.
Trade-off: searches matching only a URL slug (e.g. "configs" inside
`(/docs/references/configs)`) will no longer surface pages that just
link to that URL. The actual /docs/references/configs page still
ranks first via its own title/content, which is the right behaviour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot review on PR #212 surfaced 9 inline comments. Eight are real and fixed here; the ninth (sync-docs.sh idempotency) is moot because that script is being removed in this commit — the actual deploy-time docs sync happens in the framework's docs/Dockerfile (which does `COPY docs/navigation.js /app/src/lib` plus the per-route copies), not from a script in this repo. AutoLink (src/components/AutoLink.jsx) - Block dangerous URL schemes. Anything outside { http:, https:, mailto:, tel: } now renders as an inert <span> instead of a clickable anchor. Closes the javascript: / data: / vbscript: reflection vector for any rogue link in markdoc content. - Hash-only links (#section) and mailto:/tel: now render as plain <a>. next/link is for in-app routing and treats hash-only hrefs inconsistently; plain <a> matches native browser scroll behaviour. HowTo (src/components/HowTo.jsx) - Schema-only mode is now keyed on `children` (body content), not on `name`/`description`. Self-closing `{% howto name="..." description="..." steps=[...] /%}` correctly emits JSON-LD with no visible chrome — previously the visible <section> rendered whenever `name` was present, contradicting the documented intent. Verified: db-migrations-in-cicd / horizontal-pod-autoscaler / production-tracing now render JSON-LD only. Search (src/components/Search.jsx, src/markdoc/search.mjs) - HighlightQuery splits the query into words and passes each as a searchWord. Multi-word queries that match a single word in the snippet (via buildSnippet's per-word fallback) now actually highlight the matched word, instead of trying to highlight the full original query and matching nothing. - buildSnippet clamps `half` to >= 0 so a matched token longer than maxLen no longer produces a window that starts past the match (start advances and end retreats; the snippet would silently lose the match itself). Changelog rail (src/app/changelog/page.jsx) - <details open={...}> was effectively controlled — every render forced the prop value, overwriting user toggles. Refactor each group into ReleaseRailGroup with local useState; useEffect reopens the group when it becomes the active version line so deep-links still expand the right group, but in-between user collapses stick. - Rename majorKey → versionLineKey (it's <major>.<minor>, not just <major>), groupByMajor → groupByVersionLine, activeMajor → activeLine. Code now matches what the UI actually shows. Build (Dockerfile, utils/sync-docs.sh) - Run the offline prebuild generators (changelog-rss, llms-full) inside the Dockerfile before `next build`. Skipping `yarn build` also skipped these two even though they don't hit GitHub; the container was shipping stale RSS / llms-full.txt. GitHub-API fetches stay in CI's `yarn refresh-data` step (rate-limited outside CI). - Drop utils/sync-docs.sh: deploy-time sync is the framework docs/Dockerfile's job (FROM ghcr.io/gofr-dev/website + per-route COPY of docs/* and docs/navigation.js). The header comment in Dockerfile now spells out this overlay pattern. Navigation (src/lib/navigation.js) - Restore the missing Production Guides section (18 entries: dockerizing, k8s, helm, cloud, ci/cd, db-migrations, tracing, prometheus, logging, graceful shutdown, hpa, connection pooling, load testing, 12-factor, auth-in-k8s, service mesh, multi-environment, distributed tracing). The website-side nav had drifted behind the framework's docs/navigation.js; the framework's docs/Dockerfile overlays the framework version in prod, so this update aligns local-preview rendering with what prod actually serves. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Long-running feature branch for the website overhaul. The latest commit on the branch —
website: staging-review fixes(see file list below) — addresses the round-up of issues raised in the staging.gofr.dev walkthrough. The earlier commits on this branch are prior overhaul work already iterated on.What the latest commit changes
Theming / legibility
Answer.jsx/Callout.jsx: bold text inside callouts now stays high-contrast on dark backgrounds (was nearly invisible on/comparison).Answer.jsx: rename "Answer" label → Summary.EcosystemRecognition.jsx: collapse rose/sky/amber to a single sky accent + slate-neutral meta.examples/page.jsx: replace 16-color rainbow chips with a sky / emerald / slate 3-bucket palette.Hero CTA
/llms-full.txt.Changelog
<details>); older majors collapsed by default; older entries dimmed.\p{Extended_Pictographic}(the prior regex missed multi-codepoint sequences and let💡 Improvementsthrough as� improvements).Search palette
External links
AutoLinkcomponent wired into Markdoc's link-node renderer. External URLs (host !== gofr.dev / staging.gofr.dev) gettarget="_blank" rel="noopener noreferrer"; internal links stay onnext/link.Footer / Roadmap / Team
/docs/guides/dockerizing-gofr-services.?template=feature_request.md.grid-cols-1 sm:grid-cols-3 max-w-3xlrow (waslg:grid-cols-5 max-w-4xlwhich left-aligned with empty cells).Navigation
HowTo / SEO
HowTo.jsxgains a schema-only mode — passing onlystepsemits the JSON-LD HowTo silently, so guides can drop a self-closing{% howto steps=[...] /%}for AEO without changing visible chrome.generate-llms-full.mjs: add the production guides section tollms-full.txt.Build
node:23-alpinebuilder →nginx:1.27-alpineruntime serving the static export).utils/sync-docs.sh— idempotent rsync that pullsgofr-dev/gofr/docs/...into the correspondingsrc/app/...paths beforenext build..dockerignore.Test plan
docker build -t gofr-website:local .(after./utils/sync-docs.sh)docker run --rm -p 3001:3000 gofr-website:local/,/changelog,/examples,/team,/roadmap,/comparison,/why-gofr,/faq,/learn,/migrate,/migrate/from-gin,/docs/quick-start/introduction,/docs/guides/{dockerizing-gofr-services, deploying-to-kubernetes, helm-chart-starter, cloud-deployment},/docs/references/gofrcli,/docs/references/gofrcli/{init, migrate, wrap-grpc, store}.gofr-dev/gofr#fix/doc-broken-links.🤖 Generated with Claude Code