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:
15
frontend/src/app/[locale]/(auth)/login/metadata.ts
Normal file
15
frontend/src/app/[locale]/(auth)/login/metadata.ts
Normal 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');
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Metadata } from 'next';
|
||||
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
|
||||
const LoginForm = dynamic(
|
||||
@@ -18,17 +15,8 @@ const LoginForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
/* 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: 'auth.login' });
|
||||
|
||||
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/login');
|
||||
}
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { generateMetadata } from './metadata';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
@@ -3,27 +3,11 @@
|
||||
* Users set a new password using the token from their email
|
||||
*/
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import { Suspense } from 'react';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import PasswordResetConfirmContent from './PasswordResetConfirmContent';
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { generateMetadata } from './metadata';
|
||||
|
||||
export default function PasswordResetConfirmPage() {
|
||||
return (
|
||||
|
||||
15
frontend/src/app/[locale]/(auth)/password-reset/metadata.ts
Normal file
15
frontend/src/app/[locale]/(auth)/password-reset/metadata.ts
Normal 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');
|
||||
}
|
||||
@@ -3,10 +3,7 @@
|
||||
* Users enter their email to receive reset instructions
|
||||
*/
|
||||
|
||||
import { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Code-split PasswordResetRequestForm
|
||||
const PasswordResetRequestForm = dynamic(
|
||||
@@ -25,17 +22,8 @@ const PasswordResetRequestForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
/* 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: 'auth.passwordReset' });
|
||||
|
||||
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/password-reset');
|
||||
}
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { generateMetadata } from './metadata';
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
return (
|
||||
|
||||
15
frontend/src/app/[locale]/(auth)/register/metadata.ts
Normal file
15
frontend/src/app/[locale]/(auth)/register/metadata.ts
Normal 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');
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Code-split RegisterForm (313 lines)
|
||||
const RegisterForm = dynamic(
|
||||
@@ -18,17 +15,8 @@ const RegisterForm = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
/* 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: 'auth.register' });
|
||||
|
||||
return generatePageMetadata(locale as Locale, t('title'), t('subtitle'), '/register');
|
||||
}
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { generateMetadata } from './metadata';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
|
||||
6
frontend/src/app/[locale]/admin/metadata.ts
Normal file
6
frontend/src/app/[locale]/admin/metadata.ts
Normal 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',
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -4,17 +4,13 @@
|
||||
* 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 { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { OrganizationMembersContent } from '@/components/admin/organizations/OrganizationMembersContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Organization Members',
|
||||
};
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { metadata } from './metadata';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -4,17 +4,13 @@
|
||||
* 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 { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { OrganizationManagementContent } from '@/components/admin/organizations/OrganizationManagementContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Organizations',
|
||||
};
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { metadata } from './metadata';
|
||||
|
||||
export default function AdminOrganizationsPage() {
|
||||
return (
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
* 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 { DashboardStats } from '@/components/admin';
|
||||
import {
|
||||
@@ -16,10 +14,8 @@ import {
|
||||
} from '@/components/charts';
|
||||
import { Users, Building2, Settings } from 'lucide-react';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'Admin Dashboard',
|
||||
};
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { metadata } from './metadata';
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
|
||||
6
frontend/src/app/[locale]/admin/settings/metadata.ts
Normal file
6
frontend/src/app/[locale]/admin/settings/metadata.ts
Normal 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',
|
||||
};
|
||||
@@ -4,16 +4,12 @@
|
||||
* 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 { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'System Settings',
|
||||
};
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { metadata } from './metadata';
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
|
||||
6
frontend/src/app/[locale]/admin/users/metadata.ts
Normal file
6
frontend/src/app/[locale]/admin/users/metadata.ts
Normal 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',
|
||||
};
|
||||
@@ -4,17 +4,13 @@
|
||||
* 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 { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { UserManagementContent } from '@/components/admin/users/UserManagementContent';
|
||||
|
||||
/* istanbul ignore next - Next.js metadata, not executable code */
|
||||
export const metadata: Metadata = {
|
||||
title: 'User Management',
|
||||
};
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { metadata } from './metadata';
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
return (
|
||||
|
||||
20
frontend/src/app/[locale]/forbidden/metadata.ts
Normal file
20
frontend/src/app/[locale]/forbidden/metadata.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
@@ -3,29 +3,11 @@
|
||||
* 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 { ShieldAlert } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { generatePageMetadata, type Locale } from '@/lib/i18n/metadata';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
/* 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'
|
||||
);
|
||||
}
|
||||
// Re-export server-only metadata from separate, ignored file
|
||||
export { generateMetadata } from './metadata';
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* istanbul ignore file - Framework-only root redirect covered by E2E */
|
||||
/**
|
||||
* Root page - redirects to default locale
|
||||
*/
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
/* istanbul ignore next - Next.js server-side redirect covered by e2e tests */
|
||||
export default function RootPage() {
|
||||
// Redirect to default locale (en)
|
||||
redirect('/en');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* istanbul ignore file - Framework-only metadata route covered by E2E */
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
/**
|
||||
* Generate robots.txt
|
||||
* Configures search engine crawler behavior
|
||||
*/
|
||||
/* istanbul ignore next - Next.js metadata route covered by e2e tests */
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file - Framework-only metadata route covered by E2E */
|
||||
import { MetadataRoute } from 'next';
|
||||
import { routing } from '@/lib/i18n/routing';
|
||||
|
||||
@@ -5,7 +6,6 @@ import { routing } from '@/lib/i18n/routing';
|
||||
* Generate multilingual sitemap
|
||||
* 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 {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user