@hagicode/hagilight-core 0.1.0
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.
- package/Analytics51LA.astro +17 -0
- package/Copyright.astro +9 -0
- package/Footer.astro +167 -0
- package/GoogleAnalytics.astro +21 -0
- package/PromotoBanner.astro +346 -0
- package/dist/favicon.d.ts +21 -0
- package/dist/favicon.js +27 -0
- package/dist/head.d.ts +7 -0
- package/dist/head.js +1 -0
- package/dist/links.d.ts +68 -0
- package/dist/links.js +406 -0
- package/dist/promotions.d.ts +44 -0
- package/dist/promotions.js +216 -0
- package/dist/promoto-banner.d.ts +8 -0
- package/dist/promoto-banner.js +342 -0
- package/dist/related-sites.json +306 -0
- package/dist/rss-footer-context.d.ts +6 -0
- package/dist/rss-footer-context.js +54 -0
- package/dist/rss-ownership.d.ts +25 -0
- package/dist/rss-ownership.js +65 -0
- package/dist/rss.d.ts +34 -0
- package/dist/rss.js +145 -0
- package/dist/seo-schema.d.ts +12 -0
- package/dist/seo-schema.js +20 -0
- package/dist/seo.d.ts +78 -0
- package/dist/seo.js +257 -0
- package/favicon.ico +0 -0
- package/logo.png +0 -0
- package/package.json +79 -0
package/dist/links.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import docsRelatedSites from './related-sites.json' with { type: 'json' };
|
|
2
|
+
const translations = {
|
|
3
|
+
home: {
|
|
4
|
+
'zh-CN': '首页', 'zh-Hant': '首頁', 'en-US': 'Home', 'ja-JP': 'ホーム',
|
|
5
|
+
'ko-KR': '홈', 'de-DE': 'Startseite', 'fr-FR': 'Accueil', 'es-ES': 'Inicio',
|
|
6
|
+
'pt-BR': 'Início', 'ru-RU': 'Главная',
|
|
7
|
+
},
|
|
8
|
+
blog: {
|
|
9
|
+
'zh-CN': '博客', 'zh-Hant': '部落格', 'en-US': 'Blog', 'ja-JP': 'ブログ',
|
|
10
|
+
'ko-KR': '블로그', 'de-DE': 'Blog', 'fr-FR': 'Blog', 'es-ES': 'Blog',
|
|
11
|
+
'pt-BR': 'Blog', 'ru-RU': 'Блог',
|
|
12
|
+
},
|
|
13
|
+
support: {
|
|
14
|
+
'zh-CN': '获取技术支持', 'zh-Hant': '獲取技術支援', 'en-US': 'Get Support',
|
|
15
|
+
'ja-JP': 'サポート', 'ko-KR': '기술 지원', 'de-DE': 'Support',
|
|
16
|
+
'fr-FR': 'Assistance', 'es-ES': 'Soporte', 'pt-BR': 'Suporte', 'ru-RU': 'Поддержка',
|
|
17
|
+
},
|
|
18
|
+
downloadClient: {
|
|
19
|
+
'zh-CN': '下载 Hagicode', 'zh-Hant': '下載 Hagicode', 'en-US': 'Download Hagicode',
|
|
20
|
+
'ja-JP': 'Hagicode をダウンロード', 'ko-KR': 'Hagicode 다운로드',
|
|
21
|
+
'de-DE': 'Hagicode herunterladen', 'fr-FR': 'Télécharger Hagicode',
|
|
22
|
+
'es-ES': 'Descargar Hagicode', 'pt-BR': 'Baixar Hagicode', 'ru-RU': 'Скачать Hagicode',
|
|
23
|
+
},
|
|
24
|
+
about: {
|
|
25
|
+
'zh-CN': '关于 HagiCode', 'zh-Hant': '關於 HagiCode', 'en-US': 'About HagiCode',
|
|
26
|
+
'ja-JP': 'HagiCode について', 'ko-KR': 'HagiCode 소개', 'de-DE': 'Über HagiCode',
|
|
27
|
+
'fr-FR': 'À propos de HagiCode', 'es-ES': 'Acerca de HagiCode',
|
|
28
|
+
'pt-BR': 'Sobre a HagiCode', 'ru-RU': 'О HagiCode',
|
|
29
|
+
},
|
|
30
|
+
dockerCompose: {
|
|
31
|
+
'zh-CN': 'Docker Compose 安装', 'zh-Hant': 'Docker Compose 安裝',
|
|
32
|
+
'en-US': 'Docker Compose Installation', 'ja-JP': 'Docker Compose インストール',
|
|
33
|
+
'ko-KR': 'Docker Compose 설치', 'de-DE': 'Docker-Compose-Installation',
|
|
34
|
+
'fr-FR': 'Installation de Docker Compose', 'es-ES': 'Instalación de Docker Compose',
|
|
35
|
+
'pt-BR': 'Instalação do Docker Compose', 'ru-RU': 'Установка Docker Compose',
|
|
36
|
+
},
|
|
37
|
+
productDocs: {
|
|
38
|
+
'zh-CN': '产品文档', 'zh-Hant': '產品文件', 'en-US': 'Product Docs',
|
|
39
|
+
'ja-JP': '製品ドキュメント', 'ko-KR': '제품 문서', 'de-DE': 'Produktdokumentation',
|
|
40
|
+
'fr-FR': 'Documentation produit', 'es-ES': 'Documentación del producto',
|
|
41
|
+
'pt-BR': 'Documentação do produto', 'ru-RU': 'Документация продукта',
|
|
42
|
+
},
|
|
43
|
+
blogPosts: {
|
|
44
|
+
'zh-CN': '博客文章', 'zh-Hant': '部落格文章', 'en-US': 'Blog Posts',
|
|
45
|
+
'ja-JP': 'ブログ記事', 'ko-KR': '블로그 글', 'de-DE': 'Blogbeiträge',
|
|
46
|
+
'fr-FR': 'Articles du blog', 'es-ES': 'Artículos del blog',
|
|
47
|
+
'pt-BR': 'Posts do blog', 'ru-RU': 'Статьи блога',
|
|
48
|
+
},
|
|
49
|
+
sitemap: {
|
|
50
|
+
'zh-CN': '站点地图', 'zh-Hant': '網站地圖', 'en-US': 'Sitemap',
|
|
51
|
+
'ja-JP': 'サイトマップ', 'ko-KR': '사이트맵', 'de-DE': 'Sitemap',
|
|
52
|
+
'fr-FR': 'Plan du site', 'es-ES': 'Mapa del sitio',
|
|
53
|
+
'pt-BR': 'Mapa do site', 'ru-RU': 'Карта сайта',
|
|
54
|
+
},
|
|
55
|
+
rss: {
|
|
56
|
+
'zh-CN': 'RSS 订阅', 'zh-Hant': 'RSS 訂閱', 'en-US': 'RSS Feed',
|
|
57
|
+
'ja-JP': 'RSS 配信', 'ko-KR': 'RSS 구독', 'de-DE': 'RSS-Feed',
|
|
58
|
+
'fr-FR': 'Flux RSS', 'es-ES': 'RSS', 'pt-BR': 'Feed RSS', 'ru-RU': 'RSS-лента',
|
|
59
|
+
},
|
|
60
|
+
rssLocale: {
|
|
61
|
+
'zh-CN': '当前语言 RSS', 'zh-Hant': '目前語言 RSS', 'en-US': 'Current language RSS',
|
|
62
|
+
'ja-JP': '現在の言語 RSS', 'ko-KR': '현재 언어 RSS', 'de-DE': 'RSS der aktuellen Sprache',
|
|
63
|
+
'fr-FR': 'Flux RSS de la langue actuelle', 'es-ES': 'RSS del idioma actual',
|
|
64
|
+
'pt-BR': 'Feed RSS do idioma atual', 'ru-RU': 'RSS для текущего языка',
|
|
65
|
+
},
|
|
66
|
+
costCalculator: {
|
|
67
|
+
'zh-CN': '算一算,AI会不会淘汰我', 'zh-Hant': '算一算,AI 會不會淘汰我',
|
|
68
|
+
'en-US': 'Will AI Replace Me?', 'ja-JP': 'AI に置き換えられるか診断',
|
|
69
|
+
'ko-KR': 'AI가 나를 대체할까?', 'de-DE': 'Wird KI mich ersetzen?',
|
|
70
|
+
'fr-FR': "L'IA va-t-elle me remplacer ?", 'es-ES': 'Me reemplazará la IA?',
|
|
71
|
+
'pt-BR': 'A IA vai me substituir?', 'ru-RU': 'Заменит ли меня ИИ?',
|
|
72
|
+
},
|
|
73
|
+
github: {
|
|
74
|
+
'zh-CN': 'GitHub', 'zh-Hant': 'GitHub', 'en-US': 'GitHub', 'ja-JP': 'GitHub',
|
|
75
|
+
'ko-KR': 'GitHub', 'de-DE': 'GitHub', 'fr-FR': 'GitHub', 'es-ES': 'GitHub',
|
|
76
|
+
'pt-BR': 'GitHub', 'ru-RU': 'GitHub',
|
|
77
|
+
},
|
|
78
|
+
discord: {
|
|
79
|
+
'zh-CN': 'Discord', 'zh-Hant': 'Discord', 'en-US': 'Discord', 'ja-JP': 'Discord',
|
|
80
|
+
'ko-KR': 'Discord', 'de-DE': 'Discord', 'fr-FR': 'Discord', 'es-ES': 'Discord',
|
|
81
|
+
'pt-BR': 'Discord', 'ru-RU': 'Discord',
|
|
82
|
+
},
|
|
83
|
+
issueFeedback: {
|
|
84
|
+
'zh-CN': '问题反馈', 'zh-Hant': '問題回饋', 'en-US': 'Issue Feedback',
|
|
85
|
+
'ja-JP': '問題の報告', 'ko-KR': '문제 신고', 'de-DE': 'Probleme melden',
|
|
86
|
+
'fr-FR': 'Signaler un problème', 'es-ES': 'Reportar un problema',
|
|
87
|
+
'pt-BR': 'Relatar problema', 'ru-RU': 'Сообщить о проблеме',
|
|
88
|
+
},
|
|
89
|
+
contactEmail: {
|
|
90
|
+
'zh-CN': '联系邮箱', 'zh-Hant': '聯絡信箱', 'en-US': 'Contact Email',
|
|
91
|
+
'ja-JP': '連絡先メール', 'ko-KR': '문의 이메일', 'de-DE': 'Kontakt-E-Mail',
|
|
92
|
+
'fr-FR': 'E-mail de contact', 'es-ES': 'Correo de contacto',
|
|
93
|
+
'pt-BR': 'E-mail de contato', 'ru-RU': 'Контактный email',
|
|
94
|
+
},
|
|
95
|
+
qqGroup: {
|
|
96
|
+
'zh-CN': 'QQ 群 610394020', 'zh-Hant': 'QQ 群 610394020',
|
|
97
|
+
'en-US': 'QQ Group 610394020', 'ja-JP': 'QQ グループ 610394020',
|
|
98
|
+
'ko-KR': 'QQ 그룹 610394020', 'de-DE': 'QQ-Gruppe 610394020',
|
|
99
|
+
'fr-FR': 'Groupe QQ 610394020', 'es-ES': 'Grupo QQ 610394020',
|
|
100
|
+
'pt-BR': 'Grupo QQ 610394020', 'ru-RU': 'Группа QQ 610394020',
|
|
101
|
+
},
|
|
102
|
+
microsoftStore: {
|
|
103
|
+
'zh-CN': '下载 Hagicode Windows 版本', 'zh-Hant': '下載 Hagicode Windows 版本',
|
|
104
|
+
'en-US': 'Download Hagicode for Windows', 'ja-JP': 'Hagicode for Windows をダウンロード',
|
|
105
|
+
'ko-KR': 'Windows용 Hagicode 다운로드', 'de-DE': 'Hagicode für Windows herunterladen',
|
|
106
|
+
'fr-FR': 'Télécharger Hagicode pour Windows', 'es-ES': 'Descargar Hagicode para Windows',
|
|
107
|
+
'pt-BR': 'Baixar Hagicode para Windows', 'ru-RU': 'Скачать Hagicode для Windows',
|
|
108
|
+
},
|
|
109
|
+
icpFiling: {
|
|
110
|
+
'zh-CN': '闽ICP备2026004153号-1',
|
|
111
|
+
'zh-Hant': '闽ICP备2026004153号-1',
|
|
112
|
+
'en-US': 'ICP Filing 闽ICP备2026004153号-1',
|
|
113
|
+
'ja-JP': 'ICP届出 闽ICP备2026004153号-1',
|
|
114
|
+
'ko-KR': 'ICP 신고 闽ICP备2026004153号-1',
|
|
115
|
+
'de-DE': 'ICP-Registrierung 闽ICP备2026004153号-1',
|
|
116
|
+
'fr-FR': 'Dépôt ICP 闽ICP备2026004153号-1',
|
|
117
|
+
'es-ES': 'Registro ICP 闽ICP备2026004153号-1',
|
|
118
|
+
'pt-BR': 'Registro ICP 闽ICP备2026004153号-1',
|
|
119
|
+
'ru-RU': 'ICP регистрация 闽ICP备2026004153号-1',
|
|
120
|
+
},
|
|
121
|
+
publicSecurityFiling: {
|
|
122
|
+
'zh-CN': '闽公网安备35011102351148号',
|
|
123
|
+
'zh-Hant': '闽公网安备35011102351148号',
|
|
124
|
+
'en-US': 'Public Security Filing 闽公网安备35011102351148号',
|
|
125
|
+
'ja-JP': '公安届出 闽公网安备35011102351148号',
|
|
126
|
+
'ko-KR': '공안 신고 闽公网安备35011102351148号',
|
|
127
|
+
'de-DE': 'Sicherheitsregistrierung 闽公网安备35011102351148号',
|
|
128
|
+
'fr-FR': 'Dépôt de sécurité publique 闽公网安备35011102351148号',
|
|
129
|
+
'es-ES': 'Registro de seguridad pública 闽公网安备35011102351148号',
|
|
130
|
+
'pt-BR': 'Registro de segurança pública 闽公网安备35011102351148号',
|
|
131
|
+
'ru-RU': 'Регистрация в органах общественной безопасности 闽公网安备35011102351148号',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const defaultLinks = {
|
|
135
|
+
home: { label: translations.home, href: (locale) => marketingPath(locale, '/') },
|
|
136
|
+
blog: { label: translations.blog, href: (locale) => docsPath(locale, '/blog/') },
|
|
137
|
+
support: { label: translations.support, href: (locale) => marketingPath(locale, '/about/') },
|
|
138
|
+
downloadClient: { label: translations.downloadClient, href: (locale) => marketingPath(locale, '/desktop/') },
|
|
139
|
+
about: { label: translations.about, href: (locale) => marketingPath(locale, '/about/') },
|
|
140
|
+
dockerCompose: { label: translations.dockerCompose, href: (locale) => docsPath(locale, '/installation/docker-compose/') },
|
|
141
|
+
productDocs: { label: translations.productDocs, href: (locale) => docsPath(locale, '/product-overview/') },
|
|
142
|
+
blogPosts: { label: translations.blogPosts, href: (locale) => docsPath(locale, '/blog/') },
|
|
143
|
+
sitemap: { label: translations.sitemap, href: '/sitemap-index.xml' },
|
|
144
|
+
rss: { label: translations.rss, href: '' },
|
|
145
|
+
rssLocale: { label: translations.rssLocale, href: '' },
|
|
146
|
+
costCalculator: { label: translations.costCalculator, href: 'https://cost.hagicode.com', external: true },
|
|
147
|
+
github: { label: translations.github, href: 'https://github.com/HagiCode-org/site', external: true },
|
|
148
|
+
discord: { label: translations.discord, href: 'https://discord.gg/qY662sJK', external: true },
|
|
149
|
+
issueFeedback: { label: translations.issueFeedback, href: 'https://github.com/HagiCode-org/site/issues', external: true },
|
|
150
|
+
contactEmail: { label: translations.contactEmail, href: 'mailto:support@hagicode.com' },
|
|
151
|
+
qqGroup: { label: translations.qqGroup, href: 'https://qm.qq.com/q/Fwb0o094kw', external: true },
|
|
152
|
+
microsoftStore: {
|
|
153
|
+
label: translations.microsoftStore,
|
|
154
|
+
href: 'https://apps.microsoft.com/detail/9N3PM0N3SVDW',
|
|
155
|
+
external: true,
|
|
156
|
+
},
|
|
157
|
+
icpFiling: {
|
|
158
|
+
label: translations.icpFiling,
|
|
159
|
+
href: 'https://beian.miit.gov.cn/',
|
|
160
|
+
external: true,
|
|
161
|
+
ariaLabel: {
|
|
162
|
+
'zh-CN': '查看 ICP 备案信息', 'zh-Hant': '查看 ICP 備案資訊',
|
|
163
|
+
'en-US': 'View ICP filing information', 'ja-JP': 'ICP 登録情報を表示',
|
|
164
|
+
'ko-KR': 'ICP 등록 정보 보기', 'de-DE': 'ICP-Registrierungsinformationen anzeigen',
|
|
165
|
+
'fr-FR': 'Voir les informations de dépôt ICP', 'es-ES': 'Ver la información de registro ICP',
|
|
166
|
+
'pt-BR': 'Ver informações de registro ICP', 'ru-RU': 'Показать информацию о регистрации ICP',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
publicSecurityFiling: {
|
|
170
|
+
label: translations.publicSecurityFiling,
|
|
171
|
+
href: 'http://www.beian.gov.cn/portal/registerSystemInfo',
|
|
172
|
+
external: true,
|
|
173
|
+
ariaLabel: {
|
|
174
|
+
'zh-CN': '查看公安备案信息', 'zh-Hant': '查看公安備案資訊',
|
|
175
|
+
'en-US': 'View public security filing information', 'ja-JP': '公安登録情報を表示',
|
|
176
|
+
'ko-KR': '공안 등록 정보 보기', 'de-DE': 'Informationen zur Sicherheitsregistrierung anzeigen',
|
|
177
|
+
'fr-FR': 'Voir les informations de dépôt de sécurité publique',
|
|
178
|
+
'es-ES': 'Ver la información de registro de seguridad pública',
|
|
179
|
+
'pt-BR': 'Ver informações de registro de segurança pública',
|
|
180
|
+
'ru-RU': 'Показать информацию о регистрации в органах общественной безопасности',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
const groups = {
|
|
185
|
+
header: ['home', 'blog', 'support'],
|
|
186
|
+
quick: [
|
|
187
|
+
'downloadClient', 'microsoftStore', 'dockerCompose', 'productDocs', 'blogPosts',
|
|
188
|
+
'rss', 'rssLocale', 'sitemap', 'about',
|
|
189
|
+
],
|
|
190
|
+
community: ['github', 'discord', 'issueFeedback', 'contactEmail', 'qqGroup'],
|
|
191
|
+
filings: ['icpFiling', 'publicSecurityFiling'],
|
|
192
|
+
};
|
|
193
|
+
function normalizeLocale(locale) {
|
|
194
|
+
const normalized = locale?.trim().replaceAll('_', '-').toLowerCase();
|
|
195
|
+
if (normalized === 'zh' || normalized === 'zh-cn' || normalized === 'zh-hans' || normalized === 'root') {
|
|
196
|
+
return 'zh-CN';
|
|
197
|
+
}
|
|
198
|
+
if (normalized === 'zh-hant' || normalized === 'zh-tw' || normalized === 'zh-hk') {
|
|
199
|
+
return 'zh-Hant';
|
|
200
|
+
}
|
|
201
|
+
if (normalized === 'en' || normalized === 'en-us' || !normalized) {
|
|
202
|
+
return 'en-US';
|
|
203
|
+
}
|
|
204
|
+
const canonical = ['ja-JP', 'ko-KR', 'de-DE', 'fr-FR', 'es-ES', 'pt-BR', 'ru-RU']
|
|
205
|
+
.find((candidate) => candidate.toLowerCase() === normalized);
|
|
206
|
+
if (canonical)
|
|
207
|
+
return canonical;
|
|
208
|
+
return 'en-US';
|
|
209
|
+
}
|
|
210
|
+
function docsPath(locale, pathname) {
|
|
211
|
+
const routePrefix = locale === 'zh-CN' ? '' : `/${locale}`;
|
|
212
|
+
return `https://docs.hagicode.com${routePrefix}${pathname}`;
|
|
213
|
+
}
|
|
214
|
+
function marketingPath(locale, pathname) {
|
|
215
|
+
return `https://www.hagicode.com/${locale}${pathname}`;
|
|
216
|
+
}
|
|
217
|
+
function resolveLocalized(value, locale, fallback) {
|
|
218
|
+
if (typeof value === 'string')
|
|
219
|
+
return value;
|
|
220
|
+
for (const candidate of [locale, ...(locale === 'zh-Hant' ? ['zh-CN', 'en-US'] : ['en-US'])]) {
|
|
221
|
+
const text = value[candidate];
|
|
222
|
+
if (typeof text === 'string' && text.trim())
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
const firstValue = Object.values(value).find((text) => typeof text === 'string' && text.trim());
|
|
226
|
+
return firstValue ?? fallback;
|
|
227
|
+
}
|
|
228
|
+
function normalizeUrl(value) {
|
|
229
|
+
const input = value.trim();
|
|
230
|
+
if (!input)
|
|
231
|
+
throw new TypeError('Link destinations must not be empty.');
|
|
232
|
+
const url = new URL(input, 'https://hagilight.invalid');
|
|
233
|
+
if (!['http:', 'https:', 'mailto:', 'tel:'].includes(url.protocol)) {
|
|
234
|
+
throw new TypeError(`Unsupported link protocol: ${url.protocol}`);
|
|
235
|
+
}
|
|
236
|
+
url.hash = '';
|
|
237
|
+
url.search = '';
|
|
238
|
+
url.pathname = url.pathname.replace(/\/+$/u, '') || '/';
|
|
239
|
+
return url.origin === 'https://hagilight.invalid' ? url.pathname : url.toString();
|
|
240
|
+
}
|
|
241
|
+
function resolveLink(key, locale, definition, override) {
|
|
242
|
+
const defaultHref = typeof definition.href === 'function' ? definition.href(locale) : definition.href;
|
|
243
|
+
const href = override?.href === undefined ? defaultHref : resolveLocalized(override.href, locale, defaultHref);
|
|
244
|
+
const label = override?.label === undefined
|
|
245
|
+
? resolveLocalized(definition.label, locale, key)
|
|
246
|
+
: resolveLocalized(override.label, locale, resolveLocalized(definition.label, locale, key));
|
|
247
|
+
normalizeUrl(href);
|
|
248
|
+
const external = override?.external ?? definition.external ?? false;
|
|
249
|
+
return {
|
|
250
|
+
id: key,
|
|
251
|
+
label,
|
|
252
|
+
href,
|
|
253
|
+
...(definition.ariaLabel
|
|
254
|
+
? { ariaLabel: resolveLocalized(definition.ariaLabel, locale, label) }
|
|
255
|
+
: {}),
|
|
256
|
+
...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function resolveSiteLinks(localeInput, options = {}) {
|
|
260
|
+
const locale = normalizeLocale(localeInput);
|
|
261
|
+
if (options.rssFeedUrl !== undefined && typeof options.rssFeedUrl !== 'string') {
|
|
262
|
+
throw new TypeError('RSS feed URL must be a string.');
|
|
263
|
+
}
|
|
264
|
+
const resolvedGroups = Object.fromEntries(Object.keys(groups).map((group) => {
|
|
265
|
+
const catalogLinks = groups[group].map((key) => {
|
|
266
|
+
if (key === 'rss' && options.rssFeedUrl === undefined
|
|
267
|
+
&& options.overrides?.rss?.href === undefined) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
if (key === 'rssLocale' && options.rssLocaleFeedUrl === undefined) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
if ((group === 'quick' || group === 'community')
|
|
274
|
+
&& (options.removeLinks?.[group]?.includes(key)
|
|
275
|
+
|| key === 'rssLocale' && options.removeLinks?.quick?.includes('rss'))) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const override = options.overrides?.[key]
|
|
279
|
+
?? (key === 'blogPosts' ? options.overrides?.blog : undefined);
|
|
280
|
+
const definition = key === 'rss'
|
|
281
|
+
? { ...defaultLinks[key], href: options.rssFeedUrl ?? '' }
|
|
282
|
+
: key === 'rssLocale'
|
|
283
|
+
? { ...defaultLinks[key], href: options.rssLocaleFeedUrl ?? '' }
|
|
284
|
+
: defaultLinks[key];
|
|
285
|
+
return resolveLink(key, locale, definition, override);
|
|
286
|
+
});
|
|
287
|
+
const extraLinks = (options.extraLinks?.[group] ?? []).map((entry, index) => {
|
|
288
|
+
const definition = {
|
|
289
|
+
label: entry.label,
|
|
290
|
+
href: resolveLocalized(entry.href, locale, ''),
|
|
291
|
+
external: entry.external,
|
|
292
|
+
};
|
|
293
|
+
const id = entry.id ?? `${group}-custom-${index + 1}`;
|
|
294
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
295
|
+
throw new TypeError('Extra link IDs must be non-empty strings.');
|
|
296
|
+
}
|
|
297
|
+
return resolveLink(id, locale, definition);
|
|
298
|
+
});
|
|
299
|
+
const links = [...catalogLinks, ...extraLinks].filter((link) => link !== undefined);
|
|
300
|
+
if (group !== 'quick' && group !== 'community')
|
|
301
|
+
return [group, links];
|
|
302
|
+
const ids = new Set();
|
|
303
|
+
const urls = new Set();
|
|
304
|
+
const uniqueLinks = links.filter((link) => {
|
|
305
|
+
const url = normalizeUrl(link.href);
|
|
306
|
+
if (ids.has(link.id) || urls.has(url))
|
|
307
|
+
return false;
|
|
308
|
+
ids.add(link.id);
|
|
309
|
+
urls.add(url);
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
return [group, uniqueLinks];
|
|
313
|
+
}));
|
|
314
|
+
const renderedUrls = new Set([...resolvedGroups.quick, ...resolvedGroups.community, ...resolvedGroups.filings]
|
|
315
|
+
.map((link) => normalizeUrl(link.href)));
|
|
316
|
+
if (options.siteUrl)
|
|
317
|
+
renderedUrls.add(normalizeUrl(options.siteUrl));
|
|
318
|
+
const relatedIds = new Set();
|
|
319
|
+
const relatedUrls = new Set();
|
|
320
|
+
const bundledOrReplacementSites = (options.relatedSites ?? docsRelatedSites)
|
|
321
|
+
.filter((site) => !options.removeLinks?.relatedSites?.includes(site.id));
|
|
322
|
+
const relatedSites = [...bundledOrReplacementSites, ...(options.extraLinks?.relatedSites ?? [])]
|
|
323
|
+
.flatMap((site) => {
|
|
324
|
+
if (typeof site.id !== 'string' || !site.id.trim()) {
|
|
325
|
+
throw new TypeError('Related site IDs must be non-empty strings.');
|
|
326
|
+
}
|
|
327
|
+
const href = site.supportsLocalePath
|
|
328
|
+
? new URL(`${locale}/`, site.url.endsWith('/') ? site.url : `${site.url}/`).toString()
|
|
329
|
+
: site.url;
|
|
330
|
+
const normalizedUrl = normalizeUrl(href);
|
|
331
|
+
if (site.id === options.siteId || renderedUrls.has(normalizedUrl)
|
|
332
|
+
|| relatedIds.has(site.id) || relatedUrls.has(normalizedUrl)) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
relatedIds.add(site.id);
|
|
336
|
+
relatedUrls.add(normalizedUrl);
|
|
337
|
+
return [{
|
|
338
|
+
id: site.id,
|
|
339
|
+
name: resolveLocalized(site.name, locale, site.id),
|
|
340
|
+
description: site.description
|
|
341
|
+
? resolveLocalized(site.description, locale, '')
|
|
342
|
+
: undefined,
|
|
343
|
+
href,
|
|
344
|
+
target: '_blank',
|
|
345
|
+
rel: 'noopener noreferrer',
|
|
346
|
+
}];
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
locale,
|
|
350
|
+
labels: {
|
|
351
|
+
relatedSites: resolveLocalized({
|
|
352
|
+
'zh-CN': '生态站点', 'zh-Hant': '生態站點', 'en-US': 'Ecosystem Sites',
|
|
353
|
+
'ja-JP': 'エコシステムサイト', 'ko-KR': '에코시스템 사이트',
|
|
354
|
+
'de-DE': 'Ökosystem-Seiten', 'fr-FR': "Sites de l'écosystème",
|
|
355
|
+
'es-ES': 'Sitios del ecosistema', 'pt-BR': 'Sites do ecossistema',
|
|
356
|
+
'ru-RU': 'Сайты экосистемы',
|
|
357
|
+
}, locale, 'Ecosystem Sites'),
|
|
358
|
+
quickLinks: resolveLocalized({
|
|
359
|
+
'zh-CN': '快速链接', 'zh-Hant': '快速連結', 'en-US': 'Quick Links',
|
|
360
|
+
'ja-JP': 'クイックリンク', 'ko-KR': '빠른 링크', 'de-DE': 'Schnellzugriffe',
|
|
361
|
+
'fr-FR': 'Liens rapides', 'es-ES': 'Enlaces rápidos',
|
|
362
|
+
'pt-BR': 'Links rápidos', 'ru-RU': 'Быстрые ссылки',
|
|
363
|
+
}, locale, 'Quick Links'),
|
|
364
|
+
community: resolveLocalized({
|
|
365
|
+
'zh-CN': '社区', 'zh-Hant': '社群', 'en-US': 'Community',
|
|
366
|
+
'ja-JP': 'コミュニティ', 'ko-KR': '커뮤니티', 'de-DE': 'Community',
|
|
367
|
+
'fr-FR': 'Communauté', 'es-ES': 'Comunidad',
|
|
368
|
+
'pt-BR': 'Comunidade', 'ru-RU': 'Сообщество',
|
|
369
|
+
}, locale, 'Community'),
|
|
370
|
+
navigation: {
|
|
371
|
+
relatedSites: resolveLocalized({
|
|
372
|
+
'zh-CN': '生态站点链接', 'zh-Hant': '生態站點連結', 'en-US': 'Ecosystem site links',
|
|
373
|
+
'ja-JP': 'エコシステムサイトへのリンク', 'ko-KR': '에코시스템 사이트 링크',
|
|
374
|
+
'de-DE': 'Links zu Ökosystem-Seiten', 'fr-FR': "Liens vers les sites de l'écosystème",
|
|
375
|
+
'es-ES': 'Enlaces de sitios del ecosistema', 'pt-BR': 'Links dos sites do ecossistema',
|
|
376
|
+
'ru-RU': 'Ссылки на сайты экосистемы',
|
|
377
|
+
}, locale, 'Ecosystem site links'),
|
|
378
|
+
quickLinks: resolveLocalized({
|
|
379
|
+
'zh-CN': '快速链接', 'zh-Hant': '快速連結', 'en-US': 'Quick links',
|
|
380
|
+
'ja-JP': 'クイックリンク', 'ko-KR': '빠른 링크', 'de-DE': 'Schnellzugriffe',
|
|
381
|
+
'fr-FR': 'Liens rapides', 'es-ES': 'Enlaces rápidos',
|
|
382
|
+
'pt-BR': 'Links rápidos', 'ru-RU': 'Быстрые ссылки',
|
|
383
|
+
}, locale, 'Quick links'),
|
|
384
|
+
community: resolveLocalized({
|
|
385
|
+
'zh-CN': '社区链接', 'zh-Hant': '社群連結', 'en-US': 'Community links',
|
|
386
|
+
'ja-JP': 'コミュニティリンク', 'ko-KR': '커뮤니티 링크',
|
|
387
|
+
'de-DE': 'Community-Links', 'fr-FR': 'Liens communautaires',
|
|
388
|
+
'es-ES': 'Enlaces de la comunidad', 'pt-BR': 'Links da comunidade',
|
|
389
|
+
'ru-RU': 'Ссылки сообщества',
|
|
390
|
+
}, locale, 'Community links'),
|
|
391
|
+
filings: resolveLocalized({
|
|
392
|
+
'zh-CN': '查看备案信息', 'zh-Hant': '查看備案資訊',
|
|
393
|
+
'en-US': 'View filing information', 'ja-JP': '登録情報を表示',
|
|
394
|
+
'ko-KR': '등록 정보 보기', 'de-DE': 'Registrierungsinformationen anzeigen',
|
|
395
|
+
'fr-FR': 'Voir les informations de dépôt', 'es-ES': 'Ver información de registro',
|
|
396
|
+
'pt-BR': 'Ver informações de registro', 'ru-RU': 'Показать информацию о регистрации',
|
|
397
|
+
}, locale, 'View filing information'),
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
header: resolvedGroups.header,
|
|
401
|
+
quick: resolvedGroups.quick,
|
|
402
|
+
community: resolvedGroups.community,
|
|
403
|
+
filings: resolvedGroups.filings,
|
|
404
|
+
relatedSites,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type FetchLike = typeof fetch;
|
|
2
|
+
export interface PromotionImage {
|
|
3
|
+
src: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
variant?: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PromotionCard {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
ctaLabel: string;
|
|
14
|
+
link: string;
|
|
15
|
+
image?: PromotionImage;
|
|
16
|
+
}
|
|
17
|
+
interface PromotionFlag {
|
|
18
|
+
id: string;
|
|
19
|
+
on: boolean;
|
|
20
|
+
startTime?: string;
|
|
21
|
+
endTime?: string;
|
|
22
|
+
}
|
|
23
|
+
interface PromotionContent {
|
|
24
|
+
id: string;
|
|
25
|
+
title: Record<string, string>;
|
|
26
|
+
description: Record<string, string>;
|
|
27
|
+
cta?: Record<string, string>;
|
|
28
|
+
link: string;
|
|
29
|
+
image?: PromotionImage;
|
|
30
|
+
}
|
|
31
|
+
export declare function parsePromotionFlags(payload: unknown): PromotionFlag[];
|
|
32
|
+
export declare function parsePromotionContent(payload: unknown): PromotionContent[];
|
|
33
|
+
export declare function normalizeActivePromotions(flags: PromotionFlag[], content: PromotionContent[], locale?: string | null, now?: number): PromotionCard[];
|
|
34
|
+
export declare function resolvePromotionDocumentUrls(fetchImpl?: FetchLike): Promise<{
|
|
35
|
+
flagsUrl: string;
|
|
36
|
+
contentUrl: string;
|
|
37
|
+
source: 'catalog' | 'fallback';
|
|
38
|
+
}>;
|
|
39
|
+
export declare function loadActivePromotions(options?: {
|
|
40
|
+
locale?: string | null;
|
|
41
|
+
fetchImpl?: FetchLike;
|
|
42
|
+
now?: number;
|
|
43
|
+
}): Promise<PromotionCard[]>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const INDEX_ORIGIN = 'https://index.hagicode.com';
|
|
2
|
+
const CATALOG_URL = `${INDEX_ORIGIN}/index-catalog.json`;
|
|
3
|
+
const FLAGS_URL = `${INDEX_ORIGIN}/promote.json`;
|
|
4
|
+
const CONTENT_URL = `${INDEX_ORIGIN}/promote_content.json`;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function nonEmptyString(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
10
|
+
}
|
|
11
|
+
function parseLocalizedMap(value) {
|
|
12
|
+
if (!isRecord(value))
|
|
13
|
+
return null;
|
|
14
|
+
const entries = Object.entries(value).filter((entry) => nonEmptyString(entry[0]) && nonEmptyString(entry[1]));
|
|
15
|
+
return entries.length ? Object.fromEntries(entries.map(([key, text]) => [key.trim(), text.trim()])) : null;
|
|
16
|
+
}
|
|
17
|
+
function parseImage(value, record) {
|
|
18
|
+
const image = isRecord(value) ? value : {};
|
|
19
|
+
const src = nonEmptyString(value)
|
|
20
|
+
? value.trim()
|
|
21
|
+
: nonEmptyString(image.src)
|
|
22
|
+
? image.src.trim()
|
|
23
|
+
: nonEmptyString(image.url)
|
|
24
|
+
? image.url.trim()
|
|
25
|
+
: nonEmptyString(record.imageUrl)
|
|
26
|
+
? record.imageUrl.trim()
|
|
27
|
+
: nonEmptyString(record.imageURL)
|
|
28
|
+
? record.imageURL.trim()
|
|
29
|
+
: '';
|
|
30
|
+
if (!src)
|
|
31
|
+
return undefined;
|
|
32
|
+
const width = typeof image.width === 'number' && Number.isFinite(image.width) && image.width > 0
|
|
33
|
+
? Math.round(image.width)
|
|
34
|
+
: undefined;
|
|
35
|
+
const height = typeof image.height === 'number' && Number.isFinite(image.height) && image.height > 0
|
|
36
|
+
? Math.round(image.height)
|
|
37
|
+
: undefined;
|
|
38
|
+
let normalizedSrc;
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(src, INDEX_ORIGIN);
|
|
41
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
42
|
+
return undefined;
|
|
43
|
+
normalizedSrc = url.toString();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
src: normalizedSrc,
|
|
50
|
+
alt: nonEmptyString(image.alt)
|
|
51
|
+
? image.alt.trim()
|
|
52
|
+
: nonEmptyString(record.imageAlt)
|
|
53
|
+
? record.imageAlt.trim()
|
|
54
|
+
: '',
|
|
55
|
+
variant: nonEmptyString(image.variant) ? image.variant.trim() : undefined,
|
|
56
|
+
width,
|
|
57
|
+
height,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function isSafeLink(value) {
|
|
61
|
+
try {
|
|
62
|
+
return ['http:', 'https:'].includes(new URL(value, INDEX_ORIGIN).protocol);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function parsePromotionFlags(payload) {
|
|
69
|
+
if (!isRecord(payload) || !Array.isArray(payload.promotes))
|
|
70
|
+
return [];
|
|
71
|
+
return payload.promotes.flatMap((item) => {
|
|
72
|
+
if (!isRecord(item) || !nonEmptyString(item.id) || typeof item.on !== 'boolean')
|
|
73
|
+
return [];
|
|
74
|
+
return [{
|
|
75
|
+
id: item.id.trim(),
|
|
76
|
+
on: item.on,
|
|
77
|
+
startTime: nonEmptyString(item.startTime) ? item.startTime.trim() : undefined,
|
|
78
|
+
endTime: nonEmptyString(item.endTime) ? item.endTime.trim() : undefined,
|
|
79
|
+
}];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function parsePromotionContent(payload) {
|
|
83
|
+
if (!isRecord(payload) || !Array.isArray(payload.contents))
|
|
84
|
+
return [];
|
|
85
|
+
return payload.contents.flatMap((item) => {
|
|
86
|
+
if (!isRecord(item) || !nonEmptyString(item.id) || !nonEmptyString(item.link) || !isSafeLink(item.link)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const title = parseLocalizedMap(item.title);
|
|
90
|
+
const description = parseLocalizedMap(item.description);
|
|
91
|
+
if (!title || !description)
|
|
92
|
+
return [];
|
|
93
|
+
const cta = parseLocalizedMap(item.cta);
|
|
94
|
+
const image = parseImage(item.image, item);
|
|
95
|
+
return [{
|
|
96
|
+
id: item.id.trim(),
|
|
97
|
+
title,
|
|
98
|
+
description,
|
|
99
|
+
cta: cta ?? undefined,
|
|
100
|
+
link: item.link.trim(),
|
|
101
|
+
image,
|
|
102
|
+
}];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function localeCandidates(locale) {
|
|
106
|
+
let canonical = 'en-US';
|
|
107
|
+
if (locale?.trim()) {
|
|
108
|
+
try {
|
|
109
|
+
canonical = Intl.getCanonicalLocales(locale.trim().replace(/_/g, '-'))[0] ?? 'en-US';
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
canonical = 'en-US';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const normalized = canonical.toLowerCase();
|
|
116
|
+
const candidates = [normalized, normalized.split('-')[0] ?? normalized];
|
|
117
|
+
if (normalized.startsWith('zh-hant') || ['zh-tw', 'zh-hk', 'zh-mo'].includes(normalized)) {
|
|
118
|
+
candidates.push('zh-hant', 'zh-cn', 'zh');
|
|
119
|
+
}
|
|
120
|
+
else if (normalized.startsWith('zh')) {
|
|
121
|
+
candidates.push('zh-cn', 'zh-hant', 'zh');
|
|
122
|
+
}
|
|
123
|
+
candidates.push('en-us', 'en');
|
|
124
|
+
return [...new Set(candidates)];
|
|
125
|
+
}
|
|
126
|
+
function localized(value, locale) {
|
|
127
|
+
const values = new Map(Object.entries(value).map(([key, text]) => [key.toLowerCase(), text]));
|
|
128
|
+
for (const candidate of localeCandidates(locale)) {
|
|
129
|
+
const result = values.get(candidate);
|
|
130
|
+
if (result)
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
return Object.values(value)[0] ?? null;
|
|
134
|
+
}
|
|
135
|
+
function isActive(flag, now) {
|
|
136
|
+
if (!flag.on)
|
|
137
|
+
return false;
|
|
138
|
+
const start = flag.startTime ? Date.parse(flag.startTime) : null;
|
|
139
|
+
const end = flag.endTime ? Date.parse(flag.endTime) : null;
|
|
140
|
+
if ((start !== null && !Number.isFinite(start)) || (end !== null && !Number.isFinite(end)))
|
|
141
|
+
return false;
|
|
142
|
+
if (start !== null && end !== null && start >= end)
|
|
143
|
+
return false;
|
|
144
|
+
return (start === null || now >= start) && (end === null || now < end);
|
|
145
|
+
}
|
|
146
|
+
export function normalizeActivePromotions(flags, content, locale, now = Date.now()) {
|
|
147
|
+
const contentById = new Map(content.map((item) => [item.id, item]));
|
|
148
|
+
return flags.flatMap((flag) => {
|
|
149
|
+
const item = contentById.get(flag.id);
|
|
150
|
+
if (!isActive(flag, now) || !item)
|
|
151
|
+
return [];
|
|
152
|
+
const title = localized(item.title, locale);
|
|
153
|
+
const description = localized(item.description, locale);
|
|
154
|
+
if (!title || !description)
|
|
155
|
+
return [];
|
|
156
|
+
return [{
|
|
157
|
+
id: item.id,
|
|
158
|
+
title,
|
|
159
|
+
description,
|
|
160
|
+
ctaLabel: (item.cta && localized(item.cta, locale)) || 'Learn more',
|
|
161
|
+
link: item.link,
|
|
162
|
+
image: item.image ? { ...item.image, alt: item.image.alt || title } : undefined,
|
|
163
|
+
}];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function readJson(fetchImpl, url) {
|
|
167
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, cache: 'no-store' });
|
|
168
|
+
if (!response.ok)
|
|
169
|
+
throw new Error(`Failed to load promotion data (${response.status})`);
|
|
170
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
|
171
|
+
if (!contentType.includes('json'))
|
|
172
|
+
throw new Error(`Expected JSON promotion data, received ${contentType || 'no content type'}`);
|
|
173
|
+
return response.json();
|
|
174
|
+
}
|
|
175
|
+
export async function resolvePromotionDocumentUrls(fetchImpl = fetch) {
|
|
176
|
+
try {
|
|
177
|
+
const catalog = await readJson(fetchImpl, CATALOG_URL);
|
|
178
|
+
if (isRecord(catalog) && Array.isArray(catalog.entries)) {
|
|
179
|
+
const entries = catalog.entries.filter(isRecord);
|
|
180
|
+
const flags = entries.find((entry) => entry.id === 'promotion-flags' && nonEmptyString(entry.path));
|
|
181
|
+
const content = entries.find((entry) => entry.id === 'promotion-content' && nonEmptyString(entry.path));
|
|
182
|
+
const flagsPath = flags?.path;
|
|
183
|
+
const contentPath = content?.path;
|
|
184
|
+
if (nonEmptyString(flagsPath) && nonEmptyString(contentPath)) {
|
|
185
|
+
const flagsUrl = new URL(flagsPath, INDEX_ORIGIN);
|
|
186
|
+
const contentUrl = new URL(contentPath, INDEX_ORIGIN);
|
|
187
|
+
if (flagsUrl.origin !== INDEX_ORIGIN || contentUrl.origin !== INDEX_ORIGIN) {
|
|
188
|
+
return { flagsUrl: FLAGS_URL, contentUrl: CONTENT_URL, source: 'fallback' };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
flagsUrl: flagsUrl.toString(),
|
|
192
|
+
contentUrl: contentUrl.toString(),
|
|
193
|
+
source: 'catalog',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// The stable canonical endpoints remain available when catalog discovery fails.
|
|
200
|
+
}
|
|
201
|
+
return { flagsUrl: FLAGS_URL, contentUrl: CONTENT_URL, source: 'fallback' };
|
|
202
|
+
}
|
|
203
|
+
export async function loadActivePromotions(options = {}) {
|
|
204
|
+
const { locale, fetchImpl = fetch, now = Date.now() } = options;
|
|
205
|
+
try {
|
|
206
|
+
const urls = await resolvePromotionDocumentUrls(fetchImpl);
|
|
207
|
+
const [flags, content] = await Promise.all([
|
|
208
|
+
readJson(fetchImpl, urls.flagsUrl),
|
|
209
|
+
readJson(fetchImpl, urls.contentUrl),
|
|
210
|
+
]);
|
|
211
|
+
return normalizeActivePromotions(parsePromotionFlags(flags), parsePromotionContent(content), locale, now);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|