@djangocfg/nextjs 2.1.110 → 2.1.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +208 -7
  2. package/dist/config/index.d.mts +16 -1
  3. package/dist/config/index.mjs +83 -14
  4. package/dist/config/index.mjs.map +1 -1
  5. package/dist/i18n/client.d.mts +123 -0
  6. package/dist/i18n/client.mjs +104 -0
  7. package/dist/i18n/client.mjs.map +1 -0
  8. package/dist/i18n/components.d.mts +11 -0
  9. package/dist/i18n/components.mjs +90 -0
  10. package/dist/i18n/components.mjs.map +1 -0
  11. package/dist/i18n/index.d.mts +18 -0
  12. package/dist/i18n/index.mjs +226 -0
  13. package/dist/i18n/index.mjs.map +1 -0
  14. package/dist/i18n/navigation.d.mts +1095 -0
  15. package/dist/i18n/navigation.mjs +45 -0
  16. package/dist/i18n/navigation.mjs.map +1 -0
  17. package/dist/i18n/plugin.d.mts +41 -0
  18. package/dist/i18n/plugin.mjs +17 -0
  19. package/dist/i18n/plugin.mjs.map +1 -0
  20. package/dist/i18n/provider.d.mts +18 -0
  21. package/dist/i18n/provider.mjs +54 -0
  22. package/dist/i18n/provider.mjs.map +1 -0
  23. package/dist/i18n/proxy.d.mts +40 -0
  24. package/dist/i18n/proxy.mjs +42 -0
  25. package/dist/i18n/proxy.mjs.map +1 -0
  26. package/dist/i18n/request.d.mts +42 -0
  27. package/dist/i18n/request.mjs +63 -0
  28. package/dist/i18n/request.mjs.map +1 -0
  29. package/dist/i18n/routing.d.mts +79 -0
  30. package/dist/i18n/routing.mjs +33 -0
  31. package/dist/i18n/routing.mjs.map +1 -0
  32. package/dist/i18n/server.d.mts +90 -0
  33. package/dist/i18n/server.mjs +79 -0
  34. package/dist/i18n/server.mjs.map +1 -0
  35. package/dist/index.d.mts +3 -1
  36. package/dist/index.mjs +176 -30
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/sitemap/index.d.mts +22 -3
  39. package/dist/sitemap/index.mjs +92 -15
  40. package/dist/sitemap/index.mjs.map +1 -1
  41. package/dist/types-Cy349X20.d.mts +60 -0
  42. package/package.json +54 -4
  43. package/src/config/constants.ts +1 -0
  44. package/src/config/createNextConfig.ts +39 -17
  45. package/src/i18n/client.ts +221 -0
  46. package/src/i18n/components/LocaleSwitcher.tsx +60 -0
  47. package/src/i18n/components/index.ts +7 -0
  48. package/src/i18n/index.ts +149 -0
  49. package/src/i18n/navigation.ts +90 -0
  50. package/src/i18n/plugin.ts +66 -0
  51. package/src/i18n/provider.tsx +91 -0
  52. package/src/i18n/proxy.ts +81 -0
  53. package/src/i18n/request.ts +141 -0
  54. package/src/i18n/routing.ts +84 -0
  55. package/src/i18n/server.ts +175 -0
  56. package/src/i18n/types.ts +88 -0
  57. package/src/sitemap/generator.ts +84 -9
  58. package/src/sitemap/index.ts +1 -1
  59. package/src/sitemap/route.ts +71 -8
  60. package/src/sitemap/types.ts +9 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * i18n Proxy for Next.js App Router (Next.js 16+)
3
+ *
4
+ * Handles locale detection and routing
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // proxy.ts
9
+ * export { proxy as default, config } from '@djangocfg/nextjs/i18n';
10
+ *
11
+ * // Or with custom routing:
12
+ * import { createProxy } from '@djangocfg/nextjs/i18n';
13
+ * import { routing } from './i18n/routing';
14
+ *
15
+ * export default createProxy(routing);
16
+ * export const config = { matcher: [...] };
17
+ * ```
18
+ */
19
+
20
+ import createIntlMiddleware from 'next-intl/middleware';
21
+ import type { NextRequest } from 'next/server';
22
+
23
+ import { routing, createRouting } from './routing';
24
+ import type { I18nConfig } from './types';
25
+
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ // Proxy Factory
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Create i18n proxy handler with custom routing configuration
32
+ */
33
+ export function createProxy(routingConfig?: ReturnType<typeof createRouting>) {
34
+ const config = routingConfig ?? routing;
35
+ return createIntlMiddleware(config);
36
+ }
37
+
38
+ /**
39
+ * Create i18n proxy from config options
40
+ */
41
+ export function createProxyFromConfig(config: Partial<I18nConfig>) {
42
+ const routingConfig = createRouting(config);
43
+ return createIntlMiddleware(routingConfig);
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // Default Proxy
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ const handleI18nRouting = createProxy();
51
+
52
+ /**
53
+ * Default proxy function using default routing
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // proxy.ts
58
+ * export { proxy as default, config } from '@djangocfg/nextjs/i18n';
59
+ * ```
60
+ */
61
+ export function proxy(request: NextRequest) {
62
+ return handleI18nRouting(request);
63
+ }
64
+
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // Config
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Default proxy config
71
+ * Matches all paths except static files, API routes, and Next.js internals
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // proxy.ts
76
+ * export { proxy as default, config } from '@djangocfg/nextjs/i18n';
77
+ * ```
78
+ */
79
+ export const config = {
80
+ matcher: ['/((?!api|_next|_vercel|.*\\..*).*)',],
81
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * i18n Request Configuration for Server Components
3
+ *
4
+ * Provides translations to server components via next-intl's request scope
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // i18n/request.ts in your app
9
+ * import { createRequestConfig } from '@djangocfg/nextjs/i18n';
10
+ * import { en, ru, ko } from '@djangocfg/i18n';
11
+ * import { leadsI18n } from '@djangocfg/ext-leads';
12
+ *
13
+ * export default createRequestConfig({
14
+ * locales: { en, ru, ko },
15
+ * extensions: [leadsI18n],
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ import { getRequestConfig } from 'next-intl/server';
21
+ import { mergeTranslations, en, ru, ko } from '@djangocfg/i18n';
22
+ import type { I18nTranslations, LocaleCode } from '@djangocfg/i18n';
23
+ import type { Messages } from './types';
24
+ import { routing } from './routing';
25
+
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ // Types
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ export interface RequestConfigOptions {
31
+ /** Base locale translations from @djangocfg/i18n */
32
+ locales?: Record<LocaleCode, I18nTranslations>;
33
+ /** Extension i18n instances to merge */
34
+ extensions?: Array<{
35
+ namespace: string;
36
+ locales: Record<string, Record<string, unknown>>;
37
+ }>;
38
+ /** Custom message loader (overrides locales) */
39
+ loadMessages?: (locale: LocaleCode) => Promise<Messages> | Messages;
40
+ /** Time zone for date/time formatting */
41
+ timeZone?: string;
42
+ }
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Default Locales
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ const DEFAULT_LOCALES: Record<LocaleCode, I18nTranslations> = {
49
+ en,
50
+ ru,
51
+ ko,
52
+ };
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+ // Message Loading
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Load and merge messages for a locale
60
+ */
61
+ function loadMessages(
62
+ locale: LocaleCode,
63
+ options: RequestConfigOptions
64
+ ): Messages {
65
+ // Get base translations
66
+ const locales = options.locales ?? DEFAULT_LOCALES;
67
+ const baseMessages = locales[locale] ?? locales.en ?? en;
68
+
69
+ // If no extensions, return base
70
+ if (!options.extensions?.length) {
71
+ return baseMessages as Messages;
72
+ }
73
+
74
+ // Merge extension translations
75
+ let mergedMessages = { ...baseMessages } as Messages;
76
+
77
+ for (const extension of options.extensions) {
78
+ const extMessages = extension.locales[locale] ?? extension.locales.en;
79
+ if (extMessages) {
80
+ mergedMessages = mergeTranslations(mergedMessages, {
81
+ [extension.namespace]: extMessages,
82
+ }) as Messages;
83
+ }
84
+ }
85
+
86
+ return mergedMessages;
87
+ }
88
+
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // Request Config Factory
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Create request configuration for next-intl
95
+ *
96
+ * This is used in your app's `i18n/request.ts` file
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * // i18n/request.ts
101
+ * import { createRequestConfig } from '@djangocfg/nextjs/i18n';
102
+ * import { leadsI18n } from '@djangocfg/ext-leads';
103
+ * import { paymentsI18n } from '@djangocfg/ext-payments';
104
+ *
105
+ * export default createRequestConfig({
106
+ * extensions: [leadsI18n, paymentsI18n],
107
+ * });
108
+ * ```
109
+ */
110
+ export function createRequestConfig(options: RequestConfigOptions = {}) {
111
+ return getRequestConfig(async ({ requestLocale }) => {
112
+ // Get locale from request or default
113
+ let locale = await requestLocale;
114
+
115
+ // Validate and fallback to default
116
+ if (!locale || !routing.locales.includes(locale as LocaleCode)) {
117
+ locale = routing.defaultLocale;
118
+ }
119
+
120
+ // Load messages
121
+ const messages = options.loadMessages
122
+ ? await options.loadMessages(locale as LocaleCode)
123
+ : loadMessages(locale as LocaleCode, options);
124
+
125
+ return {
126
+ locale,
127
+ messages,
128
+ timeZone: options.timeZone ?? 'UTC',
129
+ };
130
+ });
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+ // Default Export
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Default request config with base @djangocfg/i18n translations
139
+ * Can be used directly if no extensions are needed
140
+ */
141
+ export default createRequestConfig();
@@ -0,0 +1,84 @@
1
+ /**
2
+ * i18n Routing Configuration
3
+ *
4
+ * Creates routing configuration for next-intl
5
+ * Used by proxy and navigation components
6
+ */
7
+
8
+ import { defineRouting } from 'next-intl/routing';
9
+ import type { I18nConfig, LocaleCode } from './types';
10
+
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // Default Configuration
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko'];
16
+ const DEFAULT_LOCALE: LocaleCode = 'en';
17
+
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // Routing Factory
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Create routing configuration for next-intl
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * // i18n/routing.ts
28
+ * import { createRouting } from '@djangocfg/nextjs/i18n';
29
+ *
30
+ * export const routing = createRouting({
31
+ * locales: ['en', 'ru', 'ko'],
32
+ * defaultLocale: 'en',
33
+ * });
34
+ * ```
35
+ */
36
+ export function createRouting(config?: Partial<I18nConfig>) {
37
+ const locales = config?.locales ?? DEFAULT_LOCALES;
38
+ const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;
39
+ const localePrefix = config?.localePrefix ?? 'as-needed';
40
+
41
+ return defineRouting({
42
+ locales,
43
+ defaultLocale,
44
+ localePrefix,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Default routing configuration
50
+ * Can be overridden by app-specific configuration
51
+ */
52
+ export const routing = createRouting();
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+ // Locale Utilities
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Check if a locale is supported
60
+ */
61
+ export function isValidLocale(
62
+ locale: string,
63
+ supportedLocales: readonly string[] = DEFAULT_LOCALES
64
+ ): locale is LocaleCode {
65
+ return supportedLocales.includes(locale as LocaleCode);
66
+ }
67
+
68
+ /**
69
+ * Get locale from params (handles async params in Next.js 15+)
70
+ */
71
+ export async function getLocaleFromParams(
72
+ params: Promise<{ locale: string }> | { locale: string }
73
+ ): Promise<LocaleCode> {
74
+ const resolved = await params;
75
+ return resolved.locale as LocaleCode;
76
+ }
77
+
78
+ /**
79
+ * Generate static params for all locales
80
+ * Use in generateStaticParams for locale pages
81
+ */
82
+ export function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {
83
+ return locales.map((locale) => ({ locale }));
84
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Server-side i18n Utilities
3
+ *
4
+ * For use in Server Components, Server Actions, and Route Handlers
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * // In a Server Component
9
+ * import { getTranslations, getLocale } from '@djangocfg/nextjs/i18n/server';
10
+ *
11
+ * export default async function Page() {
12
+ * const t = await getTranslations('HomePage');
13
+ * const locale = await getLocale();
14
+ *
15
+ * return <h1>{t('title')}</h1>;
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import {
21
+ getTranslations as getNextIntlTranslations,
22
+ getLocale as getNextIntlLocale,
23
+ getMessages as getNextIntlMessages,
24
+ getNow,
25
+ getTimeZone,
26
+ getFormatter,
27
+ } from 'next-intl/server';
28
+ import type { LocaleCode, Messages, LocaleParams } from './types';
29
+ import { isValidLocale, generateLocaleParams } from './routing';
30
+
31
+ // Re-export routing utilities for convenience
32
+ export { isValidLocale, generateLocaleParams };
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Core Server Functions
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Get translations for Server Components
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * const t = await getTranslations('HomePage');
44
+ * return <h1>{t('title')}</h1>;
45
+ *
46
+ * // Or without namespace
47
+ * const t = await getTranslations();
48
+ * return <h1>{t('HomePage.title')}</h1>;
49
+ * ```
50
+ */
51
+ export async function getTranslations<Namespace extends string = never>(
52
+ namespace?: Namespace
53
+ ) {
54
+ return getNextIntlTranslations(namespace);
55
+ }
56
+
57
+ /**
58
+ * Get current locale in Server Components
59
+ */
60
+ export async function getLocale(): Promise<LocaleCode> {
61
+ return (await getNextIntlLocale()) as LocaleCode;
62
+ }
63
+
64
+ /**
65
+ * Get all messages for the current request
66
+ * Useful for passing to I18nProvider in layouts
67
+ */
68
+ export async function getMessages(): Promise<Messages> {
69
+ return (await getNextIntlMessages()) as Messages;
70
+ }
71
+
72
+ /**
73
+ * Get current time for the request
74
+ * Useful for consistent time-based formatting
75
+ */
76
+ export { getNow };
77
+
78
+ /**
79
+ * Get timezone for the request
80
+ */
81
+ export { getTimeZone };
82
+
83
+ /**
84
+ * Get formatter for dates, numbers, etc.
85
+ */
86
+ export { getFormatter };
87
+
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // Locale Extraction Helpers
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Extract locale from page/layout params
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * // app/[locale]/page.tsx
98
+ * export default async function Page({ params }) {
99
+ * const locale = await getLocaleFromParams(params);
100
+ * // ...
101
+ * }
102
+ * ```
103
+ */
104
+ export async function getLocaleFromParams(
105
+ params: Promise<LocaleParams> | LocaleParams
106
+ ): Promise<LocaleCode> {
107
+ const resolved = await params;
108
+ return resolved.locale as LocaleCode;
109
+ }
110
+
111
+ /**
112
+ * Shorthand for getting locale from params
113
+ * Alias for getLocaleFromParams
114
+ */
115
+ export const extractLocale = getLocaleFromParams;
116
+
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+ // Typed Translation Helpers
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Get typed translations for a specific namespace
123
+ *
124
+ * @example
125
+ * ```tsx
126
+ * const t = await getNamespacedTranslations('payments');
127
+ * return <span>{t('balance.available')}</span>;
128
+ * ```
129
+ */
130
+ export async function getNamespacedTranslations(namespace: string) {
131
+ return getNextIntlTranslations(namespace);
132
+ }
133
+
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ // Metadata Helpers
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+
138
+ /**
139
+ * Generate localized metadata for pages
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * // app/[locale]/about/page.tsx
144
+ * import { generateLocalizedMetadata } from '@djangocfg/nextjs/i18n/server';
145
+ *
146
+ * export async function generateMetadata({ params }) {
147
+ * return generateLocalizedMetadata(params, {
148
+ * titleKey: 'AboutPage.meta.title',
149
+ * descriptionKey: 'AboutPage.meta.description',
150
+ * });
151
+ * }
152
+ * ```
153
+ */
154
+ export async function generateLocalizedMetadata(
155
+ params: Promise<LocaleParams> | LocaleParams,
156
+ options: {
157
+ titleKey?: string;
158
+ descriptionKey?: string;
159
+ namespace?: string;
160
+ } = {}
161
+ ) {
162
+ const locale = await getLocaleFromParams(params);
163
+ const t = await getTranslations(options.namespace);
164
+
165
+ return {
166
+ title: options.titleKey ? t(options.titleKey as never) : undefined,
167
+ description: options.descriptionKey ? t(options.descriptionKey as never) : undefined,
168
+ // Add locale to alternate languages
169
+ alternates: {
170
+ languages: {
171
+ [locale]: `/${locale}`,
172
+ },
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * i18n Types for Next.js App Router
3
+ *
4
+ * Integrates next-intl with @djangocfg/i18n
5
+ */
6
+
7
+ import type { LocaleCode, I18nTranslations } from '@djangocfg/i18n';
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // Configuration Types
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+
13
+ export interface I18nConfig {
14
+ /** Supported locales */
15
+ locales: LocaleCode[];
16
+ /** Default locale (fallback) */
17
+ defaultLocale: LocaleCode;
18
+ /** Locale prefix strategy */
19
+ localePrefix?: 'always' | 'as-needed' | 'never';
20
+ /** Cookie name for locale preference */
21
+ cookieName?: string;
22
+ /** Paths to exclude from locale routing (e.g., '/api', '/_next') */
23
+ excludedPaths?: string[];
24
+ }
25
+
26
+ export interface I18nPluginOptions extends I18nConfig {
27
+ /** Path to i18n request config file (default: './src/i18n/request.ts' or './i18n/request.ts') */
28
+ requestConfig?: string;
29
+ }
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Translation Types
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ /** Messages structure - compatible with next-intl */
36
+ export type Messages = I18nTranslations & {
37
+ [namespace: string]: Record<string, unknown>;
38
+ };
39
+
40
+ /** Locale-specific messages loader */
41
+ export type MessagesLoader = (locale: LocaleCode) => Promise<Messages> | Messages;
42
+
43
+ /** Extension translations that can be merged */
44
+ export interface ExtensionMessages {
45
+ namespace: string;
46
+ messages: Record<LocaleCode, Record<string, unknown>>;
47
+ }
48
+
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // Provider Types
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+
53
+ export interface I18nProviderProps {
54
+ /** Current locale */
55
+ locale: LocaleCode;
56
+ /** Translation messages */
57
+ messages: Messages;
58
+ /** Time zone for date/time formatting */
59
+ timeZone?: string;
60
+ /** Now value for relative time */
61
+ now?: Date;
62
+ /** Children */
63
+ children: React.ReactNode;
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // Routing Types
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ export interface LocaleParams {
71
+ locale: string;
72
+ }
73
+
74
+ export interface LocaleLayoutProps {
75
+ children: React.ReactNode;
76
+ params: Promise<LocaleParams>;
77
+ }
78
+
79
+ export interface LocalePageProps {
80
+ params: Promise<LocaleParams>;
81
+ searchParams?: Promise<Record<string, string | string[] | undefined>>;
82
+ }
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // Re-exports from @djangocfg/i18n
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+
88
+ export type { LocaleCode, I18nTranslations } from '@djangocfg/i18n';
@@ -2,32 +2,107 @@
2
2
  * Sitemap Generator
3
3
  *
4
4
  * Generates XML sitemap from configuration
5
+ * Supports i18n with hreflang tags for multilingual sites
5
6
  */
6
7
 
7
8
  import type { SitemapUrl } from '../types';
9
+ import type { SitemapI18nOptions } from './types';
10
+
11
+ export interface GenerateSitemapXmlOptions {
12
+ urls: SitemapUrl[];
13
+ i18n?: SitemapI18nOptions;
14
+ siteUrl: string;
15
+ }
8
16
 
9
17
  /**
10
18
  * Generate XML sitemap string from URLs
19
+ * Supports i18n with hreflang alternate links
11
20
  */
12
- export function generateSitemapXml(urls: SitemapUrl[]): string {
13
- return `<?xml version="1.0" encoding="UTF-8"?>
14
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
21
+ export function generateSitemapXml(
22
+ urlsOrOptions: SitemapUrl[] | GenerateSitemapXmlOptions
23
+ ): string {
24
+ // Support both old signature (just urls) and new signature (options object)
25
+ const isOptionsObject = !Array.isArray(urlsOrOptions);
26
+ const urls = isOptionsObject ? urlsOrOptions.urls : urlsOrOptions;
27
+ const i18n = isOptionsObject ? urlsOrOptions.i18n : undefined;
28
+ const siteUrl = isOptionsObject ? urlsOrOptions.siteUrl : '';
29
+
30
+ // Add xhtml namespace if i18n is enabled
31
+ const namespaces = i18n
32
+ ? `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
33
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
34
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
35
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
36
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"`
37
+ : `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
15
38
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
16
39
  xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
17
- http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
40
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"`;
41
+
42
+ return `<?xml version="1.0" encoding="UTF-8"?>
43
+ <urlset ${namespaces}>
18
44
  ${urls
19
- .map(
20
- ({ loc, lastmod, changefreq, priority }) => ` <url>
21
- <loc>${escapeXml(loc)}</loc>
45
+ .map(({ loc, lastmod, changefreq, priority }) => {
46
+ const hreflangLinks = i18n
47
+ ? generateHreflangLinks(loc, i18n, siteUrl)
48
+ : '';
49
+
50
+ return ` <url>
51
+ <loc>${escapeXml(loc)}</loc>${hreflangLinks}
22
52
  ${lastmod ? `<lastmod>${formatDate(lastmod)}</lastmod>` : ''}
23
53
  ${changefreq ? `<changefreq>${changefreq}</changefreq>` : ''}
24
54
  ${priority !== undefined ? `<priority>${priority.toFixed(1)}</priority>` : ''}
25
- </url>`
26
- )
55
+ </url>`;
56
+ })
27
57
  .join('\n')}
28
58
  </urlset>`;
29
59
  }
30
60
 
61
+ /**
62
+ * Generate hreflang links for a URL
63
+ */
64
+ function generateHreflangLinks(
65
+ loc: string,
66
+ i18n: SitemapI18nOptions,
67
+ siteUrl: string
68
+ ): string {
69
+ const { locales, defaultLocale } = i18n;
70
+
71
+ // Extract the path without locale prefix from the URL
72
+ // e.g., https://example.com/en/page -> /page
73
+ const baseSiteUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
74
+ let path = loc.replace(baseSiteUrl, '');
75
+
76
+ // Remove locale prefix if present
77
+ for (const locale of locales) {
78
+ const localePrefix = `/${locale}`;
79
+ if (path === localePrefix || path.startsWith(`${localePrefix}/`)) {
80
+ path = path.slice(localePrefix.length) || '/';
81
+ break;
82
+ }
83
+ }
84
+
85
+ const links: string[] = [];
86
+
87
+ // Add hreflang for each locale
88
+ for (const locale of locales) {
89
+ const localePath = path === '/' ? `/${locale}` : `/${locale}${path}`;
90
+ const fullUrl = `${baseSiteUrl}${localePath}`;
91
+ links.push(
92
+ ` <xhtml:link rel="alternate" hreflang="${locale}" href="${escapeXml(fullUrl)}"/>`
93
+ );
94
+ }
95
+
96
+ // Add x-default pointing to default locale
97
+ const defaultPath = path === '/' ? `/${defaultLocale}` : `/${defaultLocale}${path}`;
98
+ const defaultUrl = `${baseSiteUrl}${defaultPath}`;
99
+ links.push(
100
+ ` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(defaultUrl)}"/>`
101
+ );
102
+
103
+ return '\n' + links.join('\n');
104
+ }
105
+
31
106
  /**
32
107
  * Format date for sitemap (ISO 8601)
33
108
  */