feat(seo): OpenGraph image generation, SSR improvements, Lighthouse CI#2367
Conversation
…ding + guides OpenGraph: - Add opengraph-image.tsx + twitter-image.tsx to landing app (Satori-based PNG at build time) - Add opengraph-image.tsx + twitter-image.tsx to guides app root - Add per-guide opengraph-image.tsx for [slug] route with post title/description/categories - Fix landing layout to remove broken /og-image.jpg reference; Next.js auto-wires generated images - Fix guides layout to use proper Metadata type with full OG + Twitter tags, metadataBase, keywords - Improve guide slug generateMetadata to include openGraph (article type) + twitter fields Lighthouse / SSR: - Convert guides app/page.tsx from 'use client' to server component — hero, features, and featured guides now SSR; only the filter/search grid stays client via new FilterableGuides component - Extract filterable-guides.tsx client component that reads useSearchParams for category/search - Remove unnecessary useQuery wrapper in header.tsx around synchronous getAllCategories() Lighthouse CI: - Add .lighthouserc.js to both apps targeting ./out static export - Add @lhci/cli devDependency + `bun run lighthouse` script to both apps https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (20)
WalkthroughThis PR enhances SEO and web performance monitoring across the guides and landing apps by adding Lighthouse CI configuration files, implementing dynamic Open Graph and Twitter image generators, expanding metadata definitions for social sharing, and refactoring the guides home page from client-side filtering to server-side rendering with a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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. Review rate limit: 0/1 reviews remaining, refill in 7 minutes and 21 seconds.Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates the PackRat Landing and PackRat Guides Next.js apps with Lighthouse CI tooling and improved SEO/social sharing metadata, plus a refactor of the Guides home page filtering to a dedicated client component.
Changes:
- Add Lighthouse CI scripts/config (
lhci autorun) for landing and guides apps. - Add Next.js metadata image routes (
opengraph-image.tsx,twitter-image.tsx) and modernize metadata (metadataBase, structured titles). - Refactor Guides index filtering/search-param handling into a new
FilterableGuidesclient component and simplify category loading in the header.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/landing/package.json | Adds LHCI script/dependency for landing app. |
| apps/landing/app/twitter-image.tsx | Adds Twitter card image generation route. |
| apps/landing/app/opengraph-image.tsx | Adds Open Graph image generation route. |
| apps/landing/app/layout.tsx | Updates metadata (authors formatting, metadataBase, removes explicit OG/Twitter image URLs). |
| apps/landing/.lighthouserc.js | Adds LHCI configuration targeting ./out. |
| apps/guides/package.json | Adds LHCI script/dependency for guides app. |
| apps/guides/components/header.tsx | Removes React Query category fetch and computes categories inline. |
| apps/guides/components/filterable-guides.tsx | New client component for filtering guides via URL search params. |
| apps/guides/app/twitter-image.tsx | Adds Twitter card image generation route. |
| apps/guides/app/page.tsx | Converts to server component and delegates filtering to FilterableGuides. |
| apps/guides/app/opengraph-image.tsx | Adds Open Graph image generation route. |
| apps/guides/app/layout.tsx | Improves typed metadata with metadataBase, keywords, OG/Twitter config. |
| apps/guides/app/guide/[slug]/page.tsx | Enhances per-guide metadata (article OG/Twitter fields). |
| apps/guides/app/guide/[slug]/opengraph-image.tsx | Adds per-guide dynamic OG image route with static params. |
| apps/guides/.lighthouserc.js | Adds LHCI configuration targeting ./out. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "dev": "next dev", | ||
| "doctor:react": "bunx react-doctor", | ||
| "enhance-content": "bun run scripts/enhance-content.ts", | ||
| "lighthouse": "bunx lhci autorun", |
| export default function Header() { | ||
| const [scrolled, setScrolled] = useState(false); | ||
|
|
||
| // Fetch categories using TanStack Query | ||
| const { data: categories = [] } = useQuery({ | ||
| queryKey: ['categories'], | ||
| queryFn: getAllCategories, | ||
| }); | ||
| const categories = getAllCategories(); | ||
|
|
| description: post.description, | ||
| url: `${siteUrl}/guide/${slug}`, | ||
| siteName: 'PackRat Guides', | ||
| publishedTime: post.date, | ||
| authors: post.author ? [post.author] : ['PackRat Team'], | ||
| tags: post.categories, | ||
| }, |
| settings: { | ||
| // Simulate mobile (Lighthouse default) and desktop | ||
| formFactor: 'desktop', | ||
| screenEmulation: { | ||
| mobile: false, | ||
| width: 1350, |
| settings: { | ||
| formFactor: 'desktop', | ||
| screenEmulation: { | ||
| mobile: false, | ||
| width: 1350, | ||
| height: 940, | ||
| deviceScaleFactor: 1, |
| "clean": "bunx rimraf node_modules .next out", | ||
| "dev": "next dev", | ||
| "doctor:react": "bunx react-doctor", | ||
| "lighthouse": "bunx lhci autorun", |
| "clean": "bunx rimraf node_modules .next out", | ||
| "dev": "next dev", | ||
| "doctor:react": "bunx react-doctor", | ||
| "lighthouse": "bunx lhci autorun", |
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File CoverageNo changed files found. |
Coverage Report for API Unit Tests Coverage (./packages/api)
File CoverageNo changed files found. |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/guides/app/guide/`[slug]/opengraph-image.tsx:
- Line 104: The footer string "packrat.world/guides" is inconsistent with the
app's canonical domain; update the literal to "guides.packrat.world" in the
OpenGraph component so it matches the rest of the site—locate the occurrence of
the string "packrat.world/guides" in opengraph-image.tsx (the footer/text
rendering) and replace it with "guides.packrat.world".
In `@apps/guides/app/guide/`[slug]/page.tsx:
- Line 30: The duplicate siteUrl constant should be extracted to a single shared
config export (e.g., create lib/config.ts exporting const siteUrl =
'https://guides.packrat.world') and then replace the local declaration in
page.tsx (where siteUrl is defined) with an import from that config; also update
the other occurrence in layout.tsx to import the same siteUrl, run a quick
search for any other siteUrl definitions and consolidate them to the new
exported symbol to avoid drift.
In `@apps/guides/app/layout.tsx`:
- Line 17: The constant siteUrl is duplicated; export it from the module that
currently defines it by changing the declaration to an exported symbol (export
const siteUrl = 'https://guides.packrat.world') so other modules can import it,
and update the consumer (guide/[slug]/page.tsx) to import { siteUrl } from this
module (or alternatively move siteUrl into a shared config module and import
from there) to maintain a single source of truth.
In `@apps/guides/app/twitter-image.tsx`:
- Around line 6-69: The Image component in apps/guides/app/twitter-image.tsx
duplicates most JSX from opengraph-image.tsx; extract the shared markup into a
new OGImageLayout React component that accepts props (e.g., title, subtitle,
emoji/icon, and size) and returns the shared <div> structure used inside
ImageResponse, then import and use OGImageLayout inside the Image() function
(which currently calls ImageResponse) and update opengraph-image.tsx to render
the same layout via OGImageLayout with platform-specific props; ensure
ImageResponse still wraps OGImageLayout output and forward the size prop to
preserve existing behavior.
In `@apps/guides/components/filterable-guides.tsx`:
- Around line 43-45: Remove the redundant inner Suspense wrapping
CategoryFilter: since CategoryFilter reads the same useSearchParams() as
FilterableContent and FilterableContent is already wrapped in a Suspense
boundary, delete the Suspense wrapper around the CategoryFilter component
(locate the JSX where <Suspense> wraps <CategoryFilter categories={categories}
/>) so only the outer Suspense around FilterableContent remains.
In `@apps/guides/package.json`:
- Line 13: The "lighthouse" npm script currently runs "bunx lhci autorun"
without ensuring the app output exists; update the package.json "lighthouse"
script to invoke the app's build step first (i.e., run the package's "build"
script or guides-specific build target) and only then run bunx lhci autorun so
./out is produced before LHCI executes; modify the "lighthouse" entry
accordingly and ensure it references the correct build script name used in this
package.
In `@apps/landing/package.json`:
- Line 10: Update the "lighthouse" npm script in package.json so it
builds/exports the site into ./out before invoking LHCI: ensure the script runs
the project build and export step that produces the ./out directory (e.g., the
framework-specific build/export command used in this repo) and only then runs
"lhci autorun" so LHCI has the expected artifacts to read.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 6045cc3f-d029-4324-9328-b2d01c0c4eef
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock,!bun.lock
📒 Files selected for processing (15)
apps/guides/.lighthouserc.jsapps/guides/app/guide/[slug]/opengraph-image.tsxapps/guides/app/guide/[slug]/page.tsxapps/guides/app/layout.tsxapps/guides/app/opengraph-image.tsxapps/guides/app/page.tsxapps/guides/app/twitter-image.tsxapps/guides/components/filterable-guides.tsxapps/guides/components/header.tsxapps/guides/package.jsonapps/landing/.lighthouserc.jsapps/landing/app/layout.tsxapps/landing/app/opengraph-image.tsxapps/landing/app/twitter-image.tsxapps/landing/package.json
| export default function Image() { | ||
| return new ImageResponse( | ||
| <div | ||
| style={{ | ||
| background: 'linear-gradient(135deg, #1E3A5F 0%, #1a56a0 60%, #0284C7 100%)', | ||
| width: '100%', | ||
| height: '100%', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| fontFamily: 'system-ui, -apple-system, sans-serif', | ||
| padding: '60px', | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '20px', | ||
| marginBottom: '32px', | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| width: '72px', | ||
| height: '72px', | ||
| background: 'rgba(255,255,255,0.2)', | ||
| borderRadius: '20px', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| fontSize: '40px', | ||
| }} | ||
| > | ||
| 🏔️ | ||
| </div> | ||
| <div | ||
| style={{ | ||
| fontSize: '60px', | ||
| fontWeight: 700, | ||
| color: 'white', | ||
| letterSpacing: '-2px', | ||
| }} | ||
| > | ||
| PackRat Guides | ||
| </div> | ||
| </div> | ||
| <div | ||
| style={{ | ||
| fontSize: '30px', | ||
| color: 'rgba(255,255,255,0.9)', | ||
| textAlign: 'center', | ||
| maxWidth: '760px', | ||
| lineHeight: 1.4, | ||
| fontWeight: 500, | ||
| }} | ||
| > | ||
| Expert hiking and outdoor guides for your next adventure | ||
| </div> | ||
| </div>, | ||
| { ...size }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider extracting shared OG image layout.
This file shares ~90% of its JSX with opengraph-image.tsx. A shared OGImageLayout component could reduce duplication, but given this is a POC and the files may diverge for platform-specific needs, this is optional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/guides/app/twitter-image.tsx` around lines 6 - 69, The Image component
in apps/guides/app/twitter-image.tsx duplicates most JSX from
opengraph-image.tsx; extract the shared markup into a new OGImageLayout React
component that accepts props (e.g., title, subtitle, emoji/icon, and size) and
returns the shared <div> structure used inside ImageResponse, then import and
use OGImageLayout inside the Image() function (which currently calls
ImageResponse) and update opengraph-image.tsx to render the same layout via
OGImageLayout with platform-specific props; ensure ImageResponse still wraps
OGImageLayout output and forward the size prop to preserve existing behavior.
Next.js requires export const dynamic = 'force-static' on opengraph-image and twitter-image route files when the app uses output: 'export', otherwise the build fails with "revalidate not configured" error. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
TestIds enum was renamed to testIds object in feat/testids. Update the two remaining callers that were missed: profile sign-out button and trip form submit button. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
- Use siteConfig.url instead of local siteUrl constants (single source of truth) - Fix guides siteConfig.url to correct domain (guides.packrat.world) - Remove openGraph.authors from article metadata (names aren't valid og:article:author URLs) - lighthouse scripts now run build first: bun run build && bunx lhci autorun - Fix misleading comment in .lighthouserc.js (desktop-only profile) - Fix URL in per-guide OG image footer: packrat.world/guides → guides.packrat.world - Remove redundant inner Suspense around CategoryFilter in filterable-guides - Memoize getAllCategories() in header to avoid re-walking posts on scroll re-renders https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
… profile - landing siteConfig.url: getpackrat.com → packratai.com - guides siteConfig.url/ogImage: guides.packrat.world → guides.packratai.com - guides per-slug OG image footer: guides.packrat.world → guides.packratai.com - Both .lighthouserc.js now run desktop + mobile form factors (390×844, 3× DPR, mobile throttling) so Lighthouse CI reports scores for both profiles https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
Runs on PRs that touch apps/guides, apps/landing, or packages/web-ui. Both apps build and run lhci autorun in parallel jobs. Results upload to temporary-public-storage (no new secrets required). The workflow is not a required status check so it informs without blocking merges. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
Addresses CodeQL findings — locks GITHUB_TOKEN to read-only at the workflow level, which applies to both jobs. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
Performance floor: 80 (CI variability makes 90 flaky). Accessibility, best-practices, SEO, and all CWV thresholds: 90+. Jobs now fail rather than warn when scores drop below threshold. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
LHCI's settings field takes a single object, not an array — passing an array caused both jobs to fail on their first run. Fix by splitting into .lighthouserc.js (desktop) and .lighthouserc.mobile.js (mobile), then running lhci autorun --config=<file> twice in each workflow job. Mobile CWV thresholds are relaxed vs desktop (FCP 3 s, LCP 4 s, TBT 600 ms) to account for 4× CPU throttling in CI. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
Blocking on score thresholds before we have baseline data causes false CI failures. Two confirmed issues: 1. Landing app references several image paths (/trail-prep.png, /trail-map-minimal.png, etc.) that don't exist in public/ — 404s drag best-practices below 0.9. Fix requires real assets. 2. Performance scores on shared GitHub runners are noisy; our 0.8 floor may be above what CI hardware achieves even on a well-optimized site. continue-on-error keeps score data visible in job logs so we can calibrate thresholds based on real numbers, then re-enable blocking. https://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU
Summary
/og-image.jpg; guides had zero OG tags. Now both use Next.jsopengraph-image.tsx/twitter-image.tsx(Satori-based PNG generation at build time, compatible withoutput: 'export'viadynamic = 'force-static')app/guide/[slug]/opengraph-image.tsxgenerates a unique 1200×630 PNG per guide with its title, description, and category pillslayout.tsxupgraded to typedMetadatawithopenGraph,twitter,keywords,authors,metadataBase; guide sluggenerateMetadatanow returnsopenGraph: { type: 'article' }+ twitter fieldsapp/page.tsxwas'use client'(entire homepage lost SSR). Converted to server component; hero, features, and featured guides now render in initial HTML. Filtering/search extracted to newfilterable-guides.tsxclient componentuseQuerywrapper around synchronousgetAllCategories()in guides header.lighthouserc.jsadded to both apps targeting./out;bun run lighthousescript +@lhci/clidevDep addedTestIds→testIds— two files missed the enum-to-object migration from the previous PR (profile/index.tsx,TripForm.tsx); fixescheck-typesCITest plan
bun run buildinapps/landingandapps/guides— both should complete without errorsbun run lighthousein either app after build to get Lighthouse scoreshttps://claude.ai/code/session_012wcbLpyGkSMHcoy7k3xFWU