@lobehub/chat 1.19.2 → 1.19.4

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.

Potentially problematic release.


This version of @lobehub/chat might be problematic. Click here for more details.

Files changed (32) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/Dockerfile +22 -20
  3. package/Dockerfile.database +22 -20
  4. package/README.md +8 -10
  5. package/README.zh-CN.md +8 -10
  6. package/next.config.mjs +10 -0
  7. package/package.json +6 -7
  8. package/scripts/buildSitemapIndex/index.ts +12 -0
  9. package/src/app/(main)/discover/(detail)/assistant/[slug]/page.tsx +1 -0
  10. package/src/app/(main)/discover/(detail)/model/[...slugs]/features/ProviderList/ProviderItem.tsx +1 -0
  11. package/src/app/(main)/discover/(detail)/model/[...slugs]/page.tsx +1 -0
  12. package/src/app/(main)/discover/(detail)/plugin/[slug]/page.tsx +1 -0
  13. package/src/app/(main)/discover/(detail)/provider/[slug]/features/ModelList/ModelItem.tsx +1 -0
  14. package/src/app/(main)/discover/(detail)/provider/[slug]/page.tsx +1 -0
  15. package/src/app/(main)/discover/(list)/_layout/Desktop/Nav.tsx +1 -0
  16. package/src/app/(main)/discover/_layout/Desktop/index.tsx +1 -0
  17. package/src/app/page.tsx +1 -1
  18. package/src/app/robots.tsx +16 -0
  19. package/src/app/sitemap.tsx +30 -0
  20. package/src/const/url.ts +2 -2
  21. package/src/server/ld.test.ts +102 -0
  22. package/src/server/ld.ts +3 -9
  23. package/src/server/metadata.test.ts +138 -0
  24. package/src/server/metadata.ts +3 -3
  25. package/src/server/modules/AssistantStore/index.test.ts +1 -1
  26. package/src/server/modules/AssistantStore/index.ts +2 -6
  27. package/src/server/sitemap.test.ts +179 -0
  28. package/src/server/sitemap.ts +243 -0
  29. package/src/server/translation.test.ts +137 -0
  30. package/src/server/utils/url.test.ts +61 -0
  31. package/src/server/utils/url.ts +9 -0
  32. package/next-sitemap.config.mjs +0 -53
@@ -0,0 +1,138 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { BRANDING_NAME } from '@/const/branding';
5
+ import { OG_URL } from '@/const/url';
6
+
7
+ import { Meta } from './metadata';
8
+
9
+ describe('Metadata', () => {
10
+ const meta = new Meta();
11
+
12
+ describe('generate', () => {
13
+ it('should generate metadata with default values', () => {
14
+ const result = meta.generate({
15
+ title: 'Test Title',
16
+ url: 'https://example.com',
17
+ });
18
+
19
+ expect(result).toMatchObject({
20
+ title: 'Test Title',
21
+ description: expect.any(String),
22
+ openGraph: expect.objectContaining({
23
+ title: `Test Title · ${BRANDING_NAME}`,
24
+ description: expect.any(String),
25
+ images: [{ url: OG_URL, alt: `Test Title · ${BRANDING_NAME}` }],
26
+ }),
27
+ twitter: expect.objectContaining({
28
+ title: `Test Title · ${BRANDING_NAME}`,
29
+ description: expect.any(String),
30
+ images: [OG_URL],
31
+ }),
32
+ });
33
+ });
34
+
35
+ it('should generate metadata with custom values', () => {
36
+ const result = meta.generate({
37
+ title: 'Custom Title',
38
+ description: 'Custom description',
39
+ image: 'https://custom-image.com',
40
+ url: 'https://example.com/custom',
41
+ type: 'article',
42
+ tags: ['tag1', 'tag2'],
43
+ locale: 'fr-FR',
44
+ alternate: true,
45
+ });
46
+
47
+ expect(result).toMatchObject({
48
+ title: 'Custom Title',
49
+ description: expect.stringContaining('Custom description'),
50
+ openGraph: expect.objectContaining({
51
+ title: `Custom Title · ${BRANDING_NAME}`,
52
+ description: 'Custom description',
53
+ images: [{ url: 'https://custom-image.com', alt: `Custom Title · ${BRANDING_NAME}` }],
54
+ type: 'article',
55
+ locale: 'fr-FR',
56
+ }),
57
+ twitter: expect.objectContaining({
58
+ title: `Custom Title · ${BRANDING_NAME}`,
59
+ description: 'Custom description',
60
+ images: ['https://custom-image.com'],
61
+ }),
62
+ alternates: expect.objectContaining({
63
+ languages: expect.any(Object),
64
+ }),
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('genAlternateLocales', () => {
70
+ it('should generate alternate locales correctly', () => {
71
+ const result = (meta as any).genAlternateLocales('en', '/test');
72
+
73
+ expect(result).toHaveProperty('x-default', expect.stringContaining('/test'));
74
+ expect(result).toHaveProperty('zh-CN', expect.stringContaining('hl=zh-CN'));
75
+ expect(result).not.toHaveProperty('en');
76
+ });
77
+ });
78
+
79
+ describe('genTwitter', () => {
80
+ it('should generate Twitter metadata correctly', () => {
81
+ const result = (meta as any).genTwitter({
82
+ title: 'Twitter Title',
83
+ description: 'Twitter description',
84
+ image: 'https://twitter-image.com',
85
+ url: 'https://example.com/twitter',
86
+ });
87
+
88
+ expect(result).toEqual({
89
+ card: 'summary_large_image',
90
+ title: 'Twitter Title',
91
+ description: 'Twitter description',
92
+ images: ['https://twitter-image.com'],
93
+ site: '@lobehub',
94
+ url: 'https://example.com/twitter',
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('genOpenGraph', () => {
100
+ it('should generate OpenGraph metadata correctly', () => {
101
+ const result = (meta as any).genOpenGraph({
102
+ title: 'OG Title',
103
+ description: 'OG description',
104
+ image: 'https://og-image.com',
105
+ url: 'https://example.com/og',
106
+ locale: 'es-ES',
107
+ type: 'article',
108
+ alternate: true,
109
+ });
110
+
111
+ expect(result).toMatchObject({
112
+ title: 'OG Title',
113
+ description: 'OG description',
114
+ images: [{ url: 'https://og-image.com', alt: 'OG Title' }],
115
+ locale: 'es-ES',
116
+ type: 'article',
117
+ url: 'https://example.com/og',
118
+ siteName: 'LobeChat',
119
+ alternateLocale: expect.arrayContaining([
120
+ 'ar',
121
+ 'bg-BG',
122
+ 'de-DE',
123
+ 'en-US',
124
+ 'es-ES',
125
+ 'fr-FR',
126
+ 'ja-JP',
127
+ 'ko-KR',
128
+ 'pt-BR',
129
+ 'ru-RU',
130
+ 'tr-TR',
131
+ 'zh-CN',
132
+ 'zh-TW',
133
+ 'vi-VN',
134
+ ]),
135
+ });
136
+ });
137
+ });
138
+ });
@@ -3,8 +3,9 @@ import qs from 'query-string';
3
3
 
4
4
  import { BRANDING_NAME } from '@/const/branding';
5
5
  import { DEFAULT_LANG } from '@/const/locale';
6
- import { OG_URL, getCanonicalUrl } from '@/const/url';
6
+ import { OG_URL } from '@/const/url';
7
7
  import { Locales, locales } from '@/locales/resources';
8
+ import { getCanonicalUrl } from '@/server/utils/url';
8
9
  import { formatDescLength, formatTitleLength } from '@/utils/genOG';
9
10
 
10
11
  export class Meta {
@@ -59,7 +60,6 @@ export class Meta {
59
60
  let links: any = {};
60
61
  const defaultLink = getCanonicalUrl(path);
61
62
  for (const alterLocales of locales) {
62
- if (locale === alterLocales) continue;
63
63
  links[alterLocales] = qs.stringifyUrl({
64
64
  query: { hl: alterLocales },
65
65
  url: defaultLink,
@@ -125,7 +125,7 @@ export class Meta {
125
125
  };
126
126
 
127
127
  if (alternate) {
128
- data['alternateLocale'] = locales.filter((l) => l !== locale);
128
+ data['alternateLocale'] = locales;
129
129
  }
130
130
 
131
131
  return data;
@@ -13,7 +13,7 @@ describe('AssistantStore', () => {
13
13
 
14
14
  it('should return the index URL for a not supported language', () => {
15
15
  const agentMarket = new AssistantStore();
16
- const url = agentMarket.getAgentIndexUrl('ko-KR');
16
+ const url = agentMarket.getAgentIndexUrl('xxx' as any);
17
17
  expect(url).toBe('https://chat-agents.lobehub.com');
18
18
  });
19
19
 
@@ -4,10 +4,6 @@ import { appEnv } from '@/config/app';
4
4
  import { DEFAULT_LANG, isLocaleNotSupport } from '@/const/locale';
5
5
  import { Locales, normalizeLocale } from '@/locales/resources';
6
6
 
7
- const checkSupportLocale = (lang: Locales) => {
8
- return isLocaleNotSupport(lang) || normalizeLocale(lang) !== 'zh-CN';
9
- };
10
-
11
7
  export class AssistantStore {
12
8
  private readonly baseUrl: string;
13
9
 
@@ -16,13 +12,13 @@ export class AssistantStore {
16
12
  }
17
13
 
18
14
  getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => {
19
- if (checkSupportLocale(lang)) return this.baseUrl;
15
+ if (isLocaleNotSupport(lang)) return this.baseUrl;
20
16
 
21
17
  return urlJoin(this.baseUrl, `index.${normalizeLocale(lang)}.json`);
22
18
  };
23
19
 
24
20
  getAgentUrl = (identifier: string, lang: Locales = DEFAULT_LANG) => {
25
- if (checkSupportLocale(lang)) return urlJoin(this.baseUrl, `${identifier}.json`);
21
+ if (isLocaleNotSupport(lang)) return urlJoin(this.baseUrl, `${identifier}.json`);
26
22
 
27
23
  return urlJoin(this.baseUrl, `${identifier}.${normalizeLocale(lang)}.json`);
28
24
  };
@@ -0,0 +1,179 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { getCanonicalUrl } from '@/server/utils/url';
5
+ import { AssistantCategory, PluginCategory } from '@/types/discover';
6
+
7
+ import { LAST_MODIFIED, Sitemap, SitemapType } from './sitemap';
8
+
9
+ describe('Sitemap', () => {
10
+ const sitemap = new Sitemap();
11
+
12
+ describe('getIndex', () => {
13
+ it('should return a valid sitemap index', () => {
14
+ const index = sitemap.getIndex();
15
+ expect(index).toContain('<?xml version="1.0" encoding="UTF-8"?>');
16
+ expect(index).toContain('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
17
+ [
18
+ SitemapType.Pages,
19
+ SitemapType.Assistants,
20
+ SitemapType.Plugins,
21
+ SitemapType.Models,
22
+ SitemapType.Providers,
23
+ ].forEach((type) => {
24
+ expect(index).toContain(`<loc>${getCanonicalUrl(`/sitemap/${type}.xml`)}</loc>`);
25
+ });
26
+ expect(index).toContain(`<lastmod>${LAST_MODIFIED}</lastmod>`);
27
+ });
28
+ });
29
+
30
+ describe('getPage', () => {
31
+ it('should return a valid page sitemap', async () => {
32
+ const pageSitemap = await sitemap.getPage();
33
+ expect(pageSitemap).toContainEqual(
34
+ expect.objectContaining({
35
+ url: getCanonicalUrl('/'),
36
+ changeFrequency: 'monthly',
37
+ priority: 0.4,
38
+ }),
39
+ );
40
+ expect(pageSitemap).toContainEqual(
41
+ expect.objectContaining({
42
+ url: getCanonicalUrl('/discover'),
43
+ changeFrequency: 'daily',
44
+ priority: 0.7,
45
+ }),
46
+ );
47
+ Object.values(AssistantCategory).forEach((category) => {
48
+ expect(pageSitemap).toContainEqual(
49
+ expect.objectContaining({
50
+ url: getCanonicalUrl(`/discover/assistants/${category}`),
51
+ changeFrequency: 'daily',
52
+ priority: 0.7,
53
+ }),
54
+ );
55
+ });
56
+ Object.values(PluginCategory).forEach((category) => {
57
+ expect(pageSitemap).toContainEqual(
58
+ expect.objectContaining({
59
+ url: getCanonicalUrl(`/discover/plugins/${category}`),
60
+ changeFrequency: 'daily',
61
+ priority: 0.7,
62
+ }),
63
+ );
64
+ });
65
+ });
66
+ });
67
+
68
+ describe('getAssistants', () => {
69
+ it('should return a valid assistants sitemap', async () => {
70
+ vi.spyOn(sitemap['discoverService'], 'getAssistantList').mockResolvedValue([
71
+ // @ts-ignore
72
+ { identifier: 'test-assistant', createdAt: '2023-01-01' },
73
+ ]);
74
+
75
+ const assistantsSitemap = await sitemap.getAssistants();
76
+ expect(assistantsSitemap.length).toBe(14);
77
+ expect(assistantsSitemap).toContainEqual(
78
+ expect.objectContaining({
79
+ url: getCanonicalUrl('/discover/assistant/test-assistant'),
80
+ lastModified: '2023-01-01T00:00:00.000Z',
81
+ }),
82
+ );
83
+ expect(assistantsSitemap).toContainEqual(
84
+ expect.objectContaining({
85
+ url: getCanonicalUrl('/discover/assistant/test-assistant?hl=zh-CN'),
86
+ lastModified: '2023-01-01T00:00:00.000Z',
87
+ }),
88
+ );
89
+ });
90
+ });
91
+
92
+ describe('getPlugins', () => {
93
+ it('should return a valid plugins sitemap', async () => {
94
+ vi.spyOn(sitemap['discoverService'], 'getPluginList').mockResolvedValue([
95
+ // @ts-ignore
96
+ { identifier: 'test-plugin', createdAt: '2023-01-01' },
97
+ ]);
98
+
99
+ const pluginsSitemap = await sitemap.getPlugins();
100
+ expect(pluginsSitemap.length).toBe(14);
101
+ expect(pluginsSitemap).toContainEqual(
102
+ expect.objectContaining({
103
+ url: getCanonicalUrl('/discover/plugin/test-plugin'),
104
+ lastModified: '2023-01-01T00:00:00.000Z',
105
+ }),
106
+ );
107
+ expect(pluginsSitemap).toContainEqual(
108
+ expect.objectContaining({
109
+ url: getCanonicalUrl('/discover/plugin/test-plugin?hl=ja-JP'),
110
+ lastModified: '2023-01-01T00:00:00.000Z',
111
+ }),
112
+ );
113
+ });
114
+ });
115
+
116
+ describe('getModels', () => {
117
+ it('should return a valid models sitemap', async () => {
118
+ vi.spyOn(sitemap['discoverService'], 'getModelList').mockResolvedValue([
119
+ // @ts-ignore
120
+ { identifier: 'test:model', createdAt: '2023-01-01' },
121
+ ]);
122
+
123
+ const modelsSitemap = await sitemap.getModels();
124
+ expect(modelsSitemap.length).toBe(14);
125
+ expect(modelsSitemap).toContainEqual(
126
+ expect.objectContaining({
127
+ url: getCanonicalUrl('/discover/model/test:model'),
128
+ lastModified: '2023-01-01T00:00:00.000Z',
129
+ }),
130
+ );
131
+ expect(modelsSitemap).toContainEqual(
132
+ expect.objectContaining({
133
+ url: getCanonicalUrl('/discover/model/test:model?hl=ko-KR'),
134
+ lastModified: '2023-01-01T00:00:00.000Z',
135
+ }),
136
+ );
137
+ });
138
+ });
139
+
140
+ describe('getProviders', () => {
141
+ it('should return a valid providers sitemap', async () => {
142
+ vi.spyOn(sitemap['discoverService'], 'getProviderList').mockResolvedValue([
143
+ // @ts-ignore
144
+ { identifier: 'test-provider', createdAt: '2023-01-01' },
145
+ ]);
146
+
147
+ const providersSitemap = await sitemap.getProviders();
148
+ expect(providersSitemap.length).toBe(14);
149
+ expect(providersSitemap).toContainEqual(
150
+ expect.objectContaining({
151
+ url: getCanonicalUrl('/discover/provider/test-provider'),
152
+ lastModified: '2023-01-01T00:00:00.000Z',
153
+ }),
154
+ );
155
+ expect(providersSitemap).toContainEqual(
156
+ expect.objectContaining({
157
+ url: getCanonicalUrl('/discover/provider/test-provider?hl=ar'),
158
+ lastModified: '2023-01-01T00:00:00.000Z',
159
+ }),
160
+ );
161
+ });
162
+ });
163
+
164
+ describe('getRobots', () => {
165
+ it('should return correct robots.txt entries', () => {
166
+ const robots = sitemap.getRobots();
167
+ expect(robots).toContain(getCanonicalUrl('/sitemap-index.xml'));
168
+ [
169
+ SitemapType.Pages,
170
+ SitemapType.Assistants,
171
+ SitemapType.Plugins,
172
+ SitemapType.Models,
173
+ SitemapType.Providers,
174
+ ].forEach((type) => {
175
+ expect(robots).toContain(getCanonicalUrl(`/sitemap/${type}.xml`));
176
+ });
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,243 @@
1
+ import { flatten } from 'lodash-es';
2
+ import { MetadataRoute } from 'next';
3
+ import qs from 'query-string';
4
+ import urlJoin from 'url-join';
5
+
6
+ import { DEFAULT_LANG } from '@/const/locale';
7
+ import { SITEMAP_BASE_URL } from '@/const/url';
8
+ import { Locales, locales as allLocales } from '@/locales/resources';
9
+ import { DiscoverService } from '@/server/services/discover';
10
+ import { getCanonicalUrl } from '@/server/utils/url';
11
+ import { AssistantCategory, PluginCategory } from '@/types/discover';
12
+ import { isDev } from '@/utils/env';
13
+
14
+ export interface SitemapItem {
15
+ alternates?: {
16
+ languages?: string;
17
+ };
18
+ changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
19
+ lastModified?: string | Date;
20
+ priority?: number;
21
+ url: string;
22
+ }
23
+
24
+ export enum SitemapType {
25
+ Assistants = 'assistants',
26
+ Models = 'models',
27
+ Pages = 'pages',
28
+ Plugins = 'plugins',
29
+ Providers = 'providers',
30
+ }
31
+
32
+ export const LAST_MODIFIED = new Date().toISOString();
33
+
34
+ export class Sitemap {
35
+ sitemapIndexs = [
36
+ { id: SitemapType.Pages },
37
+ { id: SitemapType.Assistants },
38
+ { id: SitemapType.Plugins },
39
+ { id: SitemapType.Models },
40
+ { id: SitemapType.Providers },
41
+ ];
42
+
43
+ private discoverService = new DiscoverService();
44
+
45
+ private _generateSitemapLink(url: string) {
46
+ return [
47
+ '<sitemap>',
48
+ `<loc>${url}</loc>`,
49
+ `<lastmod>${LAST_MODIFIED}</lastmod>`,
50
+ '</sitemap>',
51
+ ].join('\n');
52
+ }
53
+
54
+ private _formatTime(time?: string) {
55
+ try {
56
+ if (!time) return LAST_MODIFIED;
57
+ return new Date(time).toISOString() || LAST_MODIFIED;
58
+ } catch {
59
+ return LAST_MODIFIED;
60
+ }
61
+ }
62
+
63
+ private _genSitemapItem = (
64
+ lang: Locales,
65
+ url: string,
66
+ {
67
+ lastModified,
68
+ changeFrequency = 'monthly',
69
+ priority = 0.4,
70
+ noLocales,
71
+ locales = allLocales,
72
+ }: {
73
+ changeFrequency?: SitemapItem['changeFrequency'];
74
+ lastModified?: string;
75
+ locales?: typeof allLocales;
76
+ noLocales?: boolean;
77
+ priority?: number;
78
+ } = {},
79
+ ) => {
80
+ const sitemap = {
81
+ changeFrequency,
82
+ lastModified: this._formatTime(lastModified),
83
+ priority,
84
+ url:
85
+ lang === DEFAULT_LANG
86
+ ? getCanonicalUrl(url)
87
+ : qs.stringifyUrl({ query: { hl: lang }, url: getCanonicalUrl(url) }),
88
+ };
89
+ if (noLocales) return sitemap;
90
+
91
+ const languages: any = {};
92
+ for (const locale of locales) {
93
+ if (locale === lang) continue;
94
+ languages[locale] = qs.stringifyUrl({
95
+ query: { hl: locale },
96
+ url: getCanonicalUrl(url),
97
+ });
98
+ }
99
+ return {
100
+ alternates: {
101
+ languages,
102
+ },
103
+ ...sitemap,
104
+ };
105
+ };
106
+
107
+ private _genSitemap(
108
+ url: string,
109
+ {
110
+ lastModified,
111
+ changeFrequency = 'monthly',
112
+ priority = 0.4,
113
+ noLocales,
114
+ locales = allLocales,
115
+ }: {
116
+ changeFrequency?: SitemapItem['changeFrequency'];
117
+ lastModified?: string;
118
+ locales?: typeof allLocales;
119
+ noLocales?: boolean;
120
+ priority?: number;
121
+ } = {},
122
+ ) {
123
+ if (noLocales)
124
+ return [
125
+ this._genSitemapItem(DEFAULT_LANG, url, {
126
+ changeFrequency,
127
+ lastModified,
128
+ locales,
129
+ noLocales,
130
+ priority,
131
+ }),
132
+ ];
133
+ return locales.map((lang) =>
134
+ this._genSitemapItem(lang, url, {
135
+ changeFrequency,
136
+ lastModified,
137
+ locales,
138
+ noLocales,
139
+ priority,
140
+ }),
141
+ );
142
+ }
143
+
144
+ getIndex(): string {
145
+ return [
146
+ '<?xml version="1.0" encoding="UTF-8"?>',
147
+ '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
148
+ ...this.sitemapIndexs.map((item) =>
149
+ this._generateSitemapLink(
150
+ getCanonicalUrl(SITEMAP_BASE_URL, isDev ? item.id : `${item.id}.xml`),
151
+ ),
152
+ ),
153
+ '</sitemapindex>',
154
+ ].join('\n');
155
+ }
156
+
157
+ async getAssistants(): Promise<MetadataRoute.Sitemap> {
158
+ const list = await this.discoverService.getAssistantList(DEFAULT_LANG);
159
+ const sitmap = list.map((item) =>
160
+ this._genSitemap(urlJoin('/discover/assistant', item.identifier), {
161
+ lastModified: item?.createdAt || LAST_MODIFIED,
162
+ }),
163
+ );
164
+ return flatten(sitmap);
165
+ }
166
+
167
+ async getPlugins(): Promise<MetadataRoute.Sitemap> {
168
+ const list = await this.discoverService.getPluginList(DEFAULT_LANG);
169
+ const sitmap = list.map((item) =>
170
+ this._genSitemap(urlJoin('/discover/plugin', item.identifier), {
171
+ lastModified: item?.createdAt || LAST_MODIFIED,
172
+ }),
173
+ );
174
+ return flatten(sitmap);
175
+ }
176
+
177
+ async getModels(): Promise<MetadataRoute.Sitemap> {
178
+ const list = await this.discoverService.getModelList(DEFAULT_LANG);
179
+ const sitmap = list.map((item) =>
180
+ this._genSitemap(urlJoin('/discover/model', item.identifier), {
181
+ lastModified: item?.createdAt || LAST_MODIFIED,
182
+ }),
183
+ );
184
+ return flatten(sitmap);
185
+ }
186
+
187
+ async getProviders(): Promise<MetadataRoute.Sitemap> {
188
+ const list = await this.discoverService.getProviderList(DEFAULT_LANG);
189
+ const sitmap = list.map((item) =>
190
+ this._genSitemap(urlJoin('/discover/provider', item.identifier), {
191
+ lastModified: item?.createdAt || LAST_MODIFIED,
192
+ }),
193
+ );
194
+ return flatten(sitmap);
195
+ }
196
+
197
+ async getPage(): Promise<MetadataRoute.Sitemap> {
198
+ const assistantsCategory = Object.values(AssistantCategory);
199
+ const pluginCategory = Object.values(PluginCategory);
200
+ const modelCategory = await this.discoverService.getProviderList(DEFAULT_LANG);
201
+ return [
202
+ ...this._genSitemap('/', { noLocales: true }),
203
+ ...this._genSitemap('/chat', { noLocales: true }),
204
+ ...this._genSitemap('/welcome', { noLocales: true }),
205
+ /* ↓ cloud slot ↓ */
206
+
207
+ /* ↑ cloud slot ↑ */
208
+ ...this._genSitemap('/discover', { changeFrequency: 'daily', priority: 0.7 }),
209
+ ...this._genSitemap('/discover/assistants', { changeFrequency: 'daily', priority: 0.7 }),
210
+ ...assistantsCategory.flatMap((slug) =>
211
+ this._genSitemap(`/discover/assistants/${slug}`, {
212
+ changeFrequency: 'daily',
213
+ priority: 0.7,
214
+ }),
215
+ ),
216
+ ...this._genSitemap('/discover/plugins', { changeFrequency: 'daily', priority: 0.7 }),
217
+ ...pluginCategory.flatMap((slug) =>
218
+ this._genSitemap(`/discover/plugins/${slug}`, {
219
+ changeFrequency: 'daily',
220
+ priority: 0.7,
221
+ }),
222
+ ),
223
+ ...this._genSitemap('/discover/models', { changeFrequency: 'daily', priority: 0.7 }),
224
+ ...modelCategory.flatMap((slug) =>
225
+ this._genSitemap(`/discover/models/${slug}`, {
226
+ changeFrequency: 'daily',
227
+ priority: 0.7,
228
+ }),
229
+ ),
230
+ ...this._genSitemap('/discover/providers', { changeFrequency: 'daily', priority: 0.7 }),
231
+ ];
232
+ }
233
+ getRobots() {
234
+ return [
235
+ getCanonicalUrl('/sitemap-index.xml'),
236
+ ...this.sitemapIndexs.map((index) =>
237
+ getCanonicalUrl(SITEMAP_BASE_URL, isDev ? index.id : `${index.id}.xml`),
238
+ ),
239
+ ];
240
+ }
241
+ }
242
+
243
+ export const sitemapModule = new Sitemap();