diff --git a/apps/guides/.lighthouserc.js b/apps/guides/.lighthouserc.js index c1ccf1895d..074ffb2ee0 100644 --- a/apps/guides/.lighthouserc.js +++ b/apps/guides/.lighthouserc.js @@ -17,30 +17,20 @@ module.exports = { }, }, assert: { - assertMatrix: [ - { - // Next.js error pages (404/500) inherently lack , <html lang>, - // and meta description. They're never user-shareable; skip them. - matchingUrlPattern: '.*/(404|500)\\.html$', - }, - { - matchingUrlPattern: '.*\\.html$', - assertions: { - 'categories:performance': ['error', { minScore: 0.8 }], - 'categories:accessibility': ['error', { minScore: 0.9 }], - 'categories:best-practices': ['error', { minScore: 0.9 }], - 'categories:seo': ['error', { minScore: 0.9 }], - 'first-contentful-paint': ['error', { maxNumericValue: 2000 }], - 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], - 'total-blocking-time': ['error', { maxNumericValue: 300 }], - 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], - 'meta-description': 'error', - 'document-title': 'error', - 'html-has-lang': 'error', - 'image-alt': 'error', - }, - }, - ], + assertions: { + 'categories:performance': ['error', { minScore: 0.8 }], + 'categories:accessibility': ['error', { minScore: 0.9 }], + 'categories:best-practices': ['error', { minScore: 0.9 }], + 'categories:seo': ['error', { minScore: 0.9 }], + 'first-contentful-paint': ['error', { maxNumericValue: 2000 }], + 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], + 'total-blocking-time': ['error', { maxNumericValue: 300 }], + 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], + 'meta-description': 'error', + 'document-title': 'error', + 'html-has-lang': 'error', + 'image-alt': 'error', + }, }, upload: { target: 'temporary-public-storage', diff --git a/apps/guides/.lighthouserc.mobile.js b/apps/guides/.lighthouserc.mobile.js index a4fef71c47..29a24a8e9a 100644 --- a/apps/guides/.lighthouserc.mobile.js +++ b/apps/guides/.lighthouserc.mobile.js @@ -17,28 +17,20 @@ module.exports = { }, }, assert: { - assertMatrix: [ - { - matchingUrlPattern: '.*/(404|500)\\.html$', - }, - { - matchingUrlPattern: '.*\\.html$', - assertions: { - 'categories:performance': ['error', { minScore: 0.8 }], - 'categories:accessibility': ['error', { minScore: 0.9 }], - 'categories:best-practices': ['error', { minScore: 0.9 }], - 'categories:seo': ['error', { minScore: 0.9 }], - 'first-contentful-paint': ['error', { maxNumericValue: 3000 }], - 'largest-contentful-paint': ['error', { maxNumericValue: 4000 }], - 'total-blocking-time': ['error', { maxNumericValue: 600 }], - 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], - 'meta-description': 'error', - 'document-title': 'error', - 'html-has-lang': 'error', - 'image-alt': 'error', - }, - }, - ], + assertions: { + 'categories:performance': ['error', { minScore: 0.8 }], + 'categories:accessibility': ['error', { minScore: 0.9 }], + 'categories:best-practices': ['error', { minScore: 0.9 }], + 'categories:seo': ['error', { minScore: 0.9 }], + 'first-contentful-paint': ['error', { maxNumericValue: 3000 }], + 'largest-contentful-paint': ['error', { maxNumericValue: 4000 }], + 'total-blocking-time': ['error', { maxNumericValue: 600 }], + 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], + 'meta-description': 'error', + 'document-title': 'error', + 'html-has-lang': 'error', + 'image-alt': 'error', + }, }, upload: { target: 'temporary-public-storage', diff --git a/apps/guides/__tests__/layout.metadata.test.ts b/apps/guides/__tests__/layout.metadata.test.ts index 2d952f7a97..fb60efe041 100644 --- a/apps/guides/__tests__/layout.metadata.test.ts +++ b/apps/guides/__tests__/layout.metadata.test.ts @@ -4,7 +4,7 @@ import { guidesMetadata as metadata } from '../lib/metadata'; describe('guides metadata', () => { it('includes absolute Open Graph and Twitter image URLs', () => { - const expectedImageUrl = new URL('/opengraph-image.png', siteConfig.url).toString(); + const expectedImageUrl = new URL('/og-image.png', siteConfig.url).toString(); expect(metadata.openGraph?.images).toEqual([ { @@ -15,8 +15,6 @@ describe('guides metadata', () => { }, ]); - expect(metadata.twitter?.images).toEqual([ - new URL('/twitter-image.png', siteConfig.url).toString(), - ]); + expect(metadata.twitter?.images).toEqual([new URL('/og-image.png', siteConfig.url).toString()]); }); }); diff --git a/apps/guides/__tests__/og-image.test.ts b/apps/guides/__tests__/og-image.test.ts index 418ea2d9b8..b61acc3ca3 100644 --- a/apps/guides/__tests__/og-image.test.ts +++ b/apps/guides/__tests__/og-image.test.ts @@ -73,14 +73,14 @@ describe('guides layout metadata', () => { expect(images).toBeDefined(); const first = Array.isArray(images) ? images[0] : images; const url = typeof first === 'string' ? first : (first as { url: string })?.url; - expect(url).toBe('/og-image.png'); + expect(url).toMatch(/\/og-image\.png$/); }); it('includes twitter.images pointing to /og-image.png', () => { const images = (guidesMetadata.twitter as { images?: unknown })?.images; expect(images).toBeDefined(); const first = Array.isArray(images) ? images[0] : images; - expect(first).toBe('/og-image.png'); + expect(first).toMatch(/\/og-image\.png$/); }); }); diff --git a/apps/guides/app/global-error.tsx b/apps/guides/app/global-error.tsx new file mode 100644 index 0000000000..58346ad357 --- /dev/null +++ b/apps/guides/app/global-error.tsx @@ -0,0 +1,114 @@ +'use client'; + +/** + * Next.js global-error replaces the root layout when an error escapes it, + * so this component renders its own <html> and <body>. Styles are inlined + * so a failed stylesheet can't cascade into a blank page. + */ +export default function GlobalError({ + error: _error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + <html lang="en"> + <head> + <title>Something went wrong + + + + + +
+

+ 500 +

+

+ Something went wrong +

+

+ An unexpected error occurred while loading this page. You can try again, or head back to + all PackRat guides. +

+
+ + + Return to all guides + +
+
+ + + ); +} diff --git a/apps/guides/app/not-found.tsx b/apps/guides/app/not-found.tsx index acd51677b9..8dd4e5590a 100644 --- a/apps/guides/app/not-found.tsx +++ b/apps/guides/app/not-found.tsx @@ -1,15 +1,39 @@ import { Button } from '@packrat/web-ui/components/button'; +import { Compass, Home } from 'lucide-react'; +import type { Metadata } from 'next'; import Link from 'next/link'; +export const metadata: Metadata = { + title: 'Page not found', + description: + "We couldn't find the guide you were looking for. Head back to all PackRat guides or explore a different topic.", + robots: { index: false, follow: false }, +}; + export default function NotFound() { return ( -
-
-

404

-

Page not found

- +
+
+

404

+

Page not found

+

+ The guide you were looking for may have been moved, renamed, or never existed. Try heading + back to all guides or browsing by category. +

+
+ + +
); diff --git a/apps/guides/app/opengraph-image.tsx b/apps/guides/app/opengraph-image.tsx deleted file mode 100644 index 0dbd5a11ae..0000000000 --- a/apps/guides/app/opengraph-image.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { - getGuidesOgImageElement, - OG_IMAGE_CONTENT_TYPE, - OG_IMAGE_SIZE, -} from 'guides-app/lib/og-image'; -import { ImageResponse } from 'next/og'; - -export const dynamic = 'force-static'; -export const size = OG_IMAGE_SIZE; -export const contentType = OG_IMAGE_CONTENT_TYPE; - -export default function Image() { - return new ImageResponse(getGuidesOgImageElement(), { ...size }); -} diff --git a/apps/guides/app/twitter-image.tsx b/apps/guides/app/twitter-image.tsx deleted file mode 100644 index 0dbd5a11ae..0000000000 --- a/apps/guides/app/twitter-image.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { - getGuidesOgImageElement, - OG_IMAGE_CONTENT_TYPE, - OG_IMAGE_SIZE, -} from 'guides-app/lib/og-image'; -import { ImageResponse } from 'next/og'; - -export const dynamic = 'force-static'; -export const size = OG_IMAGE_SIZE; -export const contentType = OG_IMAGE_CONTENT_TYPE; - -export default function Image() { - return new ImageResponse(getGuidesOgImageElement(), { ...size }); -} diff --git a/apps/guides/lib/metadata.ts b/apps/guides/lib/metadata.ts index bd6b6e70d8..056c7f7cbb 100644 --- a/apps/guides/lib/metadata.ts +++ b/apps/guides/lib/metadata.ts @@ -29,7 +29,7 @@ export const guidesMetadata: Metadata = { description: 'Expert hiking and outdoor guides to help you prepare for your next adventure', images: [ { - url: new URL('/opengraph-image.png', siteConfig.url).toString(), + url: new URL('/og-image.png', siteConfig.url).toString(), width: 1200, height: 630, alt: 'PackRat Guides | Hiking & Outdoor Adventures', @@ -41,7 +41,7 @@ export const guidesMetadata: Metadata = { title: 'PackRat Guides | Hiking & Outdoor Adventures', description: 'Expert hiking and outdoor guides to help you prepare for your next adventure', creator: '@packratai', - images: [new URL('/twitter-image.png', siteConfig.url).toString()], + images: [new URL('/og-image.png', siteConfig.url).toString()], }, icons: { icon: [{ url: '/PackRatGuides.ico', type: 'image/x-icon' }], diff --git a/apps/guides/pages/404.tsx b/apps/guides/pages/404.tsx index 753a00d67b..b0511c34c9 100644 --- a/apps/guides/pages/404.tsx +++ b/apps/guides/pages/404.tsx @@ -3,68 +3,96 @@ // condition active in the static generation worker, which strips useContext // and causes the build to fail. See apps/landing/pages/404.tsx for context. import { Backpack } from 'lucide-react'; +import Head from 'next/head'; export default function Custom404() { return ( -
-
-
-
- + <> + + Page not found | PackRat Guides + + + +
+
+
+
+
-
-

- 404 -

-

- Guide not found -

-

- Looks like you've wandered off the trail. This guide doesn't exist. -

-
- - Back to guides - + 404 + +

+ Guide not found +

+

+ Looks like you've wandered off the trail. This guide doesn't exist or has + moved. +

+
-
-
+ + ); } diff --git a/apps/guides/pages/500.tsx b/apps/guides/pages/500.tsx index 41dfab9cf8..5b07cc5d67 100644 --- a/apps/guides/pages/500.tsx +++ b/apps/guides/pages/500.tsx @@ -1,5 +1,81 @@ // Overrides Next.js internal _error page for /500 during static export. // See pages/404.tsx for explanation. +import { AlertTriangle } from 'lucide-react'; +import Head from 'next/head'; + export default function Custom500() { - return null; + return ( + <> + + Something went wrong | PackRat Guides + + + +
+
+
+
+
+
+

+ 500 +

+

+ Something went wrong +

+

+ We hit an unexpected snag on our end. Try reloading, or head back to all guides. +

+ +
+
+ + ); } diff --git a/apps/guides/pages/_document.tsx b/apps/guides/pages/_document.tsx new file mode 100644 index 0000000000..3f64cc251e --- /dev/null +++ b/apps/guides/pages/_document.tsx @@ -0,0 +1,16 @@ +// Custom _document so the Pages Router static-exported 404/500 pages get +// `` (Lighthouse "html-has-lang" + accessibility). The App +// Router routes set this via app/layout.tsx; this only affects pages/* output. +import { Head, Html, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/apps/landing/.lighthouserc.js b/apps/landing/.lighthouserc.js index c1ccf1895d..074ffb2ee0 100644 --- a/apps/landing/.lighthouserc.js +++ b/apps/landing/.lighthouserc.js @@ -17,30 +17,20 @@ module.exports = { }, }, assert: { - assertMatrix: [ - { - // Next.js error pages (404/500) inherently lack , <html lang>, - // and meta description. They're never user-shareable; skip them. - matchingUrlPattern: '.*/(404|500)\\.html$', - }, - { - matchingUrlPattern: '.*\\.html$', - assertions: { - 'categories:performance': ['error', { minScore: 0.8 }], - 'categories:accessibility': ['error', { minScore: 0.9 }], - 'categories:best-practices': ['error', { minScore: 0.9 }], - 'categories:seo': ['error', { minScore: 0.9 }], - 'first-contentful-paint': ['error', { maxNumericValue: 2000 }], - 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], - 'total-blocking-time': ['error', { maxNumericValue: 300 }], - 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], - 'meta-description': 'error', - 'document-title': 'error', - 'html-has-lang': 'error', - 'image-alt': 'error', - }, - }, - ], + assertions: { + 'categories:performance': ['error', { minScore: 0.8 }], + 'categories:accessibility': ['error', { minScore: 0.9 }], + 'categories:best-practices': ['error', { minScore: 0.9 }], + 'categories:seo': ['error', { minScore: 0.9 }], + 'first-contentful-paint': ['error', { maxNumericValue: 2000 }], + 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], + 'total-blocking-time': ['error', { maxNumericValue: 300 }], + 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], + 'meta-description': 'error', + 'document-title': 'error', + 'html-has-lang': 'error', + 'image-alt': 'error', + }, }, upload: { target: 'temporary-public-storage', diff --git a/apps/landing/.lighthouserc.mobile.js b/apps/landing/.lighthouserc.mobile.js index a4fef71c47..29a24a8e9a 100644 --- a/apps/landing/.lighthouserc.mobile.js +++ b/apps/landing/.lighthouserc.mobile.js @@ -17,28 +17,20 @@ module.exports = { }, }, assert: { - assertMatrix: [ - { - matchingUrlPattern: '.*/(404|500)\\.html$', - }, - { - matchingUrlPattern: '.*\\.html$', - assertions: { - 'categories:performance': ['error', { minScore: 0.8 }], - 'categories:accessibility': ['error', { minScore: 0.9 }], - 'categories:best-practices': ['error', { minScore: 0.9 }], - 'categories:seo': ['error', { minScore: 0.9 }], - 'first-contentful-paint': ['error', { maxNumericValue: 3000 }], - 'largest-contentful-paint': ['error', { maxNumericValue: 4000 }], - 'total-blocking-time': ['error', { maxNumericValue: 600 }], - 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], - 'meta-description': 'error', - 'document-title': 'error', - 'html-has-lang': 'error', - 'image-alt': 'error', - }, - }, - ], + assertions: { + 'categories:performance': ['error', { minScore: 0.8 }], + 'categories:accessibility': ['error', { minScore: 0.9 }], + 'categories:best-practices': ['error', { minScore: 0.9 }], + 'categories:seo': ['error', { minScore: 0.9 }], + 'first-contentful-paint': ['error', { maxNumericValue: 3000 }], + 'largest-contentful-paint': ['error', { maxNumericValue: 4000 }], + 'total-blocking-time': ['error', { maxNumericValue: 600 }], + 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], + 'meta-description': 'error', + 'document-title': 'error', + 'html-has-lang': 'error', + 'image-alt': 'error', + }, }, upload: { target: 'temporary-public-storage', diff --git a/apps/landing/__tests__/layout.metadata.test.ts b/apps/landing/__tests__/layout.metadata.test.ts index 1c2d8078d1..aa881f716e 100644 --- a/apps/landing/__tests__/layout.metadata.test.ts +++ b/apps/landing/__tests__/layout.metadata.test.ts @@ -4,7 +4,7 @@ import { landingMetadata as metadata } from '../lib/metadata'; describe('landing metadata', () => { it('includes absolute Open Graph and Twitter image URLs', () => { - const expectedImageUrl = new URL('/opengraph-image.png', siteConfig.url).toString(); + const expectedImageUrl = new URL('/og-image.png', siteConfig.url).toString(); expect(metadata.openGraph?.images).toEqual([ { @@ -15,8 +15,6 @@ describe('landing metadata', () => { }, ]); - expect(metadata.twitter?.images).toEqual([ - new URL('/twitter-image.png', siteConfig.url).toString(), - ]); + expect(metadata.twitter?.images).toEqual([new URL('/og-image.png', siteConfig.url).toString()]); }); }); diff --git a/apps/landing/__tests__/og-image.test.ts b/apps/landing/__tests__/og-image.test.ts index 1d595a1552..7646272ed1 100644 --- a/apps/landing/__tests__/og-image.test.ts +++ b/apps/landing/__tests__/og-image.test.ts @@ -50,13 +50,13 @@ describe('landing metadata', () => { expect(images).toBeDefined(); const first = Array.isArray(images) ? images[0] : images; const url = typeof first === 'string' ? first : (first as { url: string })?.url; - expect(url).toBe('/og-image.png'); + expect(url).toMatch(/\/og-image\.png$/); }); it('includes twitter.images pointing to /og-image.png', () => { const images = (landingMetadata.twitter as { images?: unknown })?.images; expect(images).toBeDefined(); const first = Array.isArray(images) ? images[0] : images; - expect(first).toBe('/og-image.png'); + expect(first).toMatch(/\/og-image\.png$/); }); }); diff --git a/apps/landing/app/global-error.tsx b/apps/landing/app/global-error.tsx new file mode 100644 index 0000000000..7c0e7c8d53 --- /dev/null +++ b/apps/landing/app/global-error.tsx @@ -0,0 +1,114 @@ +'use client'; + +/** + * Next.js global-error replaces the root layout when an error escapes it, + * so this component renders its own <html> and <body>. Styles are inlined + * so a failed stylesheet can't cascade into a blank page. + */ +export default function GlobalError({ + error: _error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + <html lang="en"> + <head> + <title>Something went wrong + + + + + +
+

+ 500 +

+

+ Something went wrong +

+

+ An unexpected error occurred while loading this page. You can try again, or head back to + the PackRat home page. +

+
+ + + Back to home + +
+
+ + + ); +} diff --git a/apps/landing/app/not-found.tsx b/apps/landing/app/not-found.tsx index acd51677b9..de0c5bf5f4 100644 --- a/apps/landing/app/not-found.tsx +++ b/apps/landing/app/not-found.tsx @@ -1,16 +1,39 @@ import { Button } from '@packrat/web-ui/components/button'; +import { Compass, Home } from 'lucide-react'; +import type { Metadata } from 'next'; import Link from 'next/link'; +export const metadata: Metadata = { + title: 'Page not found', + description: + "We couldn't find that page on PackRat. Head back home or explore what PackRat can do for your next outdoor adventure.", + robots: { index: false, follow: false }, +}; + export default function NotFound() { return ( -
-
-

404

-

Page not found

- +
+
+

404

+

Page not found

+

+ The page you're looking for doesn't exist or has been moved. Let's get you back on trail. +

+
+ + +
-
+
); } diff --git a/apps/landing/app/opengraph-image.tsx b/apps/landing/app/opengraph-image.tsx deleted file mode 100644 index e79b53bca3..0000000000 --- a/apps/landing/app/opengraph-image.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { - getLandingOgImageElement, - OG_IMAGE_CONTENT_TYPE, - OG_IMAGE_SIZE, -} from 'landing-app/lib/og-image'; -import { ImageResponse } from 'next/og'; - -export const dynamic = 'force-static'; -export const size = OG_IMAGE_SIZE; -export const contentType = OG_IMAGE_CONTENT_TYPE; - -export default function Image() { - return new ImageResponse(getLandingOgImageElement(), { ...size }); -} diff --git a/apps/landing/app/twitter-image.tsx b/apps/landing/app/twitter-image.tsx deleted file mode 100644 index e79b53bca3..0000000000 --- a/apps/landing/app/twitter-image.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { - getLandingOgImageElement, - OG_IMAGE_CONTENT_TYPE, - OG_IMAGE_SIZE, -} from 'landing-app/lib/og-image'; -import { ImageResponse } from 'next/og'; - -export const dynamic = 'force-static'; -export const size = OG_IMAGE_SIZE; -export const contentType = OG_IMAGE_CONTENT_TYPE; - -export default function Image() { - return new ImageResponse(getLandingOgImageElement(), { ...size }); -} diff --git a/apps/landing/lib/metadata.ts b/apps/landing/lib/metadata.ts index 35aa479a04..ee78793db0 100644 --- a/apps/landing/lib/metadata.ts +++ b/apps/landing/lib/metadata.ts @@ -20,7 +20,7 @@ export const landingMetadata: Metadata = { siteName: siteConfig.name, images: [ { - url: new URL('/opengraph-image.png', siteConfig.url).toString(), + url: new URL('/og-image.png', siteConfig.url).toString(), width: 1200, height: 630, alt: siteConfig.name, @@ -32,7 +32,7 @@ export const landingMetadata: Metadata = { title: siteConfig.name, description: siteConfig.description, creator: siteConfig.twitterHandle, - images: [new URL('/twitter-image.png', siteConfig.url).toString()], + images: [new URL('/og-image.png', siteConfig.url).toString()], }, icons: { icon: '/PackRat.ico', diff --git a/apps/landing/pages/404.tsx b/apps/landing/pages/404.tsx index 0401c87d33..7abe55ed80 100644 --- a/apps/landing/pages/404.tsx +++ b/apps/landing/pages/404.tsx @@ -1,80 +1,93 @@ import { Backpack } from 'lucide-react'; +import Head from 'next/head'; export default function Custom404() { return ( -
-
-
-
- + <> + + Page not found | PackRat + + + +
+
+
+
+
-
-

- 404 -

-

- Trail not found -

-

- Looks like you've wandered off the map. This page doesn't exist. -

-
- - Back to home - - - Browse FAQ - + 404 + +

+ Trail not found +

+

+ Looks like you've wandered off the map. This page doesn't exist or has moved. +

+
-
-
+ + ); } diff --git a/apps/landing/pages/500.tsx b/apps/landing/pages/500.tsx index 27469b79fa..f31fca3da0 100644 --- a/apps/landing/pages/500.tsx +++ b/apps/landing/pages/500.tsx @@ -1,80 +1,93 @@ import { AlertTriangle } from 'lucide-react'; +import Head from 'next/head'; export default function Custom500() { return ( -
-
-
-
- + <> + + Something went wrong | PackRat + + + +
+
+
+
+
-
-

- 500 -

-

- Something went wrong -

-

- We hit an unexpected snag on our end. Try again in a moment. -

-
- - Back to home - - - Contact support - + 500 + +

+ Something went wrong +

+

+ We hit an unexpected snag on our end. Try again in a moment. +

+
-
-
+ + ); } diff --git a/apps/landing/pages/_document.tsx b/apps/landing/pages/_document.tsx new file mode 100644 index 0000000000..3f64cc251e --- /dev/null +++ b/apps/landing/pages/_document.tsx @@ -0,0 +1,16 @@ +// Custom _document so the Pages Router static-exported 404/500 pages get +// `` (Lighthouse "html-has-lang" + accessibility). The App +// Router routes set this via app/layout.tsx; this only affects pages/* output. +import { Head, Html, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +}