stack-site-builder 1.17.5 โ 1.19.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/CHANGELOG.md +38 -0
- package/README.md +17 -5
- package/index.d.ts +1 -0
- package/index.mjs +12 -1
- package/package.json +1 -1
- package/src/components/BlogIndex.astro +2 -1
- package/src/components/CategoryIndex.astro +9 -3
- package/src/components/ConceptIndex.astro +2 -1
- package/src/components/CourseIndex.astro +2 -1
- package/src/components/Home.astro +2 -1
- package/src/components/PaperCard.astro +85 -0
- package/src/components/PaperDetail.astro +123 -0
- package/src/components/PapersIndex.astro +144 -0
- package/src/components/ProductsIndex.astro +2 -1
- package/src/components/RelatedPapers.astro +59 -0
- package/src/components/SlidesIndex.astro +2 -1
- package/src/components/TagIndex.astro +2 -1
- package/src/components/VendorIndex.astro +2 -1
- package/src/content.ts +87 -1
- package/src/i18n/ui.ts +26 -0
- package/src/layouts/BaseLayout.astro +117 -6
- package/src/lib/listing.ts +9 -0
- package/src/lib/papers.ts +25 -0
- package/src/lib/private-client.ts +45 -1
- package/src/lib/sections.ts +2 -0
- package/src/pages/[...lang]/paper/[...id].astro +35 -0
- package/src/pages/[...lang]/paper/category/[id].astro +25 -0
- package/src/pages/[...lang]/paper/index.astro +18 -0
- package/src/pages/aas-auth.json.ts +22 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
// "Related papers" card section on a paper detail page, mirroring
|
|
3
|
+
// RelatedConcepts (title + authors/venue meta + description, collapsible grid).
|
|
4
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
|
+
import { Image } from 'astro:assets';
|
|
6
|
+
import { paperSlugOf, shortAuthors, type PaperEntry } from '../lib/papers';
|
|
7
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
8
|
+
import { inlineMd } from '../lib/inline-md';
|
|
9
|
+
import CollapsibleGrid from './CollapsibleGrid.astro';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
lang: Lang;
|
|
13
|
+
papers: PaperEntry[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { lang, papers } = Astro.props;
|
|
17
|
+
const t = useTranslations(lang);
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
papers.length > 0 && (
|
|
22
|
+
<section class="mt-8">
|
|
23
|
+
<h2 class="text-xs font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
|
|
24
|
+
{t('paper.relatedPapers')}
|
|
25
|
+
</h2>
|
|
26
|
+
<CollapsibleGrid lang={lang} count={papers.length}>
|
|
27
|
+
{papers.map((c) => (
|
|
28
|
+
<a
|
|
29
|
+
href={getRelativeLocaleUrl(lang, `paper/${paperSlugOf(c)}/`)}
|
|
30
|
+
class="aas-lift flex items-stretch overflow-hidden rounded-2xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
31
|
+
>
|
|
32
|
+
{c.data.image && (
|
|
33
|
+
<Image src={c.data.image} alt="" class="w-28 shrink-0 self-stretch object-cover" />
|
|
34
|
+
)}
|
|
35
|
+
<div class="min-w-0 p-4">
|
|
36
|
+
<span class="block font-semibold text-[var(--aas-text)]">
|
|
37
|
+
{c.data.private && (
|
|
38
|
+
<span aria-label={t('private.badge')} title={t('private.badge')}>๐ </span>
|
|
39
|
+
)}
|
|
40
|
+
{c.data.title}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="mt-0.5 block text-xs text-[var(--aas-muted)]">
|
|
43
|
+
{shortAuthors(c.data.authors)}
|
|
44
|
+
{c.data.authors.length > 0 && (c.data.venue || c.data.year) && ' ยท '}
|
|
45
|
+
{c.data.venue}
|
|
46
|
+
{c.data.venue && c.data.year && ' '}
|
|
47
|
+
{c.data.year}
|
|
48
|
+
</span>
|
|
49
|
+
<span
|
|
50
|
+
class="aas-md mt-1.5 line-clamp-2 text-xs leading-relaxed text-[var(--aas-muted)] opacity-75"
|
|
51
|
+
set:html={inlineMd(c.data.private ? (c.data.teaser ?? '') : c.data.description)}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</a>
|
|
55
|
+
))}
|
|
56
|
+
</CollapsibleGrid>
|
|
57
|
+
</section>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import { listedInIndex } from '../lib/listing';
|
|
3
4
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
4
5
|
import { getDecks, deckSlugOf } from '../lib/slides';
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ const t = useTranslations(lang);
|
|
|
12
13
|
|
|
13
14
|
// Decks are locale-partitioned (content/slides/<lang>/); list this locale's and
|
|
14
15
|
// link to its localized deck page. `related` points at the concept it summarizes.
|
|
15
|
-
const decks = await getDecks(lang);
|
|
16
|
+
const decks = (await getDecks(lang)).filter((e) => listedInIndex(e.data));
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
<section class="py-6">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
3
|
import StackCard from './StackCard.astro';
|
|
4
4
|
import { getStacksByTag, slugOf } from '../lib/stacks';
|
|
5
|
+
import { listedInIndex } from '../lib/listing';
|
|
5
6
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -11,7 +12,7 @@ interface Props {
|
|
|
11
12
|
|
|
12
13
|
const { lang, tag } = Astro.props;
|
|
13
14
|
const t = useTranslations(lang);
|
|
14
|
-
const entries = (await getStacksByTag(lang, tag)).sort((a, b) =>
|
|
15
|
+
const entries = (await getStacksByTag(lang, tag)).filter((e) => listedInIndex(e.data)).sort((a, b) =>
|
|
15
16
|
a.data.name.localeCompare(b.data.name),
|
|
16
17
|
);
|
|
17
18
|
---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
3
|
import StackCard from './StackCard.astro';
|
|
4
4
|
import { getStacksByVendor, slugOf } from '../lib/stacks';
|
|
5
|
+
import { listedInIndex } from '../lib/listing';
|
|
5
6
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -11,7 +12,7 @@ interface Props {
|
|
|
11
12
|
|
|
12
13
|
const { lang, vendor } = Astro.props;
|
|
13
14
|
const t = useTranslations(lang);
|
|
14
|
-
const entries = await getStacksByVendor(lang, vendor);
|
|
15
|
+
const entries = (await getStacksByVendor(lang, vendor)).filter((e) => listedInIndex(e.data));
|
|
15
16
|
// The display name is whatever the entries wrote (e.g. "Hugging Face").
|
|
16
17
|
const vendorName = entries[0]?.data.vendor ?? vendor;
|
|
17
18
|
---
|
package/src/content.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function defineAasCollections({
|
|
|
19
19
|
categoryMap,
|
|
20
20
|
courseCategoryMap,
|
|
21
21
|
productCategoryMap,
|
|
22
|
+
paperCategoryMap,
|
|
22
23
|
}: {
|
|
23
24
|
categoryMap: Map<string, unknown>;
|
|
24
25
|
/** The site's course category tree (`src/data/course-categories.ts`). Optional
|
|
@@ -28,6 +29,9 @@ export function defineAasCollections({
|
|
|
28
29
|
/** The site's product category tree (`src/data/product-categories.ts`).
|
|
29
30
|
* Optional like `courseCategoryMap` โ `products` is opt-in too. */
|
|
30
31
|
productCategoryMap?: Map<string, unknown>;
|
|
32
|
+
/** The site's paper category tree (`src/data/paper-categories.ts`).
|
|
33
|
+
* Optional like the others โ `papers` is opt-in too. */
|
|
34
|
+
paperCategoryMap?: Map<string, unknown>;
|
|
31
35
|
}) {
|
|
32
36
|
/**
|
|
33
37
|
* The `stacks` collection holds one entry per tool/service used to build
|
|
@@ -125,6 +129,10 @@ export function defineAasCollections({
|
|
|
125
129
|
// See docs/private-content-design.md. Requires the AAS_PRIVATE_* env vars.
|
|
126
130
|
private: z.boolean().default(false),
|
|
127
131
|
teaser: z.string().optional(),
|
|
132
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
133
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
134
|
+
// direct links / the related sections on detail pages.
|
|
135
|
+
listed: z.boolean().default(false),
|
|
128
136
|
}),
|
|
129
137
|
});
|
|
130
138
|
|
|
@@ -169,6 +177,10 @@ export function defineAasCollections({
|
|
|
169
177
|
// PUBLIC `teaser`). See docs/private-content-design.md.
|
|
170
178
|
private: z.boolean().default(false),
|
|
171
179
|
teaser: z.string().optional(),
|
|
180
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
181
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
182
|
+
// direct links / the related sections on detail pages.
|
|
183
|
+
listed: z.boolean().default(false),
|
|
172
184
|
}),
|
|
173
185
|
});
|
|
174
186
|
|
|
@@ -207,6 +219,10 @@ export function defineAasCollections({
|
|
|
207
219
|
// PUBLIC `teaser`). See docs/private-content-design.md.
|
|
208
220
|
private: z.boolean().default(false),
|
|
209
221
|
teaser: z.string().optional(),
|
|
222
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
223
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
224
|
+
// direct links / the related sections on detail pages.
|
|
225
|
+
listed: z.boolean().default(false),
|
|
210
226
|
}),
|
|
211
227
|
});
|
|
212
228
|
|
|
@@ -263,6 +279,64 @@ export function defineAasCollections({
|
|
|
263
279
|
// PUBLIC `teaser`). See docs/private-content-design.md.
|
|
264
280
|
private: z.boolean().default(false),
|
|
265
281
|
teaser: z.string().optional(),
|
|
282
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
283
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
284
|
+
// direct links / the related sections on detail pages.
|
|
285
|
+
listed: z.boolean().default(false),
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The `papers` collection is the reading room โ one entry per academic
|
|
291
|
+
* paper, an opt-in section (`sections: { papers: true }`) for sites that
|
|
292
|
+
* review the literature behind their stack. Frontmatter powers the cards
|
|
293
|
+
* (authors, venue/year, open-source availability, links); the body is the
|
|
294
|
+
* site's own reading/review of the paper. Locale-partitioned:
|
|
295
|
+
* `papers/<lang>/<slug>.mdx`.
|
|
296
|
+
*/
|
|
297
|
+
const papers = defineCollection({
|
|
298
|
+
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/papers' }),
|
|
299
|
+
schema: ({ image }) =>
|
|
300
|
+
z.object({
|
|
301
|
+
title: z.string(),
|
|
302
|
+
// The full author list, in publication order. Cards abbreviate to the
|
|
303
|
+
// first names + "et al."; the detail page shows everyone.
|
|
304
|
+
authors: z.array(z.string()).default([]),
|
|
305
|
+
year: z.number().int().optional(),
|
|
306
|
+
venue: z.string().optional(), // NeurIPS, ICML, arXiv, JMLR, โฆ
|
|
307
|
+
description: z.string(), // one-line takeaway, shown on cards
|
|
308
|
+
// Leaf id from the site's src/data/paper-categories.ts. Validated when
|
|
309
|
+
// the site passes `paperCategoryMap`; otherwise resolved at render
|
|
310
|
+
// with an uncategorized fallback.
|
|
311
|
+
category: (paperCategoryMap
|
|
312
|
+
? z.string().refine((id) => paperCategoryMap.has(id), {
|
|
313
|
+
message:
|
|
314
|
+
'unknown paper category id โ must match a node in the site data paper category tree',
|
|
315
|
+
})
|
|
316
|
+
: z.string()
|
|
317
|
+
).optional(),
|
|
318
|
+
image: image().optional(), // key figure / teaser image
|
|
319
|
+
imageAlt: z.string().optional(),
|
|
320
|
+
arxiv: z.string().url().optional(),
|
|
321
|
+
paperUrl: z.string().url().optional(), // publisher / DOI page
|
|
322
|
+
// Has the paper's code been released? Shown as a badge and filterable
|
|
323
|
+
// at a glance; `repo` links the released implementation.
|
|
324
|
+
openSource: z.boolean().default(false),
|
|
325
|
+
repo: z.string().url().optional(),
|
|
326
|
+
tools: z.array(z.string()).default([]), // catalog stacks this paper underpins
|
|
327
|
+
related: z.array(z.string()).default([]), // related paper slugs
|
|
328
|
+
tags: z.array(z.string()).default([]),
|
|
329
|
+
// Living-doc metadata for the review itself, mirroring concepts.
|
|
330
|
+
version: z.string().optional(),
|
|
331
|
+
updated: z.coerce.date().optional(),
|
|
332
|
+
draft: z.boolean().default(false),
|
|
333
|
+
// Login-gated entry (encrypted body; see docs/private-content-design.md).
|
|
334
|
+
private: z.boolean().default(false),
|
|
335
|
+
teaser: z.string().optional(),
|
|
336
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
337
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
338
|
+
// direct links / the related sections on detail pages.
|
|
339
|
+
listed: z.boolean().default(false),
|
|
266
340
|
}),
|
|
267
341
|
});
|
|
268
342
|
|
|
@@ -307,6 +381,10 @@ export function defineAasCollections({
|
|
|
307
381
|
// optional PUBLIC `teaser`). See docs/private-content-design.md.
|
|
308
382
|
private: z.boolean().default(false),
|
|
309
383
|
teaser: z.string().optional(),
|
|
384
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
385
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
386
|
+
// direct links / the related sections on detail pages.
|
|
387
|
+
listed: z.boolean().default(false),
|
|
310
388
|
}),
|
|
311
389
|
});
|
|
312
390
|
|
|
@@ -452,6 +530,10 @@ export function defineAasCollections({
|
|
|
452
530
|
// Login-gated page (encrypted body; see docs/private-content-design.md).
|
|
453
531
|
private: z.boolean().default(false),
|
|
454
532
|
teaser: z.string().optional(),
|
|
533
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
534
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
535
|
+
// direct links / the related sections on detail pages.
|
|
536
|
+
listed: z.boolean().default(false),
|
|
455
537
|
}),
|
|
456
538
|
});
|
|
457
539
|
|
|
@@ -484,8 +566,12 @@ export function defineAasCollections({
|
|
|
484
566
|
// PUBLIC `teaser`). See docs/private-content-design.md.
|
|
485
567
|
private: z.boolean().default(false),
|
|
486
568
|
teaser: z.string().optional(),
|
|
569
|
+
// Show this PRIVATE entry on index listings as a locked teaser card.
|
|
570
|
+
// Default: private entries stay OUT of listings and are reached via
|
|
571
|
+
// direct links / the related sections on detail pages.
|
|
572
|
+
listed: z.boolean().default(false),
|
|
487
573
|
}),
|
|
488
574
|
});
|
|
489
575
|
|
|
490
|
-
return { stacks, articles, concepts, courses, slides, products, pages };
|
|
576
|
+
return { stacks, articles, concepts, courses, papers, slides, products, pages };
|
|
491
577
|
}
|
package/src/i18n/ui.ts
CHANGED
|
@@ -60,6 +60,7 @@ export const ui = {
|
|
|
60
60
|
'nav.concepts': 'Concepts',
|
|
61
61
|
'nav.courses': 'Courses',
|
|
62
62
|
'nav.products': 'Products',
|
|
63
|
+
'nav.papers': 'Papers',
|
|
63
64
|
'nav.blog': 'Writing',
|
|
64
65
|
'nav.samples': 'Samples',
|
|
65
66
|
'nav.slides': 'Slides',
|
|
@@ -70,6 +71,7 @@ export const ui = {
|
|
|
70
71
|
'code.copy': 'Copy code',
|
|
71
72
|
'code.copied': 'Copied',
|
|
72
73
|
'private.badge': 'Private',
|
|
74
|
+
'private.login': 'Log in',
|
|
73
75
|
'private.locked': 'This content is private. Log in to view it.',
|
|
74
76
|
'private.id': 'ID',
|
|
75
77
|
'private.password': 'Password',
|
|
@@ -129,6 +131,17 @@ export const ui = {
|
|
|
129
131
|
'course.updated': 'Updated',
|
|
130
132
|
'course.relatedCourses': 'Related courses',
|
|
131
133
|
'course.slides': 'Slides',
|
|
134
|
+
'paper.title': 'Papers',
|
|
135
|
+
'paper.tagline': 'Paper readings and reviews โ what matters and why.',
|
|
136
|
+
'paper.empty': 'No papers yet.',
|
|
137
|
+
'paper.backToPapers': 'All papers',
|
|
138
|
+
'paper.authors': 'Authors',
|
|
139
|
+
'paper.openSource': 'Code available',
|
|
140
|
+
'paper.arxiv': 'arXiv',
|
|
141
|
+
'paper.paper': 'Paper',
|
|
142
|
+
'paper.code': 'Code',
|
|
143
|
+
'paper.relatedPapers': 'Related papers',
|
|
144
|
+
'paper.updated': 'Updated',
|
|
132
145
|
'products.title': 'Products',
|
|
133
146
|
'products.tagline': 'What we build and offer.',
|
|
134
147
|
'products.empty': 'No products yet.',
|
|
@@ -224,6 +237,7 @@ export const ui = {
|
|
|
224
237
|
'nav.concepts': '๊ฐ๋
',
|
|
225
238
|
'nav.courses': '๊ฐ์',
|
|
226
239
|
'nav.products': '์ ํ',
|
|
240
|
+
'nav.papers': '๋
ผ๋ฌธ',
|
|
227
241
|
'nav.blog': '๊ธ',
|
|
228
242
|
'nav.samples': '์ํ',
|
|
229
243
|
'nav.slides': '์ฌ๋ผ์ด๋',
|
|
@@ -234,6 +248,7 @@ export const ui = {
|
|
|
234
248
|
'code.copy': '์ฝ๋ ๋ณต์ฌ',
|
|
235
249
|
'code.copied': '๋ณต์ฌ๋จ',
|
|
236
250
|
'private.badge': '๋น๊ณต๊ฐ',
|
|
251
|
+
'private.login': '๋ก๊ทธ์ธ',
|
|
237
252
|
'private.locked': '๋น๊ณต๊ฐ ์ฝํ
์ธ ์
๋๋ค. ๋ก๊ทธ์ธ ํ ๋ณผ ์ ์์ต๋๋ค.',
|
|
238
253
|
'private.id': '์์ด๋',
|
|
239
254
|
'private.password': '๋น๋ฐ๋ฒํธ',
|
|
@@ -292,6 +307,17 @@ export const ui = {
|
|
|
292
307
|
'course.updated': '์
๋ฐ์ดํธ',
|
|
293
308
|
'course.relatedCourses': '๊ด๋ จ ๊ฐ์',
|
|
294
309
|
'course.slides': '์ฌ๋ผ์ด๋',
|
|
310
|
+
'paper.title': '๋
ผ๋ฌธ',
|
|
311
|
+
'paper.tagline': '๋
ผ๋ฌธ์ ์ฝ๊ณ ์ ๋ฆฌํฉ๋๋ค โ ๋ฌด์์ด, ์ ์ค์ํ์ง.',
|
|
312
|
+
'paper.empty': '์์ง ๋
ผ๋ฌธ์ด ์์ต๋๋ค.',
|
|
313
|
+
'paper.backToPapers': '๋
ผ๋ฌธ ์ ์ฒด',
|
|
314
|
+
'paper.authors': '์ ์',
|
|
315
|
+
'paper.openSource': '์ฝ๋ ๊ณต๊ฐ',
|
|
316
|
+
'paper.arxiv': 'arXiv',
|
|
317
|
+
'paper.paper': '์๋ฌธ',
|
|
318
|
+
'paper.code': '์ฝ๋',
|
|
319
|
+
'paper.relatedPapers': '๊ด๋ จ ๋
ผ๋ฌธ',
|
|
320
|
+
'paper.updated': '์
๋ฐ์ดํธ',
|
|
295
321
|
'products.title': '์ ํ',
|
|
296
322
|
'products.tagline': '์ฐ๋ฆฌ๊ฐ ๋ง๋ค๊ณ ์ ๊ณตํ๋ ๊ฒ.',
|
|
297
323
|
'products.empty': '์์ง ์ ํ์ด ์์ต๋๋ค.',
|
|
@@ -8,6 +8,8 @@ import ThemeToggle from '../components/ThemeToggle.astro';
|
|
|
8
8
|
import BackToTop from '../components/BackToTop.astro';
|
|
9
9
|
import { getNavPages, pageSlugOf } from '../lib/pages';
|
|
10
10
|
import { getNavProducts, getTopProducts, productSlugOf } from '../lib/products';
|
|
11
|
+
import { getDecks } from '../lib/slides';
|
|
12
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
13
|
import { homeTemplate } from '../lib/home';
|
|
12
14
|
import { sectionEnabled, type SectionKey } from '../lib/sections';
|
|
13
15
|
import { siteIcons } from '../lib/icons';
|
|
@@ -50,6 +52,9 @@ const navPages = await getNavPages(lang);
|
|
|
50
52
|
// the Products-index link, which appears once the locale has any product.
|
|
51
53
|
const navProducts = await getNavProducts(lang);
|
|
52
54
|
const hasProducts = (await getTopProducts(lang)).length > 0;
|
|
55
|
+
// The slides nav item only makes sense when the index actually lists decks โ
|
|
56
|
+
// a site whose decks are all private+unlisted reaches them from course pages.
|
|
57
|
+
const hasListedDecks = (await getDecks(lang)).some((d) => listedInIndex(d.data));
|
|
53
58
|
// The header's GitHub link โ hidden for sites whose repo is private
|
|
54
59
|
// (`repoNav: false` in site.ts) or that declare no repoUrl at all.
|
|
55
60
|
const showRepoLink =
|
|
@@ -95,6 +100,12 @@ const allNavItems: {
|
|
|
95
100
|
section: 'courses',
|
|
96
101
|
svg: '<path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/>',
|
|
97
102
|
},
|
|
103
|
+
{
|
|
104
|
+
href: getRelativeLocaleUrl(lang, 'paper/'),
|
|
105
|
+
label: t('nav.papers'),
|
|
106
|
+
section: 'papers' as SectionKey,
|
|
107
|
+
svg: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/>',
|
|
108
|
+
},
|
|
98
109
|
{
|
|
99
110
|
href: getRelativeLocaleUrl(lang, 'concept/'),
|
|
100
111
|
label: t('nav.concepts'),
|
|
@@ -113,12 +124,16 @@ const allNavItems: {
|
|
|
113
124
|
section: 'samples',
|
|
114
125
|
svg: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
|
|
115
126
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
...(hasListedDecks
|
|
128
|
+
? [
|
|
129
|
+
{
|
|
130
|
+
href: getRelativeLocaleUrl(lang, 'slides/'),
|
|
131
|
+
label: t('nav.slides'),
|
|
132
|
+
section: 'slides' as SectionKey,
|
|
133
|
+
svg: '<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/><path d="M12 12v9"/>',
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
: []),
|
|
122
137
|
{
|
|
123
138
|
href: getRelativeLocaleUrl(lang, 'glossary/'),
|
|
124
139
|
label: t('nav.glossary'),
|
|
@@ -257,6 +272,53 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
257
272
|
))
|
|
258
273
|
}
|
|
259
274
|
</div>
|
|
275
|
+
{/* Member login/logout for private content โ hidden until
|
|
276
|
+
/aas-auth.json reports the site has login users configured. */}
|
|
277
|
+
<details class="aas-menu relative" data-auth hidden>
|
|
278
|
+
<summary
|
|
279
|
+
class="flex cursor-pointer list-none items-center rounded-lg border border-[var(--aas-border)] px-2.5 py-1 text-[var(--aas-text)] select-none"
|
|
280
|
+
aria-label={t('private.login')}
|
|
281
|
+
title={t('private.login')}
|
|
282
|
+
>
|
|
283
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
284
|
+
</summary>
|
|
285
|
+
<div
|
|
286
|
+
class="absolute right-0 z-20 mt-1 w-64 rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] p-3 shadow-lg"
|
|
287
|
+
>
|
|
288
|
+
<form data-auth-form class="flex flex-col gap-2">
|
|
289
|
+
<input
|
|
290
|
+
name="id"
|
|
291
|
+
required
|
|
292
|
+
autocomplete="username"
|
|
293
|
+
placeholder={t('private.id')}
|
|
294
|
+
class="rounded-lg border border-[var(--aas-border)] bg-[var(--aas-bg)] px-3 py-1.5 text-sm text-[var(--aas-text)]"
|
|
295
|
+
/>
|
|
296
|
+
<input
|
|
297
|
+
name="password"
|
|
298
|
+
type="password"
|
|
299
|
+
required
|
|
300
|
+
autocomplete="current-password"
|
|
301
|
+
placeholder={t('private.password')}
|
|
302
|
+
class="rounded-lg border border-[var(--aas-border)] bg-[var(--aas-bg)] px-3 py-1.5 text-sm text-[var(--aas-text)]"
|
|
303
|
+
/>
|
|
304
|
+
<p data-auth-error hidden class="text-xs text-[var(--aas-accent)]">{t('private.error')}</p>
|
|
305
|
+
<button
|
|
306
|
+
type="submit"
|
|
307
|
+
class="rounded-lg bg-[var(--aas-accent)] px-3 py-1.5 text-sm font-semibold text-white"
|
|
308
|
+
>
|
|
309
|
+
{t('private.submit')}
|
|
310
|
+
</button>
|
|
311
|
+
</form>
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
data-auth-logout
|
|
315
|
+
hidden
|
|
316
|
+
class="w-full rounded-lg border border-[var(--aas-border)] px-3 py-1.5 text-sm text-[var(--aas-text)] hover:bg-[var(--aas-tint)]"
|
|
317
|
+
>
|
|
318
|
+
๐ {t('private.logout')}
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</details>
|
|
260
322
|
<ThemeToggle lang={lang} />
|
|
261
323
|
<LanguageSwitcher lang={lang} path={path} />
|
|
262
324
|
<details class="aas-menu relative sm:hidden">
|
|
@@ -322,6 +384,55 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
322
384
|
<BackToTop lang={lang} />
|
|
323
385
|
|
|
324
386
|
<script>
|
|
387
|
+
import {
|
|
388
|
+
hasSession,
|
|
389
|
+
clearSession,
|
|
390
|
+
loginWithCredentials,
|
|
391
|
+
type AuthData,
|
|
392
|
+
} from '../lib/private-client';
|
|
393
|
+
|
|
394
|
+
// Header member login/logout: reveal the control only when the site has
|
|
395
|
+
// login users configured (/aas-auth.json โ { enabled: false } otherwise).
|
|
396
|
+
const authCtrl = document.querySelector<HTMLDetailsElement>('[data-auth]');
|
|
397
|
+
if (authCtrl) {
|
|
398
|
+
(async () => {
|
|
399
|
+
try {
|
|
400
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
401
|
+
const res = await fetch(`${base}/aas-auth.json`);
|
|
402
|
+
const data = (await res.json()) as { enabled: boolean } & AuthData;
|
|
403
|
+
if (!data.enabled) return;
|
|
404
|
+
authCtrl.hidden = false;
|
|
405
|
+
const form = authCtrl.querySelector<HTMLFormElement>('[data-auth-form]')!;
|
|
406
|
+
const logoutBtn = authCtrl.querySelector<HTMLButtonElement>('[data-auth-logout]')!;
|
|
407
|
+
const errorEl = authCtrl.querySelector<HTMLElement>('[data-auth-error]')!;
|
|
408
|
+
const sync = () => {
|
|
409
|
+
const on = hasSession(data.days);
|
|
410
|
+
form.hidden = on;
|
|
411
|
+
logoutBtn.hidden = !on;
|
|
412
|
+
};
|
|
413
|
+
sync();
|
|
414
|
+
logoutBtn.addEventListener('click', () => {
|
|
415
|
+
clearSession();
|
|
416
|
+
location.reload();
|
|
417
|
+
});
|
|
418
|
+
form.addEventListener('submit', async (e) => {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
errorEl.hidden = true;
|
|
421
|
+
const fd = new FormData(form);
|
|
422
|
+
const ok = await loginWithCredentials(
|
|
423
|
+
data,
|
|
424
|
+
String(fd.get('id') ?? ''),
|
|
425
|
+
String(fd.get('password') ?? ''),
|
|
426
|
+
);
|
|
427
|
+
if (ok) location.reload();
|
|
428
|
+
else errorEl.hidden = false;
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
/* endpoint unavailable โ control stays hidden */
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
}
|
|
435
|
+
|
|
325
436
|
import { initOnReady } from '../lib/reinit';
|
|
326
437
|
|
|
327
438
|
// Clicking a heading's "#" anchor copies that section's URL to the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an entry belongs on index listings. Public entries always do;
|
|
3
|
+
* PRIVATE entries only when they opt in with `listed: true` (shown as a
|
|
4
|
+
* locked teaser card). Unlisted private entries still build and stay
|
|
5
|
+
* reachable via direct links and the related-entry sections on detail pages โ
|
|
6
|
+
* that's the discovery path for gated content.
|
|
7
|
+
*/
|
|
8
|
+
export const listedInIndex = (d: { private?: boolean; listed?: boolean }): boolean =>
|
|
9
|
+
!d.private || d.listed === true;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { Lang } from '../i18n/ui';
|
|
3
|
+
|
|
4
|
+
export type PaperEntry = CollectionEntry<'papers'>;
|
|
5
|
+
|
|
6
|
+
/** The url slug of a paper, i.e. its id with the `<lang>/` prefix removed. */
|
|
7
|
+
export function paperSlugOf(entry: PaperEntry): string {
|
|
8
|
+
return entry.id.replace(/^[a-z]{2}\//, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Published papers for one locale โ newest publication year first, then title. */
|
|
12
|
+
export async function getPapers(lang: Lang): Promise<PaperEntry[]> {
|
|
13
|
+
const all = await getCollection('papers');
|
|
14
|
+
return all
|
|
15
|
+
.filter((e) => e.id.startsWith(`${lang}/`) && !e.data.draft)
|
|
16
|
+
.sort(
|
|
17
|
+
(a, b) => (b.data.year ?? 0) - (a.data.year ?? 0) || a.data.title.localeCompare(b.data.title),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Cards abbreviate the author list: first `max` names, then "et al.". */
|
|
22
|
+
export function shortAuthors(authors: string[], max = 3): string {
|
|
23
|
+
if (authors.length === 0) return '';
|
|
24
|
+
return authors.length <= max ? authors.join(', ') : `${authors.slice(0, max).join(', ')} et al.`;
|
|
25
|
+
}
|
|
@@ -60,7 +60,7 @@ function storedKey(days: number): Uint8Array | null {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function clearSession(): void {
|
|
63
|
+
export function clearSession(): void {
|
|
64
64
|
try {
|
|
65
65
|
localStorage.removeItem(STORE);
|
|
66
66
|
} catch {
|
|
@@ -68,6 +68,50 @@ function clearSession(): void {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The user table a login form needs โ a GateData without the content. */
|
|
72
|
+
export interface AuthData {
|
|
73
|
+
users: { h: string; s: string; iv: string; w: string }[];
|
|
74
|
+
salt: string;
|
|
75
|
+
days: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether this device holds a live session key. */
|
|
79
|
+
export function hasSession(days: number): boolean {
|
|
80
|
+
return storedKey(days) !== null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verify credentials by unwrapping the site key (AES-GCM authentication
|
|
85
|
+
* fails on a wrong password) and store the session on success. Shared by
|
|
86
|
+
* the per-page gate form and the header login control.
|
|
87
|
+
*/
|
|
88
|
+
export async function loginWithCredentials(
|
|
89
|
+
data: AuthData,
|
|
90
|
+
id: string,
|
|
91
|
+
password: string,
|
|
92
|
+
): Promise<boolean> {
|
|
93
|
+
const salt = dec(data.salt);
|
|
94
|
+
const idBytes = new TextEncoder().encode(id.trim().toLowerCase());
|
|
95
|
+
const joined = new Uint8Array(salt.length + idBytes.length);
|
|
96
|
+
joined.set(salt);
|
|
97
|
+
joined.set(idBytes, salt.length);
|
|
98
|
+
const h = await sha256Hex(joined);
|
|
99
|
+
const user = data.users.find((u) => u.h === h);
|
|
100
|
+
if (!user) return false;
|
|
101
|
+
try {
|
|
102
|
+
const kek = await deriveKek(password, dec(user.s));
|
|
103
|
+
const k = new Uint8Array(await aesDecrypt(kek, dec(user.iv), dec(user.w)));
|
|
104
|
+
try {
|
|
105
|
+
localStorage.setItem(STORE, JSON.stringify({ k: enc(k), t: Date.now() }));
|
|
106
|
+
} catch {
|
|
107
|
+
/* private browsing โ session just won't persist */
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false; // wrong password (unwrap failed)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/** Swap the gate for the decrypted HTML; re-run inline scripts; notify re-init hooks. */
|
|
72
116
|
function inject(gate: HTMLElement, html: string, logoutLabel: string): void {
|
|
73
117
|
const host = document.createElement('div');
|
package/src/lib/sections.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type SectionKey =
|
|
|
21
21
|
| 'articles'
|
|
22
22
|
| 'courses'
|
|
23
23
|
| 'products'
|
|
24
|
+
| 'papers'
|
|
24
25
|
| 'samples'
|
|
25
26
|
| 'slides'
|
|
26
27
|
| 'glossary'
|
|
@@ -31,6 +32,7 @@ const DEFAULTS: Record<SectionKey, boolean> = {
|
|
|
31
32
|
articles: true,
|
|
32
33
|
courses: false, // opt-in: needs src/data/course-categories.ts on the site
|
|
33
34
|
products: false, // opt-in: needs src/data/product-categories.ts on the site
|
|
35
|
+
papers: false, // opt-in: needs src/data/paper-categories.ts on the site
|
|
34
36
|
samples: true,
|
|
35
37
|
slides: true,
|
|
36
38
|
glossary: true,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { site } from '@aas-data/site';
|
|
3
|
+
import type { GetStaticPaths } from 'astro';
|
|
4
|
+
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
|
5
|
+
import PaperDetail from '../../../components/PaperDetail.astro';
|
|
6
|
+
import PrivateGate from '../../../components/PrivateGate.astro';
|
|
7
|
+
import { getPapers, paperSlugOf } from '../../../lib/papers';
|
|
8
|
+
import { allLocales, langParam } from '../../../lib/locales';
|
|
9
|
+
|
|
10
|
+
export const getStaticPaths = (async () => {
|
|
11
|
+
const paths: Awaited<ReturnType<GetStaticPaths>> = [];
|
|
12
|
+
for (const lang of allLocales) {
|
|
13
|
+
for (const entry of await getPapers(lang)) {
|
|
14
|
+
paths.push({ params: { lang: langParam(lang), id: paperSlugOf(entry) }, props: { lang, entry } });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return paths;
|
|
18
|
+
}) satisfies GetStaticPaths;
|
|
19
|
+
|
|
20
|
+
const { lang, entry } = Astro.props;
|
|
21
|
+
const slug = paperSlugOf(entry);
|
|
22
|
+
const priv = entry.data.private;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<BaseLayout
|
|
26
|
+
title={`${entry.data.title} โ ${site.name}`}
|
|
27
|
+
description={priv ? entry.data.teaser : entry.data.description}
|
|
28
|
+
lang={lang}
|
|
29
|
+
path={`paper/${slug}/`}
|
|
30
|
+
noindex={priv}
|
|
31
|
+
>
|
|
32
|
+
<PrivateGate enabled={priv} lang={lang} title={entry.data.title} teaser={entry.data.teaser}>
|
|
33
|
+
<PaperDetail entry={entry} lang={lang} />
|
|
34
|
+
</PrivateGate>
|
|
35
|
+
</BaseLayout>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { site } from '@aas-data/site';
|
|
3
|
+
import type { GetStaticPaths } from 'astro';
|
|
4
|
+
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
|
5
|
+
import PapersIndex from '../../../../components/PapersIndex.astro';
|
|
6
|
+
import { paperTree } from '@aas-data/paper-categories';
|
|
7
|
+
import { allLocales, langParam } from '../../../../lib/locales';
|
|
8
|
+
|
|
9
|
+
export const getStaticPaths = (() => {
|
|
10
|
+
const paths: Awaited<ReturnType<GetStaticPaths>> = [];
|
|
11
|
+
for (const lang of allLocales) {
|
|
12
|
+
for (const id of paperTree.allIds) {
|
|
13
|
+
paths.push({ params: { lang: langParam(lang), id }, props: { lang, id } });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return paths;
|
|
17
|
+
}) satisfies GetStaticPaths;
|
|
18
|
+
|
|
19
|
+
const { lang, id } = Astro.props as { lang: string; id: string };
|
|
20
|
+
const node = paperTree.map.get(id)!;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<BaseLayout title={`${node.label[lang]} โ ${site.name}`} lang={lang} path={`paper/category/${id}/`}>
|
|
24
|
+
<PapersIndex lang={lang} categoryId={id} />
|
|
25
|
+
</BaseLayout>
|