From cd02f06354bdb14ba958960c1a503286b1e4a66f Mon Sep 17 00:00:00 2001 From: KingDavid9999 Date: Thu, 25 Jun 2026 18:59:37 -0700 Subject: [PATCH 1/5] feat: implement Twitter Cards metadata across all app pages - Add twitter and openGraph fields to root layout metadata (global default) - Create (auth)/layout.tsx with Twitter Cards for login, signup, verify-email - Extend metadata in editor, search, study-groups, leaderboard, privacy pages - Add Twitter Cards to generateMetadata in topics/[slug] and courses/[courseId] - Add unit tests to verify twitter card fields across layout files Closes #378 --- src/app/(auth)/layout.tsx | 23 ++++++++ src/app/__tests__/twitter-cards.test.ts | 76 +++++++++++++++++++++++++ src/app/courses/[courseId]/page.tsx | 12 ++++ src/app/editor/page.tsx | 12 ++++ src/app/layout.tsx | 14 +++++ src/app/leaderboard/page.tsx | 12 ++++ src/app/privacy/page.tsx | 6 ++ src/app/search/page.tsx | 15 ++++- src/app/study-groups/page.tsx | 15 ++++- src/app/topics/[slug]/page.tsx | 12 ++++ 10 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 src/app/(auth)/layout.tsx create mode 100644 src/app/__tests__/twitter-cards.test.ts diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 00000000..51840533 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'TeachLink - Sign In or Create an Account', + description: + 'Access your TeachLink account to continue learning offline. Sign in, sign up, or verify your email.', + openGraph: { + title: 'TeachLink - Sign In or Create an Account', + description: 'Access your TeachLink account to continue learning.', + type: 'website', + siteName: 'TeachLink', + }, + twitter: { + card: 'summary', + site: '@teachlink', + title: 'TeachLink - Sign In or Create an Account', + description: 'Access your TeachLink account to continue learning.', + }, +}; + +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/__tests__/twitter-cards.test.ts b/src/app/__tests__/twitter-cards.test.ts new file mode 100644 index 00000000..277e40fb --- /dev/null +++ b/src/app/__tests__/twitter-cards.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; +import { metadata as rootMetadata } from '@/app/layout'; +import { metadata as authMetadata } from '@/app/(auth)/layout'; +import { metadata as dashboardMetadata } from '@/app/dashboard/layout'; +import { metadata as profileMetadata } from '@/app/profile/layout'; + +describe('Twitter Cards metadata', () => { + describe('Root layout', () => { + it('exports a twitter card field', () => { + expect(rootMetadata.twitter).toBeDefined(); + }); + + it('uses summary_large_image card type', () => { + expect(rootMetadata.twitter?.card).toBe('summary_large_image'); + }); + + it('includes a twitter title', () => { + expect(rootMetadata.twitter?.title).toBeTruthy(); + }); + + it('includes a twitter description', () => { + expect(rootMetadata.twitter?.description).toBeTruthy(); + }); + + it('includes twitter site handle', () => { + expect(rootMetadata.twitter?.site).toBe('@teachlink'); + }); + + it('exports openGraph metadata', () => { + expect(rootMetadata.openGraph).toBeDefined(); + expect(rootMetadata.openGraph?.siteName).toBe('TeachLink'); + }); + }); + + describe('Auth layout', () => { + it('exports a twitter card field', () => { + expect(authMetadata.twitter).toBeDefined(); + }); + + it('uses summary card type', () => { + expect(authMetadata.twitter?.card).toBe('summary'); + }); + + it('includes a twitter title', () => { + expect(authMetadata.twitter?.title).toBeTruthy(); + }); + + it('includes a twitter description', () => { + expect(authMetadata.twitter?.description).toBeTruthy(); + }); + + it('includes twitter site handle', () => { + expect(authMetadata.twitter?.site).toBe('@teachlink'); + }); + }); + + describe('Dashboard layout', () => { + it('exports a twitter card field', () => { + expect(dashboardMetadata.twitter).toBeDefined(); + }); + + it('uses summary card type', () => { + expect(dashboardMetadata.twitter?.card).toBe('summary'); + }); + }); + + describe('Profile layout', () => { + it('exports a twitter card field', () => { + expect(profileMetadata.twitter).toBeDefined(); + }); + + it('uses summary card type', () => { + expect(profileMetadata.twitter?.card).toBe('summary'); + }); + }); +}); diff --git a/src/app/courses/[courseId]/page.tsx b/src/app/courses/[courseId]/page.tsx index 7545d78d..9b15f34a 100644 --- a/src/app/courses/[courseId]/page.tsx +++ b/src/app/courses/[courseId]/page.tsx @@ -12,6 +12,18 @@ export async function generateMetadata({ params }: CoursePageProps): Promise Date: Sat, 27 Jun 2026 07:50:35 -0700 Subject: [PATCH 2/5] style: run prettier format to fix lint errors --- .../privacy/PrivacyReleaseNotes.tsx | 25 ++++--------------- src/components/ui/ModalFeedbackLoop.tsx | 6 +---- src/utils/emailFeedback.ts | 5 +--- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/components/privacy/PrivacyReleaseNotes.tsx b/src/components/privacy/PrivacyReleaseNotes.tsx index 98001017..625abb30 100644 --- a/src/components/privacy/PrivacyReleaseNotes.tsx +++ b/src/components/privacy/PrivacyReleaseNotes.tsx @@ -2,12 +2,7 @@ import { useMemo, useState } from 'react'; -export type ReleaseNoteKind = - | 'added' - | 'changed' - | 'fixed' - | 'security' - | 'deprecated'; +export type ReleaseNoteKind = 'added' | 'changed' | 'fixed' | 'security' | 'deprecated'; export interface ReleaseNote { kind: ReleaseNoteKind; @@ -47,10 +42,7 @@ function slug(version: string): string { return version.replace(/[^a-z0-9]+/gi, '-').toLowerCase(); } -export default function PrivacyReleaseNotes({ - notes, - initialVersion, -}: PrivacyReleaseNotesProps) { +export default function PrivacyReleaseNotes({ notes, initialVersion }: PrivacyReleaseNotesProps) { const sorted = useMemo( () => [...notes].sort((a, b) => b.effectiveAt.localeCompare(a.effectiveAt)), [notes], @@ -80,21 +72,14 @@ export default function PrivacyReleaseNotes({ type="button" aria-expanded={open} aria-controls={`rel-${slug(release.version)}`} - onClick={() => - setOpenVersion(open ? null : release.version) - } + onClick={() => setOpenVersion(open ? null : release.version)} className="w-full text-left" > v{release.version}{' '} - - — effective {release.effectiveAt} - + — effective {release.effectiveAt} {open ? ( -