@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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PromotionCard } from './promotions.js';
|
|
2
|
+
export type PromotionVisibility = 'dismissed' | 'footer-hidden' | 'hidden' | 'ready';
|
|
3
|
+
export declare function selectPromotionCards(remote: PromotionCard[], fallback?: PromotionCard | null): PromotionCard[];
|
|
4
|
+
export declare function getPromotionSetSignature(cards: PromotionCard[]): string | null;
|
|
5
|
+
export declare function getPromotionVisibility(cards: PromotionCard[], signature: string | null, dismissedSignature: string | null, footerVisible: boolean): PromotionVisibility;
|
|
6
|
+
export declare function getNextPromotionIndex(index: number, count: number, direction: -1 | 1): number;
|
|
7
|
+
export declare function shouldAutoRotate(count: number, paused: boolean, reducedMotion: boolean, documentVisible: boolean, visibility: PromotionVisibility): boolean;
|
|
8
|
+
export declare function definePromotoBannerElement(): void;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { loadActivePromotions } from './promotions.js';
|
|
2
|
+
const ELEMENT_NAME = 'hagilight-promoto-banner';
|
|
3
|
+
const DISMISSED_SIGNATURE_KEY = 'hagilight:promoto-banner:dismissed-signature';
|
|
4
|
+
const ROTATION_INTERVAL_MS = 8000;
|
|
5
|
+
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function isSafeUrl(value) {
|
|
10
|
+
try {
|
|
11
|
+
return ['http:', 'https:'].includes(new URL(value, 'https://hagilight.invalid/').protocol);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isUsableCard(value) {
|
|
18
|
+
if (!isRecord(value))
|
|
19
|
+
return false;
|
|
20
|
+
const image = value.image;
|
|
21
|
+
const imageIsValid = image === undefined || (isRecord(image) && typeof image.src === 'string' && isSafeUrl(image.src)
|
|
22
|
+
&& typeof image.alt === 'string'
|
|
23
|
+
&& (image.variant === undefined || typeof image.variant === 'string')
|
|
24
|
+
&& (image.width === undefined || (typeof image.width === 'number' && Number.isFinite(image.width) && image.width > 0))
|
|
25
|
+
&& (image.height === undefined || (typeof image.height === 'number' && Number.isFinite(image.height) && image.height > 0)));
|
|
26
|
+
return typeof value.id === 'string' && value.id.trim().length > 0
|
|
27
|
+
&& typeof value.title === 'string' && value.title.trim().length > 0
|
|
28
|
+
&& typeof value.description === 'string' && value.description.trim().length > 0
|
|
29
|
+
&& typeof value.ctaLabel === 'string' && value.ctaLabel.trim().length > 0
|
|
30
|
+
&& typeof value.link === 'string' && value.link.trim().length > 0 && isSafeUrl(value.link)
|
|
31
|
+
&& imageIsValid;
|
|
32
|
+
}
|
|
33
|
+
export function selectPromotionCards(remote, fallback) {
|
|
34
|
+
const eligibleRemote = remote.filter(isUsableCard);
|
|
35
|
+
if (eligibleRemote.length)
|
|
36
|
+
return eligibleRemote;
|
|
37
|
+
return fallback && isUsableCard(fallback) ? [fallback] : [];
|
|
38
|
+
}
|
|
39
|
+
export function getPromotionSetSignature(cards) {
|
|
40
|
+
if (!cards.length)
|
|
41
|
+
return null;
|
|
42
|
+
return JSON.stringify(cards.map(({ id, title, description, ctaLabel, link, image }) => [
|
|
43
|
+
id, title, description, ctaLabel, link, image?.src ?? '', image?.alt ?? '',
|
|
44
|
+
image?.variant ?? '', image?.width ?? 0, image?.height ?? 0,
|
|
45
|
+
]));
|
|
46
|
+
}
|
|
47
|
+
export function getPromotionVisibility(cards, signature, dismissedSignature, footerVisible) {
|
|
48
|
+
if (!cards.length)
|
|
49
|
+
return 'hidden';
|
|
50
|
+
if (signature && signature === dismissedSignature)
|
|
51
|
+
return 'dismissed';
|
|
52
|
+
return footerVisible ? 'footer-hidden' : 'ready';
|
|
53
|
+
}
|
|
54
|
+
export function getNextPromotionIndex(index, count, direction) {
|
|
55
|
+
return count > 0 ? (index + direction + count) % count : 0;
|
|
56
|
+
}
|
|
57
|
+
export function shouldAutoRotate(count, paused, reducedMotion, documentVisible, visibility) {
|
|
58
|
+
return count > 1 && !paused && !reducedMotion && documentVisible && visibility === 'ready';
|
|
59
|
+
}
|
|
60
|
+
function readDismissedSignature() {
|
|
61
|
+
try {
|
|
62
|
+
return window.localStorage.getItem(DISMISSED_SIGNATURE_KEY);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function saveDismissedSignature(signature) {
|
|
69
|
+
try {
|
|
70
|
+
window.localStorage.setItem(DISMISSED_SIGNATURE_KEY, signature);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.warn('Unable to save the dismissed promotion preference.', error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function definePromotoBannerElement() {
|
|
77
|
+
if (typeof customElements === 'undefined' || typeof HTMLElement === 'undefined'
|
|
78
|
+
|| customElements.get(ELEMENT_NAME))
|
|
79
|
+
return;
|
|
80
|
+
class PromotoBannerElement extends HTMLElement {
|
|
81
|
+
cards = [];
|
|
82
|
+
currentIndex = 0;
|
|
83
|
+
dismissedSignature = readDismissedSignature();
|
|
84
|
+
paused = false;
|
|
85
|
+
reducedMotion = false;
|
|
86
|
+
rotationTimer;
|
|
87
|
+
refreshTimer;
|
|
88
|
+
motionQuery;
|
|
89
|
+
resizeObserver;
|
|
90
|
+
footer = null;
|
|
91
|
+
loadedLocale = null;
|
|
92
|
+
onPrevious = () => this.move(-1);
|
|
93
|
+
onNext = () => this.move(1);
|
|
94
|
+
onPause = () => {
|
|
95
|
+
if (this.reducedMotion)
|
|
96
|
+
return;
|
|
97
|
+
this.paused = !this.paused;
|
|
98
|
+
this.updateControls();
|
|
99
|
+
this.syncRotation();
|
|
100
|
+
};
|
|
101
|
+
onDismiss = () => {
|
|
102
|
+
const signature = getPromotionSetSignature(this.cards);
|
|
103
|
+
if (!signature)
|
|
104
|
+
return;
|
|
105
|
+
this.dismissedSignature = signature;
|
|
106
|
+
saveDismissedSignature(signature);
|
|
107
|
+
this.updateVisibility();
|
|
108
|
+
this.stopRotation();
|
|
109
|
+
};
|
|
110
|
+
onLayoutChange = () => {
|
|
111
|
+
const previousState = this.dataset.state;
|
|
112
|
+
this.updateVisibility();
|
|
113
|
+
if (previousState !== this.dataset.state)
|
|
114
|
+
this.syncRotation();
|
|
115
|
+
};
|
|
116
|
+
onVisibilityChange = () => {
|
|
117
|
+
if (document.visibilityState === 'hidden') {
|
|
118
|
+
this.stopRotation();
|
|
119
|
+
this.stopRefresh();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.syncRotation();
|
|
123
|
+
this.startRefresh();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
onMotionChange = () => {
|
|
127
|
+
this.reducedMotion = this.motionQuery?.matches ?? false;
|
|
128
|
+
this.updateControls();
|
|
129
|
+
this.syncRotation();
|
|
130
|
+
};
|
|
131
|
+
onPageLoad = () => {
|
|
132
|
+
// Re-fetch in the current page locale after a Starlight view-transition
|
|
133
|
+
// navigation. The element may persist across a soft language switch, so
|
|
134
|
+
// its previously loaded locale can differ from the new page's data-locale.
|
|
135
|
+
if (this.dataset.locale !== this.loadedLocale)
|
|
136
|
+
void this.reload();
|
|
137
|
+
};
|
|
138
|
+
connectedCallback() {
|
|
139
|
+
this.footer = document.querySelector('footer');
|
|
140
|
+
this.motionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
141
|
+
this.reducedMotion = this.motionQuery?.matches ?? false;
|
|
142
|
+
this.querySelector('[data-promoto-previous]')?.addEventListener('click', this.onPrevious);
|
|
143
|
+
this.querySelector('[data-promoto-next]')?.addEventListener('click', this.onNext);
|
|
144
|
+
this.querySelector('[data-promoto-pause]')?.addEventListener('click', this.onPause);
|
|
145
|
+
this.querySelector('[data-promoto-dismiss]')?.addEventListener('click', this.onDismiss);
|
|
146
|
+
window.addEventListener('scroll', this.onLayoutChange, { passive: true });
|
|
147
|
+
window.addEventListener('resize', this.onLayoutChange);
|
|
148
|
+
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
|
149
|
+
this.motionQuery?.addEventListener('change', this.onMotionChange);
|
|
150
|
+
document.addEventListener('astro:page-load', this.onPageLoad);
|
|
151
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
152
|
+
this.resizeObserver = new ResizeObserver(this.onLayoutChange);
|
|
153
|
+
if (this.footer)
|
|
154
|
+
this.resizeObserver.observe(this.footer);
|
|
155
|
+
const shell = this.querySelector('[data-promoto-shell]');
|
|
156
|
+
if (shell)
|
|
157
|
+
this.resizeObserver.observe(shell);
|
|
158
|
+
}
|
|
159
|
+
void this.reload();
|
|
160
|
+
this.startRefresh();
|
|
161
|
+
}
|
|
162
|
+
disconnectedCallback() {
|
|
163
|
+
this.querySelector('[data-promoto-previous]')?.removeEventListener('click', this.onPrevious);
|
|
164
|
+
this.querySelector('[data-promoto-next]')?.removeEventListener('click', this.onNext);
|
|
165
|
+
this.querySelector('[data-promoto-pause]')?.removeEventListener('click', this.onPause);
|
|
166
|
+
this.querySelector('[data-promoto-dismiss]')?.removeEventListener('click', this.onDismiss);
|
|
167
|
+
window.removeEventListener('scroll', this.onLayoutChange);
|
|
168
|
+
window.removeEventListener('resize', this.onLayoutChange);
|
|
169
|
+
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
|
170
|
+
this.motionQuery?.removeEventListener('change', this.onMotionChange);
|
|
171
|
+
document.removeEventListener('astro:page-load', this.onPageLoad);
|
|
172
|
+
this.resizeObserver?.disconnect();
|
|
173
|
+
this.stopRotation();
|
|
174
|
+
this.stopRefresh();
|
|
175
|
+
}
|
|
176
|
+
async reload() {
|
|
177
|
+
const previousId = this.cards[this.currentIndex]?.id;
|
|
178
|
+
this.loadedLocale = this.dataset.locale ?? null;
|
|
179
|
+
const remote = await loadActivePromotions({ locale: this.dataset.locale });
|
|
180
|
+
if (!this.isConnected)
|
|
181
|
+
return;
|
|
182
|
+
const fallback = this.readFallback();
|
|
183
|
+
this.cards = selectPromotionCards(remote, fallback);
|
|
184
|
+
const previousIndex = previousId ? this.cards.findIndex((card) => card.id === previousId) : -1;
|
|
185
|
+
this.currentIndex = previousIndex >= 0 ? previousIndex : 0;
|
|
186
|
+
this.renderCards();
|
|
187
|
+
this.updateVisibility();
|
|
188
|
+
this.updateControls();
|
|
189
|
+
this.syncRotation();
|
|
190
|
+
}
|
|
191
|
+
readFallback() {
|
|
192
|
+
const serialized = this.dataset.fallback;
|
|
193
|
+
if (!serialized)
|
|
194
|
+
return null;
|
|
195
|
+
try {
|
|
196
|
+
const value = JSON.parse(serialized);
|
|
197
|
+
return isUsableCard(value) ? value : null;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
console.warn('Unable to parse the site-provided promotion fallback.', error);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
renderCards() {
|
|
205
|
+
const track = this.querySelector('[data-promoto-track]');
|
|
206
|
+
if (!track)
|
|
207
|
+
return;
|
|
208
|
+
const fragment = document.createDocumentFragment();
|
|
209
|
+
this.cards.forEach((card, index) => {
|
|
210
|
+
const slide = document.createElement('article');
|
|
211
|
+
slide.className = 'hagilight-promoto__slide';
|
|
212
|
+
slide.dataset.hasImage = String(Boolean(card.image));
|
|
213
|
+
slide.setAttribute('aria-hidden', index === this.currentIndex ? 'false' : 'true');
|
|
214
|
+
if (index !== this.currentIndex)
|
|
215
|
+
slide.setAttribute('inert', '');
|
|
216
|
+
const copy = document.createElement('div');
|
|
217
|
+
copy.className = 'hagilight-promoto__copy';
|
|
218
|
+
const title = document.createElement('h2');
|
|
219
|
+
title.className = 'hagilight-promoto__title';
|
|
220
|
+
title.textContent = card.title;
|
|
221
|
+
const description = document.createElement('p');
|
|
222
|
+
description.className = 'hagilight-promoto__description';
|
|
223
|
+
description.textContent = card.description;
|
|
224
|
+
copy.append(title, description);
|
|
225
|
+
const link = document.createElement('a');
|
|
226
|
+
link.className = 'hagilight-promoto__cta';
|
|
227
|
+
link.href = card.link;
|
|
228
|
+
link.target = '_blank';
|
|
229
|
+
link.rel = 'noopener noreferrer';
|
|
230
|
+
link.textContent = card.ctaLabel;
|
|
231
|
+
slide.append(copy);
|
|
232
|
+
if (card.image) {
|
|
233
|
+
const media = document.createElement('span');
|
|
234
|
+
media.className = 'hagilight-promoto__media';
|
|
235
|
+
const image = document.createElement('img');
|
|
236
|
+
image.className = 'hagilight-promoto__image';
|
|
237
|
+
image.src = card.image.src;
|
|
238
|
+
image.alt = card.image.alt || card.title;
|
|
239
|
+
image.loading = index === this.currentIndex ? 'eager' : 'lazy';
|
|
240
|
+
image.decoding = 'async';
|
|
241
|
+
if (card.image.width)
|
|
242
|
+
image.width = card.image.width;
|
|
243
|
+
if (card.image.height)
|
|
244
|
+
image.height = card.image.height;
|
|
245
|
+
media.append(image);
|
|
246
|
+
slide.append(media);
|
|
247
|
+
}
|
|
248
|
+
slide.append(link);
|
|
249
|
+
fragment.append(slide);
|
|
250
|
+
});
|
|
251
|
+
track.replaceChildren(fragment);
|
|
252
|
+
this.updateSlides();
|
|
253
|
+
}
|
|
254
|
+
updateSlides() {
|
|
255
|
+
const track = this.querySelector('[data-promoto-track]');
|
|
256
|
+
if (!track)
|
|
257
|
+
return;
|
|
258
|
+
Array.from(track.children).forEach((slide, index) => {
|
|
259
|
+
const active = index === this.currentIndex;
|
|
260
|
+
slide.setAttribute('aria-hidden', String(!active));
|
|
261
|
+
if (active)
|
|
262
|
+
slide.removeAttribute('inert');
|
|
263
|
+
else
|
|
264
|
+
slide.setAttribute('inert', '');
|
|
265
|
+
});
|
|
266
|
+
track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
|
|
267
|
+
const current = this.cards[this.currentIndex];
|
|
268
|
+
const status = this.querySelector('[data-promoto-status]');
|
|
269
|
+
if (status)
|
|
270
|
+
status.textContent = current?.title ?? '';
|
|
271
|
+
const count = this.querySelector('[data-promoto-count]');
|
|
272
|
+
if (count)
|
|
273
|
+
count.textContent = `${this.currentIndex + 1} / ${this.cards.length}`;
|
|
274
|
+
}
|
|
275
|
+
updateControls() {
|
|
276
|
+
const multiple = this.cards.length > 1;
|
|
277
|
+
const controls = this.querySelector('[data-promoto-controls]');
|
|
278
|
+
if (controls)
|
|
279
|
+
controls.hidden = !multiple;
|
|
280
|
+
const pause = this.querySelector('[data-promoto-pause]');
|
|
281
|
+
if (pause) {
|
|
282
|
+
const paused = this.paused || this.reducedMotion;
|
|
283
|
+
pause.disabled = this.reducedMotion;
|
|
284
|
+
pause.setAttribute('aria-pressed', String(paused));
|
|
285
|
+
pause.setAttribute('aria-label', paused ? 'Resume automatic promotion rotation' : 'Pause automatic promotion rotation');
|
|
286
|
+
pause.textContent = paused ? 'Resume' : 'Pause';
|
|
287
|
+
}
|
|
288
|
+
this.updateSlides();
|
|
289
|
+
}
|
|
290
|
+
move(direction) {
|
|
291
|
+
if (this.cards.length < 2)
|
|
292
|
+
return;
|
|
293
|
+
this.currentIndex = getNextPromotionIndex(this.currentIndex, this.cards.length, direction);
|
|
294
|
+
this.updateSlides();
|
|
295
|
+
this.syncRotation();
|
|
296
|
+
}
|
|
297
|
+
updateVisibility() {
|
|
298
|
+
const signature = getPromotionSetSignature(this.cards);
|
|
299
|
+
const state = getPromotionVisibility(this.cards, signature, this.dismissedSignature, this.isFooterVisible());
|
|
300
|
+
const hidden = state === 'hidden' || state === 'dismissed';
|
|
301
|
+
const shell = this.querySelector('[data-promoto-shell]');
|
|
302
|
+
this.hidden = hidden;
|
|
303
|
+
if (shell) {
|
|
304
|
+
shell.hidden = hidden;
|
|
305
|
+
shell.inert = hidden || state === 'footer-hidden';
|
|
306
|
+
}
|
|
307
|
+
this.dataset.state = state;
|
|
308
|
+
}
|
|
309
|
+
isFooterVisible() {
|
|
310
|
+
if (!this.footer)
|
|
311
|
+
return false;
|
|
312
|
+
const bounds = this.footer.getBoundingClientRect();
|
|
313
|
+
return bounds.top < window.innerHeight && bounds.bottom > 0;
|
|
314
|
+
}
|
|
315
|
+
syncRotation() {
|
|
316
|
+
this.stopRotation();
|
|
317
|
+
const state = this.dataset.state;
|
|
318
|
+
if (state !== 'ready' && state !== 'footer-hidden' && state !== 'hidden' && state !== 'dismissed')
|
|
319
|
+
return;
|
|
320
|
+
if (!shouldAutoRotate(this.cards.length, this.paused, this.reducedMotion, document.visibilityState !== 'hidden', state))
|
|
321
|
+
return;
|
|
322
|
+
this.rotationTimer = window.setInterval(() => this.move(1), ROTATION_INTERVAL_MS);
|
|
323
|
+
}
|
|
324
|
+
stopRotation() {
|
|
325
|
+
if (this.rotationTimer !== undefined)
|
|
326
|
+
window.clearInterval(this.rotationTimer);
|
|
327
|
+
this.rotationTimer = undefined;
|
|
328
|
+
}
|
|
329
|
+
startRefresh() {
|
|
330
|
+
this.stopRefresh();
|
|
331
|
+
if (document.visibilityState !== 'hidden') {
|
|
332
|
+
this.refreshTimer = window.setInterval(() => void this.reload(), REFRESH_INTERVAL_MS);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
stopRefresh() {
|
|
336
|
+
if (this.refreshTimer !== undefined)
|
|
337
|
+
window.clearInterval(this.refreshTimer);
|
|
338
|
+
this.refreshTimer = undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
customElements.define(ELEMENT_NAME, PromotoBannerElement);
|
|
342
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "hagitask",
|
|
4
|
+
"name": {
|
|
5
|
+
"zh-CN": "HagiTask",
|
|
6
|
+
"zh-Hant": "HagiTask",
|
|
7
|
+
"en-US": "HagiTask",
|
|
8
|
+
"ja-JP": "HagiTask",
|
|
9
|
+
"ko-KR": "HagiTask",
|
|
10
|
+
"de-DE": "HagiTask",
|
|
11
|
+
"fr-FR": "HagiTask",
|
|
12
|
+
"es-ES": "HagiTask",
|
|
13
|
+
"pt-BR": "HagiTask",
|
|
14
|
+
"ru-RU": "HagiTask"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"zh-CN": "任务与工作流管理",
|
|
18
|
+
"zh-Hant": "任務與工作流程管理",
|
|
19
|
+
"en-US": "Task and workflow management.",
|
|
20
|
+
"ja-JP": "タスクとワークフローを管理します。",
|
|
21
|
+
"ko-KR": "작업과 워크플로를 관리합니다.",
|
|
22
|
+
"de-DE": "Aufgaben und Workflows verwalten.",
|
|
23
|
+
"fr-FR": "Gestion des tâches et des workflows.",
|
|
24
|
+
"es-ES": "Gestión de tareas y flujos de trabajo.",
|
|
25
|
+
"pt-BR": "Gerenciamento de tarefas e fluxos de trabalho.",
|
|
26
|
+
"ru-RU": "Управление задачами и рабочими процессами."
|
|
27
|
+
},
|
|
28
|
+
"url": "https://tasks.hagicode.com/"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "costCalculator",
|
|
32
|
+
"name": {
|
|
33
|
+
"zh-CN": "算一算,AI会不会淘汰我",
|
|
34
|
+
"zh-Hant": "算一算,AI 會不會淘汰我",
|
|
35
|
+
"en-US": "Will AI Replace Me?",
|
|
36
|
+
"ja-JP": "AI に置き換えられるか診断",
|
|
37
|
+
"ko-KR": "AI가 나를 대체할까?",
|
|
38
|
+
"de-DE": "Wird KI mich ersetzen?",
|
|
39
|
+
"fr-FR": "L'IA va-t-elle me remplacer ?",
|
|
40
|
+
"es-ES": "Me reemplazará la IA?",
|
|
41
|
+
"pt-BR": "A IA vai me substituir?",
|
|
42
|
+
"ru-RU": "Заменит ли меня ИИ?"
|
|
43
|
+
},
|
|
44
|
+
"url": "https://cost.hagicode.com"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "hagicode-main",
|
|
48
|
+
"name": {
|
|
49
|
+
"zh-CN": "HagiCode 主站",
|
|
50
|
+
"zh-Hant": "HagiCode 主站",
|
|
51
|
+
"en-US": "HagiCode Main Site",
|
|
52
|
+
"ja-JP": "HagiCode 公式サイト",
|
|
53
|
+
"ko-KR": "HagiCode 메인 사이트",
|
|
54
|
+
"de-DE": "HagiCode Hauptseite",
|
|
55
|
+
"fr-FR": "Site principal HagiCode",
|
|
56
|
+
"es-ES": "Sitio principal de HagiCode",
|
|
57
|
+
"pt-BR": "Site principal da HagiCode",
|
|
58
|
+
"ru-RU": "Главный сайт HagiCode"
|
|
59
|
+
},
|
|
60
|
+
"description": {
|
|
61
|
+
"zh-CN": "产品入口",
|
|
62
|
+
"zh-Hant": "產品入口",
|
|
63
|
+
"en-US": "Primary product entry.",
|
|
64
|
+
"ja-JP": "製品全体の入口です。",
|
|
65
|
+
"ko-KR": "제품으로 들어가는 기본 진입점입니다.",
|
|
66
|
+
"de-DE": "Zentraler Einstieg zum Produkt.",
|
|
67
|
+
"fr-FR": "Point d'entrée principal du produit.",
|
|
68
|
+
"es-ES": "Entrada principal al producto.",
|
|
69
|
+
"pt-BR": "Entrada principal do produto.",
|
|
70
|
+
"ru-RU": "Основной вход в продукт."
|
|
71
|
+
},
|
|
72
|
+
"url": "https://www.hagicode.com/",
|
|
73
|
+
"supportsLocalePath": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "newbe-blog",
|
|
77
|
+
"name": {
|
|
78
|
+
"zh-CN": "newbe",
|
|
79
|
+
"zh-Hant": "newbe",
|
|
80
|
+
"en-US": "newbe",
|
|
81
|
+
"ja-JP": "newbe",
|
|
82
|
+
"ko-KR": "newbe",
|
|
83
|
+
"de-DE": "newbe",
|
|
84
|
+
"fr-FR": "newbe",
|
|
85
|
+
"es-ES": "newbe",
|
|
86
|
+
"pt-BR": "newbe",
|
|
87
|
+
"ru-RU": "newbe"
|
|
88
|
+
},
|
|
89
|
+
"description": {
|
|
90
|
+
"zh-CN": "作者博客",
|
|
91
|
+
"zh-Hant": "作者部落格",
|
|
92
|
+
"en-US": "Author blog.",
|
|
93
|
+
"ja-JP": "作者ブログです。",
|
|
94
|
+
"ko-KR": "작성자 블로그입니다.",
|
|
95
|
+
"de-DE": "Blog des Autors.",
|
|
96
|
+
"fr-FR": "Blog de l'auteur.",
|
|
97
|
+
"es-ES": "Blog del autor.",
|
|
98
|
+
"pt-BR": "Blog do autor.",
|
|
99
|
+
"ru-RU": "Блог автора."
|
|
100
|
+
},
|
|
101
|
+
"url": "https://newbe.hagicode.com/"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "index-data",
|
|
105
|
+
"name": {
|
|
106
|
+
"zh-CN": "Index Data Mirror",
|
|
107
|
+
"zh-Hant": "Index Data Mirror",
|
|
108
|
+
"en-US": "Index Data Mirror",
|
|
109
|
+
"ja-JP": "Index Data Mirror",
|
|
110
|
+
"ko-KR": "Index Data Mirror",
|
|
111
|
+
"de-DE": "Index Data Mirror",
|
|
112
|
+
"fr-FR": "Index Data Mirror",
|
|
113
|
+
"es-ES": "Index Data Mirror",
|
|
114
|
+
"pt-BR": "Index Data Mirror",
|
|
115
|
+
"ru-RU": "Index Data Mirror"
|
|
116
|
+
},
|
|
117
|
+
"description": {
|
|
118
|
+
"zh-CN": "公开数据",
|
|
119
|
+
"zh-Hant": "公開資料",
|
|
120
|
+
"en-US": "Public data mirror.",
|
|
121
|
+
"ja-JP": "公開データミラーです。",
|
|
122
|
+
"ko-KR": "공개 데이터 미러입니다.",
|
|
123
|
+
"de-DE": "Öffentlicher Daten-Spiegel.",
|
|
124
|
+
"fr-FR": "Miroir de données public.",
|
|
125
|
+
"es-ES": "Espejo de datos público.",
|
|
126
|
+
"pt-BR": "Espelho público de dados.",
|
|
127
|
+
"ru-RU": "Публичное зеркало данных."
|
|
128
|
+
},
|
|
129
|
+
"url": "https://index.hagicode.com/data/"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "compose-builder",
|
|
133
|
+
"name": {
|
|
134
|
+
"zh-CN": "Docker Compose Builder",
|
|
135
|
+
"zh-Hant": "Docker Compose Builder",
|
|
136
|
+
"en-US": "Docker Compose Builder",
|
|
137
|
+
"ja-JP": "Docker Compose Builder",
|
|
138
|
+
"ko-KR": "Docker Compose Builder",
|
|
139
|
+
"de-DE": "Docker Compose Builder",
|
|
140
|
+
"fr-FR": "Docker Compose Builder",
|
|
141
|
+
"es-ES": "Docker Compose Builder",
|
|
142
|
+
"pt-BR": "Docker Compose Builder",
|
|
143
|
+
"ru-RU": "Docker Compose Builder"
|
|
144
|
+
},
|
|
145
|
+
"description": {
|
|
146
|
+
"zh-CN": "Docker 部署 Hagicode",
|
|
147
|
+
"zh-Hant": "使用 Docker 部署 Hagicode",
|
|
148
|
+
"en-US": "Deploy HagiCode with Docker.",
|
|
149
|
+
"ja-JP": "Docker で HagiCode をデプロイします。",
|
|
150
|
+
"ko-KR": "Docker로 HagiCode를 배포합니다.",
|
|
151
|
+
"de-DE": "HagiCode mit Docker bereitstellen.",
|
|
152
|
+
"fr-FR": "Déployer HagiCode avec Docker.",
|
|
153
|
+
"es-ES": "Despliega HagiCode con Docker.",
|
|
154
|
+
"pt-BR": "Implante a HagiCode com Docker.",
|
|
155
|
+
"ru-RU": "Развёртывание HagiCode через Docker."
|
|
156
|
+
},
|
|
157
|
+
"url": "https://builder.hagicode.com/"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "status-page",
|
|
161
|
+
"name": {
|
|
162
|
+
"zh-CN": "HagiCode Status",
|
|
163
|
+
"zh-Hant": "HagiCode Status",
|
|
164
|
+
"en-US": "HagiCode Status",
|
|
165
|
+
"ja-JP": "HagiCode Status",
|
|
166
|
+
"ko-KR": "HagiCode Status",
|
|
167
|
+
"de-DE": "HagiCode Status",
|
|
168
|
+
"fr-FR": "HagiCode Status",
|
|
169
|
+
"es-ES": "HagiCode Status",
|
|
170
|
+
"pt-BR": "HagiCode Status",
|
|
171
|
+
"ru-RU": "HagiCode Status"
|
|
172
|
+
},
|
|
173
|
+
"description": {
|
|
174
|
+
"zh-CN": "服务状态",
|
|
175
|
+
"zh-Hant": "服務狀態",
|
|
176
|
+
"en-US": "Service status.",
|
|
177
|
+
"ja-JP": "サービス状態の確認ページです。",
|
|
178
|
+
"ko-KR": "서비스 상태를 확인하는 페이지입니다.",
|
|
179
|
+
"de-DE": "Dienststatus.",
|
|
180
|
+
"fr-FR": "État des services.",
|
|
181
|
+
"es-ES": "Estado del servicio.",
|
|
182
|
+
"pt-BR": "Status do serviço.",
|
|
183
|
+
"ru-RU": "Статус сервисов."
|
|
184
|
+
},
|
|
185
|
+
"url": "https://status.hagicode.com/"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"id": "awesome-design-gallery",
|
|
189
|
+
"name": {
|
|
190
|
+
"zh-CN": "Awesome Design MD",
|
|
191
|
+
"zh-Hant": "Awesome Design MD",
|
|
192
|
+
"en-US": "Awesome Design MD",
|
|
193
|
+
"ja-JP": "Awesome Design MD",
|
|
194
|
+
"ko-KR": "Awesome Design MD",
|
|
195
|
+
"de-DE": "Awesome Design MD",
|
|
196
|
+
"fr-FR": "Awesome Design MD",
|
|
197
|
+
"es-ES": "Awesome Design MD",
|
|
198
|
+
"pt-BR": "Awesome Design MD",
|
|
199
|
+
"ru-RU": "Awesome Design MD"
|
|
200
|
+
},
|
|
201
|
+
"description": {
|
|
202
|
+
"zh-CN": "超酷的AI Web 设计",
|
|
203
|
+
"zh-Hant": "超酷的 AI Web 設計",
|
|
204
|
+
"en-US": "A gallery of standout AI web design work.",
|
|
205
|
+
"ja-JP": "印象的な AI Web デザインを集めたギャラリーです。",
|
|
206
|
+
"ko-KR": "인상적인 AI 웹 디자인을 모아 둔 갤러리입니다.",
|
|
207
|
+
"de-DE": "Galerie mit markanten AI-Webdesigns.",
|
|
208
|
+
"fr-FR": "Galerie de designs web IA remarquables.",
|
|
209
|
+
"es-ES": "Galería de diseños web de IA destacados.",
|
|
210
|
+
"pt-BR": "Galeria com designs web de IA de destaque.",
|
|
211
|
+
"ru-RU": "Галерея выразительных AI web-дизайнов."
|
|
212
|
+
},
|
|
213
|
+
"url": "https://design.hagicode.com/",
|
|
214
|
+
"supportsLocalePath": true
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"id": "soul-builder",
|
|
218
|
+
"name": {
|
|
219
|
+
"zh-CN": "Soul Builder",
|
|
220
|
+
"zh-Hant": "Soul Builder",
|
|
221
|
+
"en-US": "Soul Builder",
|
|
222
|
+
"ja-JP": "Soul Builder",
|
|
223
|
+
"ko-KR": "Soul Builder",
|
|
224
|
+
"de-DE": "Soul Builder",
|
|
225
|
+
"fr-FR": "Soul Builder",
|
|
226
|
+
"es-ES": "Soul Builder",
|
|
227
|
+
"pt-BR": "Soul Builder",
|
|
228
|
+
"ru-RU": "Soul Builder"
|
|
229
|
+
},
|
|
230
|
+
"description": {
|
|
231
|
+
"zh-CN": "Agent之魂",
|
|
232
|
+
"zh-Hant": "Agent 之魂",
|
|
233
|
+
"en-US": "Define the soul of an agent.",
|
|
234
|
+
"ja-JP": "Agent の魂を形にする体験です。",
|
|
235
|
+
"ko-KR": "에이전트의 영혼을 설계하는 경험입니다.",
|
|
236
|
+
"de-DE": "Forme die Seele eines Agenten.",
|
|
237
|
+
"fr-FR": "Façonnez l'âme d'un agent.",
|
|
238
|
+
"es-ES": "Define el alma de un agente.",
|
|
239
|
+
"pt-BR": "Defina a alma de um agente.",
|
|
240
|
+
"ru-RU": "Определите душу агента."
|
|
241
|
+
},
|
|
242
|
+
"url": "https://soul.hagicode.com/"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"id": "trait-builder",
|
|
246
|
+
"name": {
|
|
247
|
+
"zh-CN": "Trait Builder",
|
|
248
|
+
"zh-Hant": "Trait Builder",
|
|
249
|
+
"en-US": "Trait Builder",
|
|
250
|
+
"ja-JP": "Trait Builder",
|
|
251
|
+
"ko-KR": "Trait Builder",
|
|
252
|
+
"de-DE": "Trait Builder",
|
|
253
|
+
"fr-FR": "Trait Builder",
|
|
254
|
+
"es-ES": "Trait Builder",
|
|
255
|
+
"pt-BR": "Trait Builder",
|
|
256
|
+
"ru-RU": "Trait Builder"
|
|
257
|
+
},
|
|
258
|
+
"description": {
|
|
259
|
+
"zh-CN": "Agent之能",
|
|
260
|
+
"zh-Hant": "Agent 之能",
|
|
261
|
+
"en-US": "Shape what an agent can do.",
|
|
262
|
+
"ja-JP": "Agent の能力を組み立てる体験です。",
|
|
263
|
+
"ko-KR": "에이전트의 능력을 설계하는 경험입니다.",
|
|
264
|
+
"de-DE": "Definiere, was ein Agent kann.",
|
|
265
|
+
"fr-FR": "Définissez ce qu'un agent peut faire.",
|
|
266
|
+
"es-ES": "Define lo que un agente puede hacer.",
|
|
267
|
+
"pt-BR": "Defina o que um agente pode fazer.",
|
|
268
|
+
"ru-RU": "Соберите способности агента."
|
|
269
|
+
},
|
|
270
|
+
"url": "https://trait.hagicode.com/"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "openspec-docs",
|
|
274
|
+
"name": {
|
|
275
|
+
"zh-CN": "OpenSpec 文档",
|
|
276
|
+
"zh-Hant": "OpenSpec 文件",
|
|
277
|
+
"en-US": "OpenSpec Docs",
|
|
278
|
+
"ja-JP": "OpenSpec ドキュメント",
|
|
279
|
+
"ko-KR": "OpenSpec 문서",
|
|
280
|
+
"de-DE": "OpenSpec-Dokumentation",
|
|
281
|
+
"fr-FR": "Documentation OpenSpec",
|
|
282
|
+
"es-ES": "Documentación de OpenSpec",
|
|
283
|
+
"pt-BR": "Documentação do OpenSpec",
|
|
284
|
+
"ru-RU": "Документация OpenSpec"
|
|
285
|
+
},
|
|
286
|
+
"url": "https://openspec.hagicode.com/",
|
|
287
|
+
"supportsLocalePath": true
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "omniroute-docs",
|
|
291
|
+
"name": {
|
|
292
|
+
"zh-CN": "OmniRoute 文档",
|
|
293
|
+
"zh-Hant": "OmniRoute 文件",
|
|
294
|
+
"en-US": "OmniRoute Docs",
|
|
295
|
+
"ja-JP": "OmniRoute ドキュメント",
|
|
296
|
+
"ko-KR": "OmniRoute 문서",
|
|
297
|
+
"de-DE": "OmniRoute-Dokumentation",
|
|
298
|
+
"fr-FR": "Documentation OmniRoute",
|
|
299
|
+
"es-ES": "Documentación de OmniRoute",
|
|
300
|
+
"pt-BR": "Documentação do OmniRoute",
|
|
301
|
+
"ru-RU": "Документация OmniRoute"
|
|
302
|
+
},
|
|
303
|
+
"url": "https://omniroute.hagicode.com/",
|
|
304
|
+
"supportsLocalePath": true
|
|
305
|
+
}
|
|
306
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SiteLinksOptions } from './links.js';
|
|
2
|
+
/**
|
|
3
|
+
* Merge generated `hagilightRss()` feed URLs from request locals into Footer
|
|
4
|
+
* link options. Explicit `rssFeedUrl`/`rssLocaleFeedUrl` values win.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveRssFooterLinks(locals: unknown, locale: string | undefined, links?: SiteLinksOptions): SiteLinksOptions;
|