stack-site-builder 1.17.4 → 1.18.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 +27 -0
- package/README.md +17 -5
- package/index.d.ts +1 -0
- package/index.mjs +5 -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 +24 -0
- package/src/layouts/BaseLayout.astro +6 -0
- package/src/lib/listing.ts +9 -0
- package/src/lib/papers.ts +25 -0
- 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/styles/global.css +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,26 @@ content schema, while a consuming site supplies only content, taxonomy data and
|
|
|
11
11
|
config. Sites track the theme with `pnpm up stack-site-builder`, so each release
|
|
12
12
|
here is a plain version bump they pull in.
|
|
13
13
|
|
|
14
|
+
## [1.18.0] - 2026-07-23
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`papers` collection — the reading room** — an opt-in section
|
|
19
|
+
(`sections: { papers: true }` + `src/data/paper-categories.ts`, with an
|
|
20
|
+
optional `paperCategoryMap` for build-time category validation) for sites
|
|
21
|
+
that review the literature behind their stack. Frontmatter carries the
|
|
22
|
+
full author list (cards abbreviate to "et al."), venue/year, arXiv /
|
|
23
|
+
publisher / released-code links, and an **open-source availability badge**
|
|
24
|
+
(`openSource` + `repo`); `tools` cross-links catalog stacks on the detail
|
|
25
|
+
page. Routes mirror concepts: `/paper/`, `/paper/<id>/`,
|
|
26
|
+
`/paper/category/<id>/`, with a category-grouped index.
|
|
27
|
+
- **Private entries are unlisted by default** — index listings (catalog
|
|
28
|
+
home, categories, tags, vendors, blog, concepts, courses, papers,
|
|
29
|
+
products, slides) now skip `private` entries unless they opt in with
|
|
30
|
+
`listed: true`, which shows the familiar locked teaser card. Unlisted
|
|
31
|
+
private entries still build and stay reachable via direct links and the
|
|
32
|
+
related sections on detail pages — the discovery path for gated content.
|
|
33
|
+
|
|
14
34
|
## [1.17.4] - 2026-07-22
|
|
15
35
|
|
|
16
36
|
### Changed
|
|
@@ -23,6 +43,12 @@ here is a plain version bump they pull in.
|
|
|
23
43
|
its slide leaves the screen — the deck never carries a live embed between
|
|
24
44
|
slides.
|
|
25
45
|
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Table label columns wrapped** — in a wide table the first column got
|
|
49
|
+
squeezed to its minimum width, breaking short labels ("Part 1") onto two
|
|
50
|
+
lines. The first column of prose and slide tables no longer wraps.
|
|
51
|
+
|
|
26
52
|
## [1.17.3] - 2026-07-22
|
|
27
53
|
|
|
28
54
|
### Fixed
|
|
@@ -277,6 +303,7 @@ catalog sites from a thin content-only repository.
|
|
|
277
303
|
- **Standalone development setup** — a devcontainer and a minimal `playground/`
|
|
278
304
|
consuming site for developing and previewing the theme on its own.
|
|
279
305
|
|
|
306
|
+
[1.18.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.4...v1.18.0
|
|
280
307
|
[1.17.4]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.3...v1.17.4
|
|
281
308
|
[1.17.3]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.2...v1.17.3
|
|
282
309
|
[1.17.2]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.1...v1.17.2
|
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ export const collections = defineAasCollections({ categoryMap });
|
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `src/data/site.ts` | Site identity: name, repo URL (`repoNav: false` hides the header's GitHub link), the `locales` it ships, optional `sections` toggles, browser icons (`icons: { favicon, appleTouch, manifest }`), the `home` template, per-locale UI string overrides |
|
|
40
40
|
| `src/data/categories.ts` | The tool-catalog category tree (validated against content) |
|
|
41
|
-
| `src/data/concept-categories.ts` · `article-categories.ts` · `course-categories.ts` · `product-categories.ts` (opt-in) | Taxonomies for concepts / articles / courses / products |
|
|
41
|
+
| `src/data/concept-categories.ts` · `article-categories.ts` · `course-categories.ts` · `product-categories.ts` · `paper-categories.ts` (opt-in) | Taxonomies for concepts / articles / courses / products / papers |
|
|
42
42
|
| `src/data/glossary.mjs` | `[[Term]]` wikilink targets |
|
|
43
|
-
| `src/content/{stacks,concepts,courses,products,articles,slides}/` | The content, one MDX file per locale |
|
|
43
|
+
| `src/content/{stacks,concepts,courses,products,papers,articles,slides}/` | The content, one MDX file per locale |
|
|
44
44
|
| `src/content/pages/` | Standalone top-level pages (e.g. an About/소개), rendered at `/<slug>/` and optionally linked in the header nav |
|
|
45
45
|
| `public/` · `samples/` | Logos/favicons and runnable sample projects |
|
|
46
46
|
|
|
@@ -72,7 +72,7 @@ The rest are opt-out — **concepts, articles, samples, slides, glossary** and t
|
|
|
72
72
|
standalone **pages** collection (About/소개, …) — so a site can ship only what it
|
|
73
73
|
needs. Turning one off removes both its routes and its header-nav item. (`pages`
|
|
74
74
|
also has finer control: each page's `nav` / `draft` frontmatter, or simply not
|
|
75
|
-
authoring it.) **courses** and **
|
|
75
|
+
authoring it.) **courses**, **products** and **papers** are the opt-IN sections — see below.
|
|
76
76
|
|
|
77
77
|
Declare the toggles once in `src/data/site.ts` and forward them to the theme in
|
|
78
78
|
astro.config (which needs them to skip route injection). Import `SectionKey` from
|
|
@@ -166,6 +166,16 @@ Localized values are either one string or a per-locale record with
|
|
|
166
166
|
default-locale fallback. On a cards home the header's Browse link (which
|
|
167
167
|
anchors into the catalog) hides itself.
|
|
168
168
|
|
|
169
|
+
## Papers (opt-in)
|
|
170
|
+
|
|
171
|
+
A reading room for the literature behind the stack: each entry is one paper
|
|
172
|
+
with its full author list, venue/year, arXiv / publisher / code links and an
|
|
173
|
+
open-source availability badge; the body is your reading of it. `tools`
|
|
174
|
+
cross-links catalog stacks. Enable with `sections: { papers: true }` plus
|
|
175
|
+
`src/data/paper-categories.ts` (exporting `paperTree` / `paperCatOf`), and
|
|
176
|
+
optionally pass `paperCategoryMap` to `defineAasCollections`. Routes mirror
|
|
177
|
+
concepts: `/paper/`, `/paper/<id>/`, `/paper/category/<id>/`.
|
|
178
|
+
|
|
169
179
|
## Body components
|
|
170
180
|
|
|
171
181
|
Reusable MDX-body components, importable from any collection's content:
|
|
@@ -196,8 +206,10 @@ teaser: A public one-liner shown on cards and above the login form. # optional
|
|
|
196
206
|
|
|
197
207
|
The body ships **encrypted** (AES-256-GCM, key wrapped per user with PBKDF2) and
|
|
198
208
|
is decrypted in the browser after login — no server needed, works on any static
|
|
199
|
-
host.
|
|
200
|
-
|
|
209
|
+
host. By default a private entry stays OUT of index listings (reachable via
|
|
210
|
+
direct links and the related sections on detail pages); set `listed: true` to
|
|
211
|
+
show it as a locked teaser card (title + 🔒 + teaser). Private URLs get
|
|
212
|
+
`noindex` and stay out of the sitemap. Users and keys come from env vars (see
|
|
201
213
|
`playground/.env.sample`): `AAS_PRIVATE_USERS` (`id:password,…`),
|
|
202
214
|
`AAS_PRIVATE_MASTER_SECRET` (rotate it to log every device out) and
|
|
203
215
|
`AAS_PRIVATE_SESSION_DAYS`. Set them in `.env` locally or as CI secrets.
|
package/index.d.ts
CHANGED
package/index.mjs
CHANGED
|
@@ -44,6 +44,9 @@ const PAGES = [
|
|
|
44
44
|
'concept/[...id].astro',
|
|
45
45
|
'concept/category/[id].astro',
|
|
46
46
|
'course/index.astro',
|
|
47
|
+
'paper/index.astro',
|
|
48
|
+
'paper/[...id].astro',
|
|
49
|
+
'paper/category/[id].astro',
|
|
47
50
|
'course/[...id].astro',
|
|
48
51
|
'course/category/[id].astro',
|
|
49
52
|
'glossary.astro',
|
|
@@ -72,6 +75,7 @@ function sectionOf(file) {
|
|
|
72
75
|
if (file.startsWith('concept/')) return 'concepts';
|
|
73
76
|
if (file.startsWith('article/')) return 'articles';
|
|
74
77
|
if (file.startsWith('course/')) return 'courses';
|
|
78
|
+
if (file.startsWith('paper/')) return 'papers';
|
|
75
79
|
if (file.startsWith('products/')) return 'products';
|
|
76
80
|
if (file.startsWith('sample/')) return 'samples';
|
|
77
81
|
if (file.startsWith('slides/')) return 'slides';
|
|
@@ -85,7 +89,7 @@ function sectionOf(file) {
|
|
|
85
89
|
// when the site passes `{ <key>: true }`. Both need site-side data
|
|
86
90
|
// (src/data/{course,product}-categories.ts), so a theme upgrade alone must
|
|
87
91
|
// not enable them.
|
|
88
|
-
const OPT_IN_SECTIONS = new Set(['courses', 'products']);
|
|
92
|
+
const OPT_IN_SECTIONS = new Set(['courses', 'products', 'papers']);
|
|
89
93
|
|
|
90
94
|
/**
|
|
91
95
|
* @param {object} opts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stack-site-builder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.18.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The engine behind the awesome-*-stack catalog sites: an Astro theme with the catalog/concepts/articles/slides/samples routes, components, styles and markdown pipeline. Sites provide content, taxonomy data and config.",
|
|
7
7
|
"repository": {
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { articleTree, articleCatOf } from '@aas-data/article-categories';
|
|
10
10
|
import { getArticles, articleSlugOf, type ArticleEntry } from '../lib/articles';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the writing index (no `categoryId` → all articles, grouped by
|
|
@@ -20,7 +21,7 @@ interface Props {
|
|
|
20
21
|
|
|
21
22
|
const { lang, categoryId } = Astro.props;
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
|
-
const all = await getArticles(lang);
|
|
24
|
+
const all = (await getArticles(lang)).filter((e) => listedInIndex(e.data));
|
|
24
25
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `article/category/${cid}/`);
|
|
25
26
|
|
|
26
27
|
const itemsIn = (cid: string): ArticleEntry[] => {
|
|
@@ -7,6 +7,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
7
7
|
import { categoryMap, childrenOf, pathOf } from '@aas-data/categories';
|
|
8
8
|
import { getStacks, getStacksByCategory, slugOf, type StackEntry } from '../lib/stacks';
|
|
9
9
|
import { deriveFacets } from '../lib/facets';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -31,16 +32,21 @@ const crumbs = [
|
|
|
31
32
|
|
|
32
33
|
const children = childrenOf(id);
|
|
33
34
|
// Stacks attached directly to this node (vs. nested under a subcategory).
|
|
34
|
-
const direct = (await getStacksByCategory(lang, id)).filter(
|
|
35
|
+
const direct = (await getStacksByCategory(lang, id)).filter(
|
|
36
|
+
(s) => s.data.category === id && listedInIndex(s.data),
|
|
37
|
+
);
|
|
35
38
|
// Each child subcategory with its own rolled-up stacks.
|
|
36
39
|
const childGroups = await Promise.all(
|
|
37
|
-
children.map(async (c) => ({
|
|
40
|
+
children.map(async (c) => ({
|
|
41
|
+
cat: c,
|
|
42
|
+
items: (await getStacksByCategory(lang, c.id)).filter((s) => listedInIndex(s.data)),
|
|
43
|
+
})),
|
|
38
44
|
);
|
|
39
45
|
|
|
40
46
|
// Filter facets are built from the WHOLE catalog (not just this page's tools) so
|
|
41
47
|
// they're identical on every page — a sort/filter carried in from elsewhere can
|
|
42
48
|
// always be restored here, even if no tool in this category has that value.
|
|
43
|
-
const facets = deriveFacets(await getStacks(lang), lang);
|
|
49
|
+
const facets = deriveFacets((await getStacks(lang)).filter((e) => listedInIndex(e.data)), lang);
|
|
44
50
|
|
|
45
51
|
const cardProps = (e: StackEntry) => ({
|
|
46
52
|
slug: slugOf(e),
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { conceptTree, conceptCatOf } from '@aas-data/concept-categories';
|
|
10
10
|
import { getConcepts, conceptSlugOf, type ConceptEntry } from '../lib/concepts';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the concept index (no `categoryId` → all concepts, grouped by
|
|
@@ -22,7 +23,7 @@ const { lang, categoryId } = Astro.props;
|
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
24
|
// Default order is most-recently-updated first (matches the ListControls
|
|
24
25
|
// default), so the server render and the client sort agree — no reflow on load.
|
|
25
|
-
const all = (await getConcepts(lang)).sort(
|
|
26
|
+
const all = (await getConcepts(lang)).filter((e) => listedInIndex(e.data)).sort(
|
|
26
27
|
(a, b) =>
|
|
27
28
|
(b.data.updated?.getTime() ?? 0) - (a.data.updated?.getTime() ?? 0) ||
|
|
28
29
|
a.data.title.localeCompare(b.data.title),
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { courseTree, courseCatOf } from '@aas-data/course-categories';
|
|
10
10
|
import { getCourses, courseSlugOf, type CourseEntry } from '../lib/courses';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the course index (no `categoryId` → all courses, grouped by
|
|
@@ -22,7 +23,7 @@ const { lang, categoryId } = Astro.props;
|
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
24
|
// getCourses() order (manual `order` desc, then date) agrees with the client
|
|
24
25
|
// "recent" comparator over CourseCard's data-date keys — no reflow on load.
|
|
25
|
-
const all = await getCourses(lang);
|
|
26
|
+
const all = (await getCourses(lang)).filter((e) => listedInIndex(e.data));
|
|
26
27
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `course/category/${cid}/`);
|
|
27
28
|
|
|
28
29
|
const itemsIn = (cid: string): CourseEntry[] => {
|
|
@@ -7,6 +7,7 @@ import { rootCategories, descendantIds } from '@aas-data/categories';
|
|
|
7
7
|
import { getStacks, slugOf } from '../lib/stacks';
|
|
8
8
|
import { site } from '@aas-data/site';
|
|
9
9
|
import { deriveFacets } from '../lib/facets';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -15,7 +16,7 @@ interface Props {
|
|
|
15
16
|
|
|
16
17
|
const { lang } = Astro.props;
|
|
17
18
|
const t = useTranslations(lang);
|
|
18
|
-
const entries = await getStacks(lang);
|
|
19
|
+
const entries = (await getStacks(lang)).filter((e) => listedInIndex(e.data));
|
|
19
20
|
|
|
20
21
|
// Default order is alphabetical (matches the client's default sort, so no flash).
|
|
21
22
|
// Group by top-level category, rolling up every subcategory in its subtree.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from 'astro:assets';
|
|
3
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
4
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
5
|
+
import { shortAuthors } from '../lib/papers';
|
|
6
|
+
import { inlineMd } from '../lib/inline-md';
|
|
7
|
+
import type { ImageMetadata } from 'astro';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
lang: Lang;
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
authors: string[];
|
|
15
|
+
year?: number;
|
|
16
|
+
venue?: string;
|
|
17
|
+
openSource?: boolean;
|
|
18
|
+
tags: string[];
|
|
19
|
+
image?: ImageMetadata;
|
|
20
|
+
imageAlt?: string;
|
|
21
|
+
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
22
|
+
isPrivate?: boolean;
|
|
23
|
+
teaser?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
lang,
|
|
28
|
+
slug,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
authors,
|
|
32
|
+
year,
|
|
33
|
+
venue,
|
|
34
|
+
openSource = false,
|
|
35
|
+
tags,
|
|
36
|
+
image,
|
|
37
|
+
imageAlt,
|
|
38
|
+
isPrivate = false,
|
|
39
|
+
teaser,
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
const t = useTranslations(lang);
|
|
42
|
+
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<a
|
|
46
|
+
href={getRelativeLocaleUrl(lang, `paper/${slug}/`)}
|
|
47
|
+
data-name={title.toLowerCase()}
|
|
48
|
+
data-date={year ? String(year) : ''}
|
|
49
|
+
class="aas-lift block overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
50
|
+
>
|
|
51
|
+
{image && <Image src={image} alt={imageAlt ?? title} class="aspect-[16/9] w-full object-cover" />}
|
|
52
|
+
<div class="p-5">
|
|
53
|
+
<p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-[var(--aas-muted)]">
|
|
54
|
+
{venue && <span>{venue}</span>}
|
|
55
|
+
{venue && year && <span>·</span>}
|
|
56
|
+
{year && <span>{year}</span>}
|
|
57
|
+
{
|
|
58
|
+
openSource && (
|
|
59
|
+
<span class="rounded-full border border-[var(--aas-accent)] px-2 py-0.5 text-[var(--aas-accent)]">
|
|
60
|
+
{t('paper.openSource')}
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
</p>
|
|
65
|
+
<h3 class="mt-1 text-lg font-semibold text-[var(--aas-text)]">
|
|
66
|
+
{isPrivate && <span aria-label={t('private.badge')} title={t('private.badge')}>🔒 </span>}{title}
|
|
67
|
+
</h3>
|
|
68
|
+
{authors.length > 0 && <p class="mt-1 text-sm text-[var(--aas-muted)]">{shortAuthors(authors)}</p>}
|
|
69
|
+
<p
|
|
70
|
+
class="aas-md mt-2 text-sm leading-relaxed text-[var(--aas-muted)]"
|
|
71
|
+
set:html={inlineMd(shownDesc)}
|
|
72
|
+
/>
|
|
73
|
+
{
|
|
74
|
+
tags.length > 0 && (
|
|
75
|
+
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
76
|
+
{tags.slice(0, 4).map((tag) => (
|
|
77
|
+
<span class="rounded-full border border-[var(--aas-border)] px-2 py-0.5 text-xs text-[var(--aas-muted)]">
|
|
78
|
+
{tag}
|
|
79
|
+
</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
</div>
|
|
85
|
+
</a>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
4
|
+
import { getPapers, paperSlugOf, type PaperEntry } from '../lib/papers';
|
|
5
|
+
import { inlineMd } from '../lib/inline-md';
|
|
6
|
+
import { Image } from 'astro:assets';
|
|
7
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
8
|
+
import { formatDate } from '../lib/dates';
|
|
9
|
+
import MermaidLoader from './MermaidLoader.astro';
|
|
10
|
+
import TocRail from './TocRail.astro';
|
|
11
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
12
|
+
import ArrowUpRight from './ArrowUpRight.astro';
|
|
13
|
+
import { paperTree, paperCatOf } from '@aas-data/paper-categories';
|
|
14
|
+
import RelatedPapers from './RelatedPapers.astro';
|
|
15
|
+
import RelatedTools from './RelatedTools.astro';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
entry: PaperEntry;
|
|
19
|
+
lang: Lang;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { entry, lang } = Astro.props;
|
|
23
|
+
const t = useTranslations(lang);
|
|
24
|
+
const { Content, headings } = await render(entry);
|
|
25
|
+
const data = entry.data;
|
|
26
|
+
const tocItems = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
27
|
+
const bodyHasMermaid = (entry.body ?? '').includes('```mermaid');
|
|
28
|
+
|
|
29
|
+
// Sibling papers linked from frontmatter (mutual cross-links, like concepts).
|
|
30
|
+
const papers = await getPapers(lang);
|
|
31
|
+
const paperBySlug = new Map(papers.map((c) => [paperSlugOf(c), c]));
|
|
32
|
+
const relatedPapers = data.related
|
|
33
|
+
.map((sl) => paperBySlug.get(sl))
|
|
34
|
+
.filter(Boolean) as PaperEntry[];
|
|
35
|
+
|
|
36
|
+
// External resource buttons: arXiv, the publisher page, and released code.
|
|
37
|
+
const links = [
|
|
38
|
+
data.arxiv && { label: t('paper.arxiv'), href: data.arxiv },
|
|
39
|
+
data.paperUrl && { label: t('paper.paper'), href: data.paperUrl },
|
|
40
|
+
data.repo && { label: t('paper.code'), href: data.repo },
|
|
41
|
+
].filter(Boolean) as { label: string; href: string }[];
|
|
42
|
+
|
|
43
|
+
// Breadcrumb: All papers › category › subcategory… (each segment linked).
|
|
44
|
+
const crumbs = [
|
|
45
|
+
{ label: t('paper.backToPapers'), href: getRelativeLocaleUrl(lang, 'paper/') },
|
|
46
|
+
...paperTree.pathOf(paperCatOf(data.category)).map((c) => ({
|
|
47
|
+
label: c.label[lang],
|
|
48
|
+
href: getRelativeLocaleUrl(lang, `paper/category/${c.id}/`),
|
|
49
|
+
})),
|
|
50
|
+
];
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<Breadcrumb segments={crumbs} />
|
|
54
|
+
|
|
55
|
+
<header class="mt-4 pb-6">
|
|
56
|
+
<h1 class="text-3xl font-bold tracking-tight">{data.title}</h1>
|
|
57
|
+
{
|
|
58
|
+
data.authors.length > 0 && (
|
|
59
|
+
<p class="mt-2 text-sm text-[var(--aas-muted)]">
|
|
60
|
+
<span class="font-semibold">{t('paper.authors')}:</span> {data.authors.join(', ')}
|
|
61
|
+
</p>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
<p class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--aas-muted)]">
|
|
65
|
+
{data.venue && <span>{data.venue}</span>}
|
|
66
|
+
{data.year && <span>{data.year}</span>}
|
|
67
|
+
{
|
|
68
|
+
data.openSource && (
|
|
69
|
+
<span class="rounded-full border border-[var(--aas-accent)] px-2 py-0.5 text-xs text-[var(--aas-accent)]">
|
|
70
|
+
{t('paper.openSource')}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
{data.version && <span>{t('meta.docVersion')} {data.version}</span>}
|
|
75
|
+
{
|
|
76
|
+
data.updated && (
|
|
77
|
+
<span>
|
|
78
|
+
{t('paper.updated')} {formatDate(data.updated, lang)}
|
|
79
|
+
</span>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</p>
|
|
83
|
+
{
|
|
84
|
+
links.length > 0 && (
|
|
85
|
+
<p class="mt-3 flex flex-wrap items-center gap-2">
|
|
86
|
+
{links.map((l) => (
|
|
87
|
+
<a
|
|
88
|
+
href={l.href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
rel="noopener noreferrer"
|
|
91
|
+
class="rounded-full border border-[var(--aas-border)] bg-[var(--aas-panel)] px-3 py-1 text-sm text-[var(--aas-text)] no-underline hover:bg-[var(--aas-tint)]"
|
|
92
|
+
>
|
|
93
|
+
{l.label}
|
|
94
|
+
<ArrowUpRight size={14} class="ml-0.5 text-[var(--aas-muted)]" />
|
|
95
|
+
</a>
|
|
96
|
+
))}
|
|
97
|
+
</p>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
<p class="mt-2 aas-md text-lg whitespace-pre-line text-[var(--aas-muted)]" set:html={inlineMd(data.description)} />
|
|
101
|
+
{
|
|
102
|
+
data.image && (
|
|
103
|
+
<Image
|
|
104
|
+
src={data.image}
|
|
105
|
+
alt={data.imageAlt ?? data.title}
|
|
106
|
+
class="mt-5 aspect-[2/1] w-full rounded-2xl border border-[var(--aas-border)] object-cover"
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
</header>
|
|
111
|
+
|
|
112
|
+
<RelatedPapers lang={lang} papers={relatedPapers} />
|
|
113
|
+
|
|
114
|
+
<div class="mt-8 flex items-start gap-8">
|
|
115
|
+
<article class="prose max-w-none aas-reading min-w-0 flex-1">
|
|
116
|
+
<Content />
|
|
117
|
+
</article>
|
|
118
|
+
<TocRail lang={lang} items={tocItems} />
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<RelatedTools lang={lang} slugs={data.tools} />
|
|
122
|
+
|
|
123
|
+
{bodyHasMermaid && <MermaidLoader />}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import PaperCard from './PaperCard.astro';
|
|
4
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
5
|
+
import ListControls from './ListControls.astro';
|
|
6
|
+
import ArrowUpRight from './ArrowUpRight.astro';
|
|
7
|
+
|
|
8
|
+
const PAGE = 12;
|
|
9
|
+
import { paperTree, paperCatOf } from '@aas-data/paper-categories';
|
|
10
|
+
import { getPapers, paperSlugOf, type PaperEntry } from '../lib/papers';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
12
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
|
+
|
|
14
|
+
// Doubles as the papers index (no `categoryId` → all papers, grouped by
|
|
15
|
+
// category) and a category browse page (`categoryId` → that category, with a
|
|
16
|
+
// breadcrumb, its direct papers, and a section per subcategory).
|
|
17
|
+
interface Props {
|
|
18
|
+
lang: Lang;
|
|
19
|
+
categoryId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { lang, categoryId } = Astro.props;
|
|
23
|
+
const t = useTranslations(lang);
|
|
24
|
+
// getPapers() sorts newest publication year first, matching the client-side
|
|
25
|
+
// "recent" comparator over PaperCard's data-date (= year) — no reflow on load.
|
|
26
|
+
const all = (await getPapers(lang)).filter((e) => listedInIndex(e.data));
|
|
27
|
+
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `paper/category/${cid}/`);
|
|
28
|
+
|
|
29
|
+
const itemsIn = (cid: string): PaperEntry[] => {
|
|
30
|
+
const ids = new Set(paperTree.descendantIds(cid));
|
|
31
|
+
return all.filter((c) => ids.has(paperCatOf(c.data.category)));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const node = categoryId ? paperTree.map.get(categoryId) : undefined;
|
|
35
|
+
const sections = node ? paperTree.childrenOf(node.id) : paperTree.roots;
|
|
36
|
+
const direct = node ? all.filter((c) => paperCatOf(c.data.category) === node.id) : [];
|
|
37
|
+
|
|
38
|
+
// A leaf category page shows one flat list — that grid gets ?page= pagination.
|
|
39
|
+
// Index and parent-category pages stay grouped overviews: each section is
|
|
40
|
+
// capped to PAGE cards with a "see all" link to the fuller list.
|
|
41
|
+
const isLeaf = !!node && sections.length === 0;
|
|
42
|
+
|
|
43
|
+
const path = node ? paperTree.pathOf(node.id) : [];
|
|
44
|
+
const crumbs = node
|
|
45
|
+
? [
|
|
46
|
+
{ label: t('paper.backToPapers'), href: getRelativeLocaleUrl(lang, 'paper/') },
|
|
47
|
+
...path.map((c, i) => ({
|
|
48
|
+
label: c.label[lang],
|
|
49
|
+
href: i === path.length - 1 ? undefined : catUrl(c.id),
|
|
50
|
+
})),
|
|
51
|
+
]
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
const card = (c: PaperEntry) => ({
|
|
55
|
+
lang,
|
|
56
|
+
slug: paperSlugOf(c),
|
|
57
|
+
title: c.data.title,
|
|
58
|
+
description: c.data.description,
|
|
59
|
+
authors: c.data.authors,
|
|
60
|
+
year: c.data.year,
|
|
61
|
+
venue: c.data.venue,
|
|
62
|
+
openSource: c.data.openSource,
|
|
63
|
+
tags: c.data.tags,
|
|
64
|
+
image: c.data.image,
|
|
65
|
+
imageAlt: c.data.imageAlt,
|
|
66
|
+
isPrivate: c.data.private,
|
|
67
|
+
teaser: c.data.teaser,
|
|
68
|
+
});
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
node ? (
|
|
73
|
+
<>
|
|
74
|
+
<Breadcrumb segments={crumbs} />
|
|
75
|
+
<header class="mt-4">
|
|
76
|
+
<h1 class="text-3xl font-bold tracking-tight">{node.label[lang]}</h1>
|
|
77
|
+
<p class="mt-2 max-w-2xl text-lg text-[var(--aas-muted)]">
|
|
78
|
+
{node.detail?.[lang] ?? node.description[lang]}
|
|
79
|
+
</p>
|
|
80
|
+
</header>
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
<section class="py-6">
|
|
84
|
+
<h1 class="text-4xl font-bold tracking-tight">{t('paper.title')}</h1>
|
|
85
|
+
<p class="mt-3 max-w-2xl text-lg text-[var(--aas-muted)]">{t('paper.tagline')}</p>
|
|
86
|
+
</section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('paper.empty')}</p>}
|
|
91
|
+
|
|
92
|
+
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
direct.length > 0 && (
|
|
96
|
+
<div
|
|
97
|
+
class="mt-4 grid gap-5 sm:grid-cols-2 lg:grid-cols-3"
|
|
98
|
+
data-sortgrid
|
|
99
|
+
data-paginate={isLeaf ? true : undefined}
|
|
100
|
+
data-page-size={isLeaf ? PAGE : undefined}
|
|
101
|
+
>
|
|
102
|
+
{direct.map((c) => (
|
|
103
|
+
<PaperCard {...card(c)} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
sections.map((cat) => {
|
|
111
|
+
const items = itemsIn(cat.id);
|
|
112
|
+
return (
|
|
113
|
+
items.length > 0 && (
|
|
114
|
+
<section class="mt-8">
|
|
115
|
+
<h2 class="text-2xl font-semibold">
|
|
116
|
+
<a
|
|
117
|
+
href={catUrl(cat.id)}
|
|
118
|
+
class="text-[var(--aas-text)] no-underline hover:text-[var(--aas-accent)]"
|
|
119
|
+
>
|
|
120
|
+
{cat.label[lang]}
|
|
121
|
+
<ArrowUpRight size={20} class="ml-1 text-[var(--aas-muted)]" />
|
|
122
|
+
</a>
|
|
123
|
+
</h2>
|
|
124
|
+
<p class="mt-1 text-sm text-[var(--aas-muted)]">{cat.description[lang]}</p>
|
|
125
|
+
<div class="mt-4 grid gap-5 sm:grid-cols-2 lg:grid-cols-3" data-sortgrid>
|
|
126
|
+
{items.slice(0, PAGE).map((c) => (
|
|
127
|
+
<PaperCard {...card(c)} />
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
{items.length > PAGE && (
|
|
131
|
+
<a
|
|
132
|
+
href={catUrl(cat.id)}
|
|
133
|
+
class="mt-3 inline-block text-sm font-medium text-[var(--aas-accent)] no-underline"
|
|
134
|
+
>
|
|
135
|
+
{t('list.seeAll').replace('{n}', String(items.length))} →
|
|
136
|
+
</a>
|
|
137
|
+
)}
|
|
138
|
+
</section>
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
{isLeaf && <nav data-pager aria-label="Pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" />}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
8
8
|
import { productTree, productCatOf } from '@aas-data/product-categories';
|
|
9
9
|
import { getTopProducts, productSlugOf, type ProductEntry } from '../lib/products';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -15,7 +16,7 @@ interface Props {
|
|
|
15
16
|
|
|
16
17
|
const { lang } = Astro.props;
|
|
17
18
|
const t = useTranslations(lang);
|
|
18
|
-
const all = await getTopProducts(lang);
|
|
19
|
+
const all = (await getTopProducts(lang)).filter((e) => listedInIndex(e.data));
|
|
19
20
|
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
20
21
|
const withBase = (p?: string) => (p && p.startsWith('/') ? base + p : p);
|
|
21
22
|
|
|
@@ -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',
|
|
@@ -129,6 +130,17 @@ export const ui = {
|
|
|
129
130
|
'course.updated': 'Updated',
|
|
130
131
|
'course.relatedCourses': 'Related courses',
|
|
131
132
|
'course.slides': 'Slides',
|
|
133
|
+
'paper.title': 'Papers',
|
|
134
|
+
'paper.tagline': 'Paper readings and reviews — what matters and why.',
|
|
135
|
+
'paper.empty': 'No papers yet.',
|
|
136
|
+
'paper.backToPapers': 'All papers',
|
|
137
|
+
'paper.authors': 'Authors',
|
|
138
|
+
'paper.openSource': 'Code available',
|
|
139
|
+
'paper.arxiv': 'arXiv',
|
|
140
|
+
'paper.paper': 'Paper',
|
|
141
|
+
'paper.code': 'Code',
|
|
142
|
+
'paper.relatedPapers': 'Related papers',
|
|
143
|
+
'paper.updated': 'Updated',
|
|
132
144
|
'products.title': 'Products',
|
|
133
145
|
'products.tagline': 'What we build and offer.',
|
|
134
146
|
'products.empty': 'No products yet.',
|
|
@@ -224,6 +236,7 @@ export const ui = {
|
|
|
224
236
|
'nav.concepts': '개념',
|
|
225
237
|
'nav.courses': '강의',
|
|
226
238
|
'nav.products': '제품',
|
|
239
|
+
'nav.papers': '논문',
|
|
227
240
|
'nav.blog': '글',
|
|
228
241
|
'nav.samples': '샘플',
|
|
229
242
|
'nav.slides': '슬라이드',
|
|
@@ -292,6 +305,17 @@ export const ui = {
|
|
|
292
305
|
'course.updated': '업데이트',
|
|
293
306
|
'course.relatedCourses': '관련 강의',
|
|
294
307
|
'course.slides': '슬라이드',
|
|
308
|
+
'paper.title': '논문',
|
|
309
|
+
'paper.tagline': '논문을 읽고 정리합니다 — 무엇이, 왜 중요한지.',
|
|
310
|
+
'paper.empty': '아직 논문이 없습니다.',
|
|
311
|
+
'paper.backToPapers': '논문 전체',
|
|
312
|
+
'paper.authors': '저자',
|
|
313
|
+
'paper.openSource': '코드 공개',
|
|
314
|
+
'paper.arxiv': 'arXiv',
|
|
315
|
+
'paper.paper': '원문',
|
|
316
|
+
'paper.code': '코드',
|
|
317
|
+
'paper.relatedPapers': '관련 논문',
|
|
318
|
+
'paper.updated': '업데이트',
|
|
295
319
|
'products.title': '제품',
|
|
296
320
|
'products.tagline': '우리가 만들고 제공하는 것.',
|
|
297
321
|
'products.empty': '아직 제품이 없습니다.',
|
|
@@ -95,6 +95,12 @@ const allNavItems: {
|
|
|
95
95
|
section: 'courses',
|
|
96
96
|
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
97
|
},
|
|
98
|
+
{
|
|
99
|
+
href: getRelativeLocaleUrl(lang, 'paper/'),
|
|
100
|
+
label: t('nav.papers'),
|
|
101
|
+
section: 'papers' as SectionKey,
|
|
102
|
+
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"/>',
|
|
103
|
+
},
|
|
98
104
|
{
|
|
99
105
|
href: getRelativeLocaleUrl(lang, 'concept/'),
|
|
100
106
|
label: t('nav.concepts'),
|
|
@@ -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
|
+
}
|
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>
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { useTranslations } from '../../../i18n/ui';
|
|
7
|
+
import { allLocales, langParam } from '../../../lib/locales';
|
|
8
|
+
|
|
9
|
+
export const getStaticPaths = (() =>
|
|
10
|
+
allLocales.map((lang) => ({ params: { lang: langParam(lang) }, props: { lang } }))) satisfies GetStaticPaths;
|
|
11
|
+
|
|
12
|
+
const { lang } = Astro.props;
|
|
13
|
+
const t = useTranslations(lang);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<BaseLayout title={`${t('paper.title')} — ${site.name}`} lang={lang} path="paper/">
|
|
17
|
+
<PapersIndex lang={lang} />
|
|
18
|
+
</BaseLayout>
|
package/src/styles/global.css
CHANGED
|
@@ -2384,3 +2384,11 @@ summary {
|
|
|
2384
2384
|
.aas-embed.running .aas-embed-run {
|
|
2385
2385
|
display: none;
|
|
2386
2386
|
}
|
|
2387
|
+
|
|
2388
|
+
/* Tables: keep the first (label) column on one line — a wide table otherwise
|
|
2389
|
+
squeezes short labels like "Part 1" into two rows. Applies to reading prose
|
|
2390
|
+
and slide-deck tables alike; label columns are short by convention. */
|
|
2391
|
+
.prose :where(table) :where(th, td):first-child,
|
|
2392
|
+
.aas-slide :where(table) :where(th, td):first-child {
|
|
2393
|
+
white-space: nowrap;
|
|
2394
|
+
}
|