diff --git a/.env.example b/.env.example index 0ad035c..4254e69 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ -# Optional in local development (falls back to http://localhost:3000) -# Set this in production to your canonical app URL, for example: -# NEXT_PUBLIC_SITE_URL=https://teacherbuddy.example.com +# Optional in local development. +# Set this in production to your canonical app URL: +# NEXT_PUBLIC_SITE_URL= diff --git a/documentation/project-docs/metadata-and-seo.md b/documentation/project-docs/metadata-and-seo.md index 1b959c8..347531f 100644 --- a/documentation/project-docs/metadata-and-seo.md +++ b/documentation/project-docs/metadata-and-seo.md @@ -32,6 +32,7 @@ TeacherBuddy uses a static OG image asset: - Declared dimensions: `895x372` The shared OG and Twitter image path is defined once in `lib/metadata.ts`. +Social image tags are emitted as absolute URLs derived from `metadataBase` so crawlers can fetch them reliably. ## Environment Variables diff --git a/lib/__tests__/metadata.test.ts b/lib/__tests__/metadata.test.ts index 4410547..b0dc02f 100644 --- a/lib/__tests__/metadata.test.ts +++ b/lib/__tests__/metadata.test.ts @@ -48,8 +48,10 @@ describe('resolveMetadataBase', () => { ); }); - it('falls back to localhost when no env URL values exist', () => { - expect(resolveMetadataBase().toString()).toBe('http://localhost:3000/'); + it('falls back to production URL when no env URL values exist', () => { + expect(resolveMetadataBase().toString()).toBe( + 'https://teacherbuddy.mrbubbles-src.dev/', + ); }); }); @@ -66,7 +68,7 @@ describe('buildPageMetadata', () => { ...SHARED_OPEN_GRAPH, title: 'Student Management', description: 'Add students, mark absences, and manage your roster.', - url: '/students', + url: 'https://teacherbuddy.mrbubbles-src.dev/students', }); expect(metadata.twitter).toMatchObject({ ...SHARED_TWITTER, @@ -85,7 +87,7 @@ describe('buildPageMetadata', () => { ...SHARED_OPEN_GRAPH, title: 'TeacherBuddy', description: DEFAULT_SITE_DESCRIPTION, - url: '/missing', + url: 'https://teacherbuddy.mrbubbles-src.dev/missing', }); expect(metadata.twitter).toMatchObject({ ...SHARED_TWITTER, diff --git a/lib/metadata.ts b/lib/metadata.ts index 7aa07df..e09f321 100644 --- a/lib/metadata.ts +++ b/lib/metadata.ts @@ -13,8 +13,9 @@ export const SITE_NAME = 'TeacherBuddy'; export const DEFAULT_SITE_DESCRIPTION = 'Manage students, build quizzes, and draw random pairs without repeats.'; -const DEFAULT_SITE_URL = 'http://localhost:3000'; -const DEFAULT_OG_IMAGE_PATH = '/images/og.png'; +const PRODUCTION_SITE_URL = 'https://teacherbuddy.mrbubbles-src.dev'; +const DEFAULT_SITE_URL = PRODUCTION_SITE_URL; +const DEFAULT_OG_IMAGE_PATH = '/api/og'; const URL_PROTOCOL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//; /** @@ -26,10 +27,10 @@ export const SHARED_OPEN_GRAPH: NonNullable = { locale: 'en_US', images: [ { - url: DEFAULT_OG_IMAGE_PATH, - width: 895, - height: 372, - alt: 'TeacherBuddy logo', + url: buildAbsoluteMetadataUrl(DEFAULT_OG_IMAGE_PATH), + width: 1200, + height: 630, + alt: 'TeacherBuddy Logo', }, ], }; @@ -39,7 +40,7 @@ export const SHARED_OPEN_GRAPH: NonNullable = { */ export const SHARED_TWITTER: NonNullable = { card: 'summary_large_image', - images: [DEFAULT_OG_IMAGE_PATH], + images: [buildAbsoluteMetadataUrl(DEFAULT_OG_IMAGE_PATH)], }; /** @@ -71,6 +72,7 @@ export function buildPageMetadata(path: string): Metadata { const title = routeMeta?.title ?? SITE_NAME; const description = routeMeta?.description ?? DEFAULT_SITE_DESCRIPTION; const canonicalPath = resolveCanonicalPath(path); + const absoluteCanonicalUrl = buildAbsoluteMetadataUrl(canonicalPath); return { title, @@ -82,12 +84,13 @@ export function buildPageMetadata(path: string): Metadata { ...SHARED_OPEN_GRAPH, title, description, - url: canonicalPath, + url: absoluteCanonicalUrl, }, twitter: { ...SHARED_TWITTER, title, description, + creator: '@_MstrBubbles', }, }; } @@ -133,3 +136,10 @@ function normalizeEnvUrl( return null; } } + +/** + * Resolves an absolute metadata URL from a path using the active site base. + */ +function buildAbsoluteMetadataUrl(path: string): string { + return new URL(path, resolveMetadataBase()).toString(); +} diff --git a/lib/og-image.tsx b/lib/og-image.tsx index d28608c..91a4629 100644 --- a/lib/og-image.tsx +++ b/lib/og-image.tsx @@ -1,9 +1,11 @@ +/* eslint-disable @next/next/no-img-element */ + import { ImageResponse } from 'next/og'; /** * Accessible alt text used for Open Graph image metadata. */ -export const OG_IMAGE_ALT = 'TeacherBuddy classroom dashboard preview'; +export const OG_IMAGE_ALT = 'TeacherBuddy Logo'; /** * Pixel dimensions used for the generated Open Graph image. @@ -16,102 +18,31 @@ export const OG_IMAGE_SIZE = { /** * MIME type returned by the Open Graph image route. */ -export const OG_IMAGE_CONTENT_TYPE = 'image/png'; +export const OG_IMAGE_CONTENT_TYPE = 'image/svg+xml'; /** * Generates the shared TeacherBuddy Open Graph image response. */ export function createOpenGraphImageResponse(): ImageResponse { + const imageUrl = 'https://teacherbuddy.mrbubbles-src.dev/logo-og.svg'; + return new ImageResponse(
-
-
-
- TeacherBuddy -
- -
-
- Classroom Tools -
-
- Manage students, run quizzes, and organize class activities in one - place. -
-
-
- -
- TeacherBuddy -
+ {OG_IMAGE_ALT}
, { ...OG_IMAGE_SIZE, diff --git a/public/logo-og.png b/public/logo-og.png new file mode 100644 index 0000000..1556a6d Binary files /dev/null and b/public/logo-og.png differ diff --git a/public/logo-og.svg b/public/logo-og.svg new file mode 100644 index 0000000..85716ee --- /dev/null +++ b/public/logo-og.svg @@ -0,0 +1,8 @@ + + + + + + + +