@hagicode/hagilight 0.3.2-dev.55.1.1e29eba → 0.3.2-dev.57.1.ee1f17e
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/SEOHead.astro +4 -9
- package/dist/integration.d.ts +33 -0
- package/dist/integration.js +243 -0
- package/dist/rss-config.d.ts +28 -0
- package/dist/rss-config.js +1 -0
- package/dist/rss-middleware.d.ts +2 -0
- package/{rss-middleware.mjs → dist/rss-middleware.js} +2 -3
- package/dist/rss-runtime.d.ts +2 -0
- package/dist/rss-runtime.js +44 -0
- package/package.json +21 -28
- package/rss.[language].xml.ts +1 -1
- package/rss.xml.ts +1 -1
- package/Analytics51LA.astro +0 -17
- package/Copyright.astro +0 -9
- package/Footer.astro +0 -157
- package/GoogleAnalytics.astro +0 -21
- package/PromotoBanner.astro +0 -346
- package/favicon.ico +0 -0
- package/favicon.mjs +0 -76
- package/integration.mjs +0 -226
- package/logo.png +0 -0
- package/promotions.ts +0 -263
- package/promoto-banner.ts +0 -368
- package/related-sites.json +0 -306
- package/rss-footer-context.mjs +0 -51
- package/rss-locales.mjs +0 -40
- package/rss-renderer.mjs +0 -114
- package/rss-runtime.mjs +0 -45
- package/seo-schema.d.ts +0 -21
- package/seo-schema.mjs +0 -24
- package/seo-utils.mjs +0 -264
- package/site-links.ts +0 -487
package/GoogleAnalytics.astro
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
measurementId: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const { measurementId } = Astro.props;
|
|
7
|
-
if (!measurementId?.trim()) throw new Error('GoogleAnalytics requires a measurementId prop.');
|
|
8
|
-
const scriptUrl = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
{import.meta.env.PROD && (
|
|
12
|
-
<>
|
|
13
|
-
<script async src={scriptUrl}></script>
|
|
14
|
-
<script is:inline define:vars={{ measurementId }}>
|
|
15
|
-
window.dataLayer = window.dataLayer || [];
|
|
16
|
-
function gtag(){dataLayer.push(arguments);}
|
|
17
|
-
gtag('js', new Date());
|
|
18
|
-
gtag('config', measurementId);
|
|
19
|
-
</script>
|
|
20
|
-
</>
|
|
21
|
-
)}
|
package/PromotoBanner.astro
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { PromotionCard } from './promotions';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
fallback?: PromotionCard;
|
|
6
|
-
locale?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { fallback, locale = Astro.currentLocale ?? 'en' } = Astro.props;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
<hagilight-promoto-banner
|
|
13
|
-
class="hagilight-promoto"
|
|
14
|
-
data-locale={locale}
|
|
15
|
-
data-fallback={fallback ? JSON.stringify(fallback) : undefined}
|
|
16
|
-
hidden
|
|
17
|
-
>
|
|
18
|
-
<section class="hagilight-promoto__shell" data-promoto-shell hidden aria-label="Featured promotion">
|
|
19
|
-
<button
|
|
20
|
-
type="button"
|
|
21
|
-
class="hagilight-promoto__dismiss"
|
|
22
|
-
data-promoto-dismiss
|
|
23
|
-
aria-label="Dismiss promotion"
|
|
24
|
-
>×</button>
|
|
25
|
-
<div class="hagilight-promoto__viewport">
|
|
26
|
-
<div class="hagilight-promoto__track" data-promoto-track></div>
|
|
27
|
-
</div>
|
|
28
|
-
<nav class="hagilight-promoto__controls" data-promoto-controls aria-label="Promotion controls" hidden>
|
|
29
|
-
<button type="button" class="hagilight-promoto__control" data-promoto-previous aria-label="Previous promotion">‹</button>
|
|
30
|
-
<span class="hagilight-promoto__count" data-promoto-count aria-label="Promotion progress"></span>
|
|
31
|
-
<button type="button" class="hagilight-promoto__control" data-promoto-next aria-label="Next promotion">›</button>
|
|
32
|
-
<button type="button" class="hagilight-promoto__control" data-promoto-pause aria-pressed="false">Pause</button>
|
|
33
|
-
</nav>
|
|
34
|
-
<span class="hagilight-promoto__status" data-promoto-status aria-live="polite"></span>
|
|
35
|
-
</section>
|
|
36
|
-
</hagilight-promoto-banner>
|
|
37
|
-
|
|
38
|
-
<script>
|
|
39
|
-
import { definePromotoBannerElement } from './promoto-banner';
|
|
40
|
-
|
|
41
|
-
definePromotoBannerElement();
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<style is:global>
|
|
45
|
-
.hagilight-promoto {
|
|
46
|
-
display: block;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.hagilight-promoto[hidden],
|
|
50
|
-
.hagilight-promoto__shell[hidden],
|
|
51
|
-
.hagilight-promoto__controls[hidden] {
|
|
52
|
-
display: none !important;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.hagilight-promoto__shell {
|
|
56
|
-
position: fixed;
|
|
57
|
-
z-index: 40;
|
|
58
|
-
inset-inline: 0;
|
|
59
|
-
bottom: max(1rem, env(safe-area-inset-bottom));
|
|
60
|
-
width: min(72rem, calc(100vw - 1rem));
|
|
61
|
-
margin-inline: auto;
|
|
62
|
-
overflow: hidden;
|
|
63
|
-
border: 1px solid color-mix(in srgb, var(--sl-color-accent, #2563eb) 24%, var(--sl-color-gray-5, #cbd5e1));
|
|
64
|
-
border-radius: 1.25rem;
|
|
65
|
-
background: color-mix(in srgb, var(--sl-color-bg, #fff) 96%, var(--sl-color-accent-low, #dbeafe) 4%);
|
|
66
|
-
box-shadow: 0 18px 48px color-mix(in srgb, var(--sl-color-black, #000) 16%, transparent);
|
|
67
|
-
color: var(--sl-color-text, #1f2937);
|
|
68
|
-
opacity: 1;
|
|
69
|
-
visibility: visible;
|
|
70
|
-
transition: opacity 0.24s ease, visibility 0s linear;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.hagilight-promoto[data-state='footer-hidden'] .hagilight-promoto__shell {
|
|
74
|
-
opacity: 0;
|
|
75
|
-
visibility: hidden;
|
|
76
|
-
pointer-events: none;
|
|
77
|
-
transition: opacity 0.24s ease, visibility 0s linear 0.24s;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.hagilight-promoto__viewport {
|
|
81
|
-
overflow: hidden;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.hagilight-promoto__track {
|
|
85
|
-
display: flex;
|
|
86
|
-
align-items: stretch;
|
|
87
|
-
width: 100%;
|
|
88
|
-
transition: transform 0.34s ease;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.hagilight-promoto__slide {
|
|
92
|
-
display: grid;
|
|
93
|
-
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
94
|
-
align-items: center;
|
|
95
|
-
gap: 1.25rem;
|
|
96
|
-
width: 100%;
|
|
97
|
-
min-width: 100%;
|
|
98
|
-
padding: 1.25rem 4rem 1.25rem 1.25rem;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.hagilight-promoto__slide[aria-hidden='true'] {
|
|
102
|
-
pointer-events: none;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.hagilight-promoto__copy {
|
|
106
|
-
display: grid;
|
|
107
|
-
grid-template-columns: 1fr;
|
|
108
|
-
align-items: start;
|
|
109
|
-
gap: 0.4rem;
|
|
110
|
-
min-width: 0;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.hagilight-promoto__title,
|
|
114
|
-
.hagilight-promoto__description {
|
|
115
|
-
display: -webkit-box;
|
|
116
|
-
overflow: hidden;
|
|
117
|
-
margin: 0;
|
|
118
|
-
color: var(--sl-color-text-high, var(--sl-color-gray-1, #111827));
|
|
119
|
-
font-size: var(--sl-text-sm);
|
|
120
|
-
font-weight: 600;
|
|
121
|
-
line-height: 1.35;
|
|
122
|
-
-webkit-box-orient: vertical;
|
|
123
|
-
-webkit-line-clamp: 2;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.hagilight-promoto__description {
|
|
127
|
-
color: var(--sl-color-text, #1f2937);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.hagilight-promoto__media {
|
|
131
|
-
display: inline-flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
justify-content: center;
|
|
134
|
-
width: clamp(4.5rem, 8vw, 6.25rem);
|
|
135
|
-
min-width: clamp(4.5rem, 8vw, 6.25rem);
|
|
136
|
-
aspect-ratio: 1;
|
|
137
|
-
overflow: hidden;
|
|
138
|
-
border: 1px solid color-mix(in srgb, var(--sl-color-accent-high, #1d4ed8) 18%, var(--sl-color-gray-5, #cbd5e1));
|
|
139
|
-
border-radius: 0.8rem;
|
|
140
|
-
background: color-mix(in srgb, var(--sl-color-bg-sidebar, #f8fafc) 82%, transparent);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.hagilight-promoto__image {
|
|
144
|
-
display: block;
|
|
145
|
-
width: 100%;
|
|
146
|
-
height: 100%;
|
|
147
|
-
object-fit: cover;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.hagilight-promoto__cta {
|
|
151
|
-
display: inline-flex;
|
|
152
|
-
align-items: center;
|
|
153
|
-
justify-content: center;
|
|
154
|
-
min-width: 7.5rem;
|
|
155
|
-
min-height: 2.6rem;
|
|
156
|
-
border: 1px solid color-mix(in srgb, var(--sl-color-accent-high, #1d4ed8) 30%, transparent);
|
|
157
|
-
border-radius: 999px;
|
|
158
|
-
background: var(--sl-color-accent, #2563eb);
|
|
159
|
-
color: #fff;
|
|
160
|
-
font-size: var(--sl-text-xs);
|
|
161
|
-
font-weight: 700;
|
|
162
|
-
padding-inline: 1rem;
|
|
163
|
-
text-align: center;
|
|
164
|
-
text-decoration: none;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.hagilight-promoto__cta:hover,
|
|
168
|
-
.hagilight-promoto__cta:focus-visible {
|
|
169
|
-
background: var(--sl-color-accent-high, #1d4ed8);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.hagilight-promoto__dismiss,
|
|
173
|
-
.hagilight-promoto__control {
|
|
174
|
-
display: inline-flex;
|
|
175
|
-
align-items: center;
|
|
176
|
-
justify-content: center;
|
|
177
|
-
min-width: 2.5rem;
|
|
178
|
-
min-height: 2.5rem;
|
|
179
|
-
border: 1px solid color-mix(in srgb, var(--sl-color-accent-high, #1d4ed8) 20%, var(--sl-color-gray-5, #cbd5e1));
|
|
180
|
-
border-radius: 999px;
|
|
181
|
-
background: var(--sl-color-bg-nav, #f8fafc);
|
|
182
|
-
color: var(--sl-color-text-high, #111827);
|
|
183
|
-
cursor: pointer;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.hagilight-promoto__dismiss {
|
|
187
|
-
position: absolute;
|
|
188
|
-
z-index: 1;
|
|
189
|
-
inset-block-start: 0.6rem;
|
|
190
|
-
inset-inline-end: 0.6rem;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.hagilight-promoto__controls {
|
|
194
|
-
display: flex;
|
|
195
|
-
align-items: center;
|
|
196
|
-
justify-content: flex-end;
|
|
197
|
-
gap: 0.5rem;
|
|
198
|
-
padding: 0 1.25rem 1rem;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.hagilight-promoto__count {
|
|
202
|
-
min-width: 3.5rem;
|
|
203
|
-
text-align: center;
|
|
204
|
-
font-size: var(--sl-text-xs);
|
|
205
|
-
font-variant-numeric: tabular-nums;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.hagilight-promoto__control:disabled {
|
|
209
|
-
cursor: default;
|
|
210
|
-
opacity: 0.6;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.hagilight-promoto__status {
|
|
214
|
-
position: absolute;
|
|
215
|
-
width: 1px;
|
|
216
|
-
height: 1px;
|
|
217
|
-
overflow: hidden;
|
|
218
|
-
clip: rect(0, 0, 0, 0);
|
|
219
|
-
white-space: nowrap;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.hagilight-promoto__dismiss:focus-visible,
|
|
223
|
-
.hagilight-promoto__control:focus-visible,
|
|
224
|
-
.hagilight-promoto__cta:focus-visible {
|
|
225
|
-
outline: 3px solid var(--sl-color-accent-high, #1d4ed8);
|
|
226
|
-
outline-offset: 3px;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
@media (max-width: 960px) {
|
|
230
|
-
.hagilight-promoto__slide {
|
|
231
|
-
grid-template-columns: 1fr;
|
|
232
|
-
gap: 0.75rem;
|
|
233
|
-
padding-inline-end: 3.7rem;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.hagilight-promoto__slide[data-has-image='true'] {
|
|
237
|
-
grid-template-columns: 1fr;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.hagilight-promoto__copy {
|
|
241
|
-
grid-template-columns: 1fr;
|
|
242
|
-
gap: 0.45rem;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.hagilight-promoto__media {
|
|
246
|
-
width: min(100%, 8rem);
|
|
247
|
-
min-width: 0;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.hagilight-promoto__cta {
|
|
251
|
-
justify-self: start;
|
|
252
|
-
white-space: normal;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.hagilight-promoto__controls {
|
|
256
|
-
justify-content: flex-start;
|
|
257
|
-
padding-top: 0;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
@media (max-width: 720px) {
|
|
262
|
-
.hagilight-promoto__shell {
|
|
263
|
-
bottom: calc(env(safe-area-inset-bottom) + 0.5rem);
|
|
264
|
-
width: min(calc(100vw - 1rem), 24rem);
|
|
265
|
-
border-radius: 1rem;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
.hagilight-promoto__slide {
|
|
269
|
-
grid-template-columns: 1fr;
|
|
270
|
-
gap: 0.7rem;
|
|
271
|
-
padding: 0.85rem 4.8rem 0.85rem 0.85rem;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
.hagilight-promoto__slide[data-has-image='true'] {
|
|
275
|
-
grid-template-columns: minmax(0, 1fr) 4.5rem;
|
|
276
|
-
align-items: start;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
.hagilight-promoto__copy {
|
|
280
|
-
grid-column: 1;
|
|
281
|
-
grid-row: 1;
|
|
282
|
-
gap: 0.45rem;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.hagilight-promoto__title {
|
|
286
|
-
font-size: 0.92rem;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
.hagilight-promoto__description {
|
|
290
|
-
font-size: 0.84rem;
|
|
291
|
-
font-weight: 500;
|
|
292
|
-
line-height: 1.3;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
.hagilight-promoto__media {
|
|
296
|
-
width: 4.5rem;
|
|
297
|
-
min-width: 4.5rem;
|
|
298
|
-
align-self: start;
|
|
299
|
-
grid-column: 2;
|
|
300
|
-
grid-row: 1;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.hagilight-promoto__cta {
|
|
304
|
-
grid-column: 1 / -1;
|
|
305
|
-
justify-self: stretch;
|
|
306
|
-
min-width: 0;
|
|
307
|
-
min-height: 2.35rem;
|
|
308
|
-
width: 100%;
|
|
309
|
-
font-size: 0.82rem;
|
|
310
|
-
white-space: normal;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
.hagilight-promoto__control {
|
|
314
|
-
min-width: 2.2rem;
|
|
315
|
-
min-height: 2.2rem;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.hagilight-promoto__count {
|
|
319
|
-
min-width: 2.75rem;
|
|
320
|
-
font-size: 0.75rem;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.hagilight-promoto__dismiss {
|
|
324
|
-
inset-block-start: 0.55rem;
|
|
325
|
-
inset-inline-end: 0.55rem;
|
|
326
|
-
width: auto;
|
|
327
|
-
min-width: 2rem;
|
|
328
|
-
height: 2rem;
|
|
329
|
-
gap: 0.25rem;
|
|
330
|
-
padding-inline: 0.55rem;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.hagilight-promoto__controls {
|
|
334
|
-
flex-wrap: wrap;
|
|
335
|
-
gap: 0.35rem;
|
|
336
|
-
padding: 0 0.85rem 0.9rem;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
@media (prefers-reduced-motion: reduce) {
|
|
341
|
-
.hagilight-promoto__shell,
|
|
342
|
-
.hagilight-promoto__track {
|
|
343
|
-
transition: none;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
</style>
|
package/favicon.ico
DELETED
|
Binary file
|
package/favicon.mjs
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
const faviconPath = fileURLToPath(new URL('./favicon.ico', import.meta.url));
|
|
5
|
-
let cachedDataUri;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resolve the bundled HagiCode favicon as an inline data URI.
|
|
9
|
-
* Memoized so the binary is read at most once per process.
|
|
10
|
-
*/
|
|
11
|
-
export function getHagilightFaviconDataUri() {
|
|
12
|
-
if (cachedDataUri === undefined) {
|
|
13
|
-
const bytes = readFileSync(faviconPath);
|
|
14
|
-
cachedDataUri = `data:image/x-icon;base64,${bytes.toString('base64')}`;
|
|
15
|
-
}
|
|
16
|
-
return cachedDataUri;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isIconLink(entry) {
|
|
20
|
-
return entry?.tag === 'link'
|
|
21
|
-
&& typeof entry.attrs?.rel === 'string'
|
|
22
|
-
&& /\bicon\b/u.test(entry.attrs.rel);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function headHasIconLink(head) {
|
|
26
|
-
return (head ?? []).some(isIconLink);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve the favicon `<link>` head entry to inject, or `undefined` to skip.
|
|
31
|
-
*
|
|
32
|
-
* Shared by both the `hagilightFavicon` integration and the Starlight plugin so the
|
|
33
|
-
* favicon behavior has a single source of truth.
|
|
34
|
-
*
|
|
35
|
-
* Override options:
|
|
36
|
-
* - Pass `{ href: '/your-favicon.ico' }` to use a consumer-provided favicon instead.
|
|
37
|
-
* - If `head` already contains an icon link, injection is skipped (so manual favicons
|
|
38
|
-
* are never duplicated).
|
|
39
|
-
*
|
|
40
|
-
* @param {Array<{tag:string, attrs?:Record<string,string>}>} head existing head entries
|
|
41
|
-
* @param {{href?:string}} [options] optional override href
|
|
42
|
-
*/
|
|
43
|
-
export function resolveFaviconHeadEntry(head = [], options = {}) {
|
|
44
|
-
if (headHasIconLink(head)) return undefined;
|
|
45
|
-
const overrideHref = typeof options.href === 'string' && options.href.trim()
|
|
46
|
-
? options.href.trim()
|
|
47
|
-
: undefined;
|
|
48
|
-
const href = overrideHref ?? getHagilightFaviconDataUri();
|
|
49
|
-
const attrs = { rel: 'icon', href };
|
|
50
|
-
if (overrideHref === undefined) attrs.type = 'image/x-icon';
|
|
51
|
-
return { tag: 'link', attrs };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Astro integration that auto-applies the bundled HagiCode favicon.
|
|
56
|
-
*
|
|
57
|
-
* Any site referencing `@hagicode/hagilight` can add `integrations: [hagilightFavicon()]`
|
|
58
|
-
* to get the shared favicon with no further configuration.
|
|
59
|
-
*/
|
|
60
|
-
export function hagilightFavicon(options = {}) {
|
|
61
|
-
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
|
|
62
|
-
throw new TypeError('Hagilight favicon options must be an object.');
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
name: '@hagicode/hagilight:favicon',
|
|
66
|
-
hooks: {
|
|
67
|
-
'astro:config:setup'({ config, updateConfig }) {
|
|
68
|
-
const entry = resolveFaviconHeadEntry(config.head, options);
|
|
69
|
-
if (!entry) return;
|
|
70
|
-
updateConfig({
|
|
71
|
-
head: [...(config.head ?? []), entry],
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
package/integration.mjs
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { dirname, extname, isAbsolute, resolve, win32 } from 'node:path';
|
|
4
|
-
import sitemap from '@astrojs/sitemap';
|
|
5
|
-
import { resolveRssLocales } from './rss-locales.mjs';
|
|
6
|
-
|
|
7
|
-
export const HAGILIGHT_RSS_OWNER = Symbol.for('@hagicode/hagilight/rss-owner');
|
|
8
|
-
const VIRTUAL_RSS_CONFIG = 'virtual:hagilight/rss-config';
|
|
9
|
-
const VIRTUAL_RSS_CONFIG_RESOLVED = `\0${VIRTUAL_RSS_CONFIG}`;
|
|
10
|
-
const RSS_OWNER_REGISTRY = Symbol.for('@hagicode/hagilight/rss-owner-registry');
|
|
11
|
-
const PAGE_SOURCE_EXTENSIONS = new Set(['.astro', '.html', '.js', '.jsx', '.md', '.mdx', '.ts', '.tsx']);
|
|
12
|
-
const registeredRssOwners = globalThis[RSS_OWNER_REGISTRY] ?? new Map();
|
|
13
|
-
globalThis[RSS_OWNER_REGISTRY] = registeredRssOwners;
|
|
14
|
-
|
|
15
|
-
export function hagilight({ enabled = true } = {}) {
|
|
16
|
-
if (typeof enabled !== 'boolean') {
|
|
17
|
-
throw new TypeError('Hagilight sitemap and robots enabled option must be a boolean.');
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
name: '@hagicode/hagilight:discovery',
|
|
21
|
-
hooks: {
|
|
22
|
-
'astro:config:setup'({ config, injectRoute, updateConfig }) {
|
|
23
|
-
if (!enabled) return;
|
|
24
|
-
if (!config.site) {
|
|
25
|
-
throw new Error('Hagilight sitemap and robots require an absolute Astro site URL. Set `site` in astro.config.mjs.');
|
|
26
|
-
}
|
|
27
|
-
const integrations = config.integrations ?? [];
|
|
28
|
-
if (!integrations.some(({ name }) => name === '@astrojs/sitemap' || name === '@astrojs/starlight')) {
|
|
29
|
-
updateConfig({ integrations: [sitemap()] });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const pageRoot = resolve(fileURLToPath(config.srcDir), 'pages');
|
|
33
|
-
const publicRoot = fileURLToPath(config.publicDir);
|
|
34
|
-
const hasRobotsPage = existsSync(pageRoot) && readdirSync(pageRoot).some((name) =>
|
|
35
|
-
/^robots\.txt\.(?:astro|html|js|jsx|md|mdx|ts|tsx)$/u.test(name)
|
|
36
|
-
|| name === 'robots.txt');
|
|
37
|
-
if (!hasRobotsPage && !existsSync(resolve(publicRoot, 'robots.txt'))) {
|
|
38
|
-
injectRoute({
|
|
39
|
-
pattern: '/robots.txt',
|
|
40
|
-
entrypoint: fileURLToPath(new URL('./robots.txt.ts', import.meta.url)),
|
|
41
|
-
prerender: true,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function registerStarlightRssOwner(enabled) {
|
|
50
|
-
const token = Symbol('starlight-rss-owner');
|
|
51
|
-
registeredRssOwners.set(token, { package: 'starlight', enabled });
|
|
52
|
-
return () => registeredRssOwners.delete(token);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resolveOptions(options) {
|
|
56
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
57
|
-
throw new TypeError('Hagilight RSS integration options must be an object.');
|
|
58
|
-
}
|
|
59
|
-
const locales = resolveRssLocales(options.locales, { requireNonEmpty: true });
|
|
60
|
-
if (typeof options.getFeed !== 'string' || !options.getFeed.trim()) {
|
|
61
|
-
throw new TypeError('Hagilight RSS getFeed must be a non-empty module path such as "./src/rss-feed.mjs".');
|
|
62
|
-
}
|
|
63
|
-
return { locales, getFeed: options.getFeed.trim() };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function resolveSite(site) {
|
|
67
|
-
let parsed;
|
|
68
|
-
try {
|
|
69
|
-
parsed = new URL(site);
|
|
70
|
-
} catch {
|
|
71
|
-
throw new Error('Hagilight RSS requires an absolute Astro site URL. Set `site` in astro.config.mjs.');
|
|
72
|
-
}
|
|
73
|
-
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) {
|
|
74
|
-
throw new Error('Hagilight RSS requires an absolute HTTP(S) Astro site URL.');
|
|
75
|
-
}
|
|
76
|
-
return parsed.href;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function resolveFeedModule(root, reference) {
|
|
80
|
-
if (reference.startsWith('file:') || isAbsolute(reference) || win32.isAbsolute(reference)) {
|
|
81
|
-
throw new TypeError('Hagilight RSS getFeed must be a path relative to the Astro project root.');
|
|
82
|
-
}
|
|
83
|
-
const rootPath = fileURLToPath(root);
|
|
84
|
-
const modulePath = resolve(rootPath, reference);
|
|
85
|
-
if (!existsSync(modulePath) || !statSync(modulePath).isFile()) {
|
|
86
|
-
throw new Error(`Hagilight RSS getFeed module "${reference}" was not found relative to the Astro project root.`);
|
|
87
|
-
}
|
|
88
|
-
return modulePath;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function routeCollisions(config, locales) {
|
|
92
|
-
const expectedFiles = new Set(['rss.xml', 'rss.en.xml', ...locales.map(({ filename }) => `rss.${filename}.xml`)]);
|
|
93
|
-
const pageRoot = resolve(fileURLToPath(config.srcDir), 'pages');
|
|
94
|
-
const publicRoot = fileURLToPath(config.publicDir);
|
|
95
|
-
const collisions = [];
|
|
96
|
-
|
|
97
|
-
function findPageCollisions(directory) {
|
|
98
|
-
if (!existsSync(directory)) return;
|
|
99
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
100
|
-
const entryPath = resolve(directory, entry.name);
|
|
101
|
-
if (entry.isDirectory()) {
|
|
102
|
-
if (entry.name === 'rss.xml' || /^rss\..+\.xml$/u.test(entry.name)) {
|
|
103
|
-
for (const child of readdirSync(entryPath, { withFileTypes: true })) {
|
|
104
|
-
if (child.isFile()
|
|
105
|
-
&& child.name.startsWith('index.')
|
|
106
|
-
&& PAGE_SOURCE_EXTENSIONS.has(extname(child.name))) {
|
|
107
|
-
collisions.push(resolve(entryPath, child.name));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
findPageCollisions(entryPath);
|
|
112
|
-
} else if (entry.isFile()) {
|
|
113
|
-
const extension = extname(entry.name);
|
|
114
|
-
if (!PAGE_SOURCE_EXTENSIONS.has(extension)) continue;
|
|
115
|
-
const sourceName = entry.name.slice(0, -extension.length);
|
|
116
|
-
if (expectedFiles.has(sourceName) || /^rss\..+\.xml$/u.test(sourceName)) {
|
|
117
|
-
collisions.push(entryPath);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
findPageCollisions(pageRoot);
|
|
123
|
-
|
|
124
|
-
for (const filename of expectedFiles) {
|
|
125
|
-
const publicFile = resolve(publicRoot, filename);
|
|
126
|
-
if (existsSync(publicFile) && statSync(publicFile).isFile()) collisions.push(publicFile);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (collisions.length > 0) {
|
|
130
|
-
throw new Error(`Hagilight RSS route conflicts with consumer-owned file(s): ${collisions.join(', ')}. Remove the conflicting route or do not enable hagilightRss().`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function findStarlightRss(config) {
|
|
135
|
-
const registeredOwners = [...registeredRssOwners.values()];
|
|
136
|
-
const owners = registeredOwners.length > 0
|
|
137
|
-
? registeredOwners
|
|
138
|
-
: (config.integrations ?? [])
|
|
139
|
-
.map((integration) => integration?.[HAGILIGHT_RSS_OWNER])
|
|
140
|
-
.filter((owner) => owner?.package === 'starlight');
|
|
141
|
-
registeredRssOwners.clear();
|
|
142
|
-
return owners;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUrl, localeFeedUrls }) {
|
|
146
|
-
return {
|
|
147
|
-
name: '@hagicode/hagilight:rss',
|
|
148
|
-
resolveId(id) {
|
|
149
|
-
return id === VIRTUAL_RSS_CONFIG ? VIRTUAL_RSS_CONFIG_RESOLVED : null;
|
|
150
|
-
},
|
|
151
|
-
load(id) {
|
|
152
|
-
if (id !== VIRTUAL_RSS_CONFIG_RESOLVED) return null;
|
|
153
|
-
return [
|
|
154
|
-
`import * as siteFeedModule from ${JSON.stringify(modulePath)};`,
|
|
155
|
-
'const config = {',
|
|
156
|
-
` site: ${JSON.stringify(site)},`,
|
|
157
|
-
` baseUrl: ${JSON.stringify(baseUrl)},`,
|
|
158
|
-
` locales: ${JSON.stringify(locales)},`,
|
|
159
|
-
' getFeed: siteFeedModule.default,',
|
|
160
|
-
` footer: ${JSON.stringify({ defaultFeedUrl, localeFeedUrls, locales })},`,
|
|
161
|
-
'};',
|
|
162
|
-
'export default config;',
|
|
163
|
-
].join('\n');
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function hagilightRss(input) {
|
|
169
|
-
const options = resolveOptions(input);
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
name: '@hagicode/hagilight:rss',
|
|
173
|
-
[HAGILIGHT_RSS_OWNER]: { package: 'core', enabled: true },
|
|
174
|
-
hooks: {
|
|
175
|
-
'astro:config:setup'({ config, injectRoute, addMiddleware, updateConfig }) {
|
|
176
|
-
const starlightOwners = findStarlightRss(config);
|
|
177
|
-
const activeStarlightOwners = starlightOwners.filter((owner) => owner.enabled);
|
|
178
|
-
if (activeStarlightOwners.length > 1) {
|
|
179
|
-
throw new Error('Hagilight RSS cannot determine a sole route owner because multiple Starlight RSS integrations are enabled.');
|
|
180
|
-
}
|
|
181
|
-
if (activeStarlightOwners.length > 0) return;
|
|
182
|
-
if (starlightOwners.length > 0) {
|
|
183
|
-
throw new Error('Hagilight RSS cannot establish route ownership: Starlight RSS is explicitly disabled while hagilightRss() requests feed generation. Enable Starlight RSS or remove hagilightRss().');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const site = resolveSite(config.site);
|
|
187
|
-
const modulePath = resolveFeedModule(config.root, options.getFeed);
|
|
188
|
-
routeCollisions(config, options.locales);
|
|
189
|
-
const baseSegments = `${config.base || '/'}`
|
|
190
|
-
.split('/')
|
|
191
|
-
.filter(Boolean);
|
|
192
|
-
const baseUrl = baseSegments.length > 0 ? `/${baseSegments.join('/')}/` : '/';
|
|
193
|
-
const baseLocation = new URL(baseUrl, site);
|
|
194
|
-
const defaultFeedUrl = new URL('rss.xml', baseLocation).href;
|
|
195
|
-
const localeFeedUrls = Object.fromEntries(options.locales
|
|
196
|
-
.filter(({ filename }) => filename !== 'en')
|
|
197
|
-
.map(({ lang, filename }) => [lang, new URL(`rss.${filename}.xml`, baseLocation).href]));
|
|
198
|
-
const rssIntegrationPath = dirname(fileURLToPath(import.meta.url));
|
|
199
|
-
const plugin = createVirtualPlugin({
|
|
200
|
-
modulePath,
|
|
201
|
-
site,
|
|
202
|
-
baseUrl,
|
|
203
|
-
locales: options.locales,
|
|
204
|
-
defaultFeedUrl,
|
|
205
|
-
localeFeedUrls,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
injectRoute({
|
|
209
|
-
pattern: '/rss.xml',
|
|
210
|
-
entrypoint: resolve(rssIntegrationPath, './rss.xml.ts'),
|
|
211
|
-
prerender: true,
|
|
212
|
-
});
|
|
213
|
-
injectRoute({
|
|
214
|
-
pattern: '/rss.[language].xml',
|
|
215
|
-
entrypoint: resolve(rssIntegrationPath, './rss.[language].xml.ts'),
|
|
216
|
-
prerender: true,
|
|
217
|
-
});
|
|
218
|
-
addMiddleware({
|
|
219
|
-
entrypoint: resolve(rssIntegrationPath, './rss-middleware.mjs'),
|
|
220
|
-
order: 'pre',
|
|
221
|
-
});
|
|
222
|
-
updateConfig({ vite: { plugins: [plugin] } });
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
package/logo.png
DELETED
|
Binary file
|