Refactor metadata handling for improved maintainability and localization support

- Extracted server-only metadata generation logic into separate files, reducing inline logic in page components.
- Added `/* istanbul ignore file */` annotations for E2E-covered framework-level metadata.
- Standardized `generateMetadata` export patterns across auth, admin, and error pages for consistency.
- Enhanced maintainability and readability by centralizing metadata definitions for each route.
This commit is contained in:
Felipe Cardoso
2025-11-20 10:07:15 +01:00
parent a943f79ce7
commit 444d495f83
23 changed files with 138 additions and 113 deletions

View File

@@ -0,0 +1,15 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.login' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/login');
}

View File

@@ -1,7 +1,4 @@
import { Metadata } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
import { getTranslations } from 'next-intl/server';
// Code-split LoginForm - heavy with react-hook-form + validation // Code-split LoginForm - heavy with react-hook-form + validation
const LoginForm = dynamic( const LoginForm = dynamic(
@@ -18,17 +15,8 @@ const LoginForm = dynamic(
} }
); );
/* istanbul ignore next - Next.js metadata generation covered by e2e tests */ // Re-export server-only metadata from separate, ignored file
export async function generateMetadata({ export { generateMetadata } from './metadata';
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.login' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/login');
}
export default function LoginPage() { export default function LoginPage() {
return ( return (

View File

@@ -0,0 +1,20 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.passwordResetConfirm' });
return generatePageMetadata(
locale as Locale,
t('title'),
t('instructions'),
'/password-reset/confirm'
);
}

View File

@@ -3,27 +3,11 @@
* Users set a new password using the token from their email * Users set a new password using the token from their email
*/ */
import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
import { getTranslations } from 'next-intl/server';
import PasswordResetConfirmContent from './PasswordResetConfirmContent'; import PasswordResetConfirmContent from './PasswordResetConfirmContent';
export async function generateMetadata({ // Re-export server-only metadata from separate, ignored file
params, export { generateMetadata } from './metadata';
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.passwordResetConfirm' });
return generatePageMetadata(
locale as Locale,
t('title'),
t('instructions'),
'/password-reset/confirm'
);
}
export default function PasswordResetConfirmPage() { export default function PasswordResetConfirmPage() {
return ( return (

View File

@@ -0,0 +1,15 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.passwordReset' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/password-reset');
}

View File

@@ -3,10 +3,7 @@
* Users enter their email to receive reset instructions * Users enter their email to receive reset instructions
*/ */
import { Metadata } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
import { getTranslations } from 'next-intl/server';
// Code-split PasswordResetRequestForm // Code-split PasswordResetRequestForm
const PasswordResetRequestForm = dynamic( const PasswordResetRequestForm = dynamic(
@@ -25,17 +22,8 @@ const PasswordResetRequestForm = dynamic(
} }
); );
/* istanbul ignore next - Next.js metadata generation covered by e2e tests */ // Re-export server-only metadata from separate, ignored file
export async function generateMetadata({ export { generateMetadata } from './metadata';
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.passwordReset' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/password-reset');
}
export default function PasswordResetPage() { export default function PasswordResetPage() {
return ( return (

View File

@@ -0,0 +1,15 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.register' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/register');
}

View File

@@ -1,7 +1,4 @@
import { Metadata } from 'next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
import { getTranslations } from 'next-intl/server';
// Code-split RegisterForm (313 lines) // Code-split RegisterForm (313 lines)
const RegisterForm = dynamic( const RegisterForm = dynamic(
@@ -18,17 +15,8 @@ const RegisterForm = dynamic(
} }
); );
/* istanbul ignore next - Next.js metadata generation covered by e2e tests */ // Re-export server-only metadata from separate, ignored file
export async function generateMetadata({ export { generateMetadata } from './metadata';
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'auth.register' });
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/register');
}
export default function RegisterPage() { export default function RegisterPage() {
return ( return (

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Admin Dashboard',
};

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Organization Members',
};

View File

@@ -4,17 +4,13 @@
* Protected by AuthGuard in layout with requireAdmin=true * Protected by AuthGuard in layout with requireAdmin=true
*/ */
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { OrganizationMembersContent } from '@/components/admin/organizations/OrganizationMembersContent'; import { OrganizationMembersContent } from '@/components/admin/organizations/OrganizationMembersContent';
/* istanbul ignore next - Next.js metadata, not executable code */ // Re-export server-only metadata from separate, ignored file
export const metadata: Metadata = { export { metadata } from './metadata';
title: 'Organization Members',
};
interface PageProps { interface PageProps {
params: Promise<{ params: Promise<{

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Organizations',
};

View File

@@ -4,17 +4,13 @@
* Protected by AuthGuard in layout with requireAdmin=true * Protected by AuthGuard in layout with requireAdmin=true
*/ */
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { OrganizationManagementContent } from '@/components/admin/organizations/OrganizationManagementContent'; import { OrganizationManagementContent } from '@/components/admin/organizations/OrganizationManagementContent';
/* istanbul ignore next - Next.js metadata, not executable code */ // Re-export server-only metadata from separate, ignored file
export const metadata: Metadata = { export { metadata } from './metadata';
title: 'Organizations',
};
export default function AdminOrganizationsPage() { export default function AdminOrganizationsPage() {
return ( return (

View File

@@ -4,8 +4,6 @@
* Protected by AuthGuard in layout with requireAdmin=true * Protected by AuthGuard in layout with requireAdmin=true
*/ */
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { DashboardStats } from '@/components/admin'; import { DashboardStats } from '@/components/admin';
import { import {
@@ -16,10 +14,8 @@ import {
} from '@/components/charts'; } from '@/components/charts';
import { Users, Building2, Settings } from 'lucide-react'; import { Users, Building2, Settings } from 'lucide-react';
/* istanbul ignore next - Next.js metadata, not executable code */ // Re-export server-only metadata from separate, ignored file
export const metadata: Metadata = { export { metadata } from './metadata';
title: 'Admin Dashboard',
};
export default function AdminPage() { export default function AdminPage() {
return ( return (

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'System Settings',
};

View File

@@ -4,16 +4,12 @@
* Protected by AuthGuard in layout with requireAdmin=true * Protected by AuthGuard in layout with requireAdmin=true
*/ */
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
/* istanbul ignore next - Next.js metadata, not executable code */ // Re-export server-only metadata from separate, ignored file
export const metadata: Metadata = { export { metadata } from './metadata';
title: 'System Settings',
};
export default function AdminSettingsPage() { export default function AdminSettingsPage() {
return ( return (

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'User Management',
};

View File

@@ -4,17 +4,13 @@
* Protected by AuthGuard in layout with requireAdmin=true * Protected by AuthGuard in layout with requireAdmin=true
*/ */
/* istanbul ignore next - Next.js type import for metadata */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { UserManagementContent } from '@/components/admin/users/UserManagementContent'; import { UserManagementContent } from '@/components/admin/users/UserManagementContent';
/* istanbul ignore next - Next.js metadata, not executable code */ // Re-export server-only metadata from separate, ignored file
export const metadata: Metadata = { export { metadata } from './metadata';
title: 'User Management',
};
export default function AdminUsersPage() { export default function AdminUsersPage() {
return ( return (

View File

@@ -0,0 +1,20 @@
/* istanbul ignore file - Server-only Next.js metadata generation covered by E2E */
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'errors' });
return generatePageMetadata(
locale as Locale,
t('unauthorized'),
t('unauthorizedDescription'),
'/forbidden'
);
}

View File

@@ -3,29 +3,11 @@
* Displayed when users try to access resources they don't have permission for * Displayed when users try to access resources they don't have permission for
*/ */
import type { Metadata } from 'next';
import { Link } from '@/lib/i18n/routing'; import { Link } from '@/lib/i18n/routing';
import { ShieldAlert } from 'lucide-react'; import { ShieldAlert } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata'; // Re-export server-only metadata from separate, ignored file
import { getTranslations } from 'next-intl/server'; export { generateMetadata } from './metadata';
/* istanbul ignore next - Next.js metadata generation covered by e2e tests */
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'errors' });
return generatePageMetadata(
locale as Locale,
t('unauthorized'),
t('unauthorizedDescription'),
'/forbidden'
);
}
export default function ForbiddenPage() { export default function ForbiddenPage() {
return ( return (

View File

@@ -1,10 +1,10 @@
/* istanbul ignore file - Framework-only root redirect covered by E2E */
/** /**
* Root page - redirects to default locale * Root page - redirects to default locale
*/ */
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
/* istanbul ignore next - Next.js server-side redirect covered by e2e tests */
export default function RootPage() { export default function RootPage() {
// Redirect to default locale (en) // Redirect to default locale (en)
redirect('/en'); redirect('/en');

View File

@@ -1,10 +1,10 @@
/* istanbul ignore file - Framework-only metadata route covered by E2E */
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
/** /**
* Generate robots.txt * Generate robots.txt
* Configures search engine crawler behavior * Configures search engine crawler behavior
*/ */
/* istanbul ignore next - Next.js metadata route covered by e2e tests */
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';

View File

@@ -1,3 +1,4 @@
/* istanbul ignore file - Framework-only metadata route covered by E2E */
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
import { routing } from '@/lib/i18n/routing'; import { routing } from '@/lib/i18n/routing';
@@ -5,7 +6,6 @@ import { routing } from '@/lib/i18n/routing';
* Generate multilingual sitemap * Generate multilingual sitemap
* Includes all public routes for each supported locale * Includes all public routes for each supported locale
*/ */
/* istanbul ignore next - Next.js metadata route covered by e2e tests */
export default function sitemap(): MetadataRoute.Sitemap { export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';