stack-site-builder 1.10.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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dev/local-samples-client.js +284 -0
  4. package/dev/local-samples.mjs +594 -0
  5. package/index.d.ts +12 -0
  6. package/index.mjs +124 -0
  7. package/markdown.mjs +394 -0
  8. package/package.json +52 -0
  9. package/src/components/ArrowUpRight.astro +24 -0
  10. package/src/components/ArticleCard.astro +56 -0
  11. package/src/components/ArticleDetail.astro +100 -0
  12. package/src/components/ArticleLink.astro +45 -0
  13. package/src/components/BackToTop.astro +47 -0
  14. package/src/components/BlogIndex.astro +137 -0
  15. package/src/components/Breadcrumb.astro +37 -0
  16. package/src/components/CategoryIndex.astro +115 -0
  17. package/src/components/ChevronDown.astro +23 -0
  18. package/src/components/CodeSamples.astro +141 -0
  19. package/src/components/CollapsibleGrid.astro +50 -0
  20. package/src/components/ConceptCard.astro +59 -0
  21. package/src/components/ConceptDetail.astro +102 -0
  22. package/src/components/ConceptIndex.astro +143 -0
  23. package/src/components/ConceptLink.astro +43 -0
  24. package/src/components/ConceptTools.astro +49 -0
  25. package/src/components/DeckView.astro +772 -0
  26. package/src/components/DetailTabs.astro +219 -0
  27. package/src/components/FilterControls.astro +407 -0
  28. package/src/components/Glossary.astro +264 -0
  29. package/src/components/Home.astro +90 -0
  30. package/src/components/LanguageSwitcher.astro +86 -0
  31. package/src/components/ListControls.astro +182 -0
  32. package/src/components/MermaidLoader.astro +96 -0
  33. package/src/components/PricingSection.astro +100 -0
  34. package/src/components/ProjectFileList.astro +68 -0
  35. package/src/components/ProjectFileTree.astro +95 -0
  36. package/src/components/ProjectRelatedTools.astro +70 -0
  37. package/src/components/ProjectViewer.astro +431 -0
  38. package/src/components/RelatedArticles.astro +52 -0
  39. package/src/components/RelatedConcepts.astro +60 -0
  40. package/src/components/RelatedTools.astro +55 -0
  41. package/src/components/SampleIndex.astro +464 -0
  42. package/src/components/SamplePage.astro +95 -0
  43. package/src/components/SampleProject.astro +56 -0
  44. package/src/components/Slide.astro +36 -0
  45. package/src/components/SlidesIndex.astro +60 -0
  46. package/src/components/StackCard.astro +183 -0
  47. package/src/components/StackDetail.astro +507 -0
  48. package/src/components/StarIcon.astro +21 -0
  49. package/src/components/TagIndex.astro +51 -0
  50. package/src/components/ThemeToggle.astro +106 -0
  51. package/src/components/Toc.astro +39 -0
  52. package/src/components/TocRail.astro +74 -0
  53. package/src/components/ToolAvatar.astro +133 -0
  54. package/src/components/ToolMeta.astro +95 -0
  55. package/src/components/VendorIndex.astro +51 -0
  56. package/src/content.ts +228 -0
  57. package/src/i18n/ui.ts +337 -0
  58. package/src/layouts/BaseLayout.astro +287 -0
  59. package/src/lib/articles.ts +27 -0
  60. package/src/lib/assets.ts +19 -0
  61. package/src/lib/category-tree.ts +64 -0
  62. package/src/lib/concepts.ts +35 -0
  63. package/src/lib/dates.ts +21 -0
  64. package/src/lib/facets.ts +57 -0
  65. package/src/lib/github.ts +186 -0
  66. package/src/lib/inline-md.ts +30 -0
  67. package/src/lib/pricing.ts +22 -0
  68. package/src/lib/project.ts +309 -0
  69. package/src/lib/samples.ts +88 -0
  70. package/src/lib/slides.ts +25 -0
  71. package/src/lib/stacks.ts +129 -0
  72. package/src/lib/toc-rail-client.ts +79 -0
  73. package/src/pages/article/[...id].astro +24 -0
  74. package/src/pages/article/category/[id].astro +17 -0
  75. package/src/pages/article/index.astro +12 -0
  76. package/src/pages/categories/[id].astro +25 -0
  77. package/src/pages/concept/[...id].astro +24 -0
  78. package/src/pages/concept/category/[id].astro +17 -0
  79. package/src/pages/concept/index.astro +12 -0
  80. package/src/pages/glossary.astro +12 -0
  81. package/src/pages/index.astro +9 -0
  82. package/src/pages/ko/article/[...id].astro +24 -0
  83. package/src/pages/ko/article/category/[id].astro +17 -0
  84. package/src/pages/ko/article/index.astro +12 -0
  85. package/src/pages/ko/categories/[id].astro +24 -0
  86. package/src/pages/ko/concept/[...id].astro +24 -0
  87. package/src/pages/ko/concept/category/[id].astro +17 -0
  88. package/src/pages/ko/concept/index.astro +12 -0
  89. package/src/pages/ko/glossary.astro +12 -0
  90. package/src/pages/ko/index.astro +9 -0
  91. package/src/pages/ko/sample/[folder].astro +16 -0
  92. package/src/pages/ko/sample/index.astro +12 -0
  93. package/src/pages/ko/slides/[deck].astro +14 -0
  94. package/src/pages/ko/slides/index.astro +12 -0
  95. package/src/pages/ko/stack/[...id].astro +51 -0
  96. package/src/pages/ko/tags/[tag].astro +18 -0
  97. package/src/pages/ko/vendors/[vendor].astro +18 -0
  98. package/src/pages/sample/[folder].astro +16 -0
  99. package/src/pages/sample/index.astro +12 -0
  100. package/src/pages/slides/[deck].astro +14 -0
  101. package/src/pages/slides/index.astro +12 -0
  102. package/src/pages/stack/[...id].astro +51 -0
  103. package/src/pages/tags/[tag].astro +18 -0
  104. package/src/pages/vendors/[vendor].astro +18 -0
  105. package/src/styles/global.css +2350 -0
@@ -0,0 +1,39 @@
1
+ ---
2
+ interface Item {
3
+ slug: string;
4
+ text: string;
5
+ depth: number;
6
+ }
7
+
8
+ interface Props {
9
+ items: Item[];
10
+ title: string;
11
+ }
12
+
13
+ const { items, title } = Astro.props;
14
+ ---
15
+
16
+ {
17
+ items.length > 0 && (
18
+ <nav aria-label={title} class="text-sm">
19
+ <p class="mb-2 text-xs font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
20
+ {title}
21
+ </p>
22
+ <ul class="space-y-0.5 rounded-xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-1.5">
23
+ {items.map((it) => (
24
+ <li>
25
+ <a
26
+ href={`#${it.slug}`}
27
+ class:list={[
28
+ 'block rounded-lg py-1 pr-2 text-[var(--aas-muted)] no-underline hover:bg-[var(--aas-tint)] hover:text-[var(--aas-text)]',
29
+ it.depth >= 3 ? 'pl-5' : 'pl-2.5',
30
+ ]}
31
+ >
32
+ {it.text}
33
+ </a>
34
+ </li>
35
+ ))}
36
+ </ul>
37
+ </nav>
38
+ )
39
+ }
@@ -0,0 +1,74 @@
1
+ ---
2
+ // A self-contained fixed right-rail table of contents (with scroll-spy and a
3
+ // back-to-top button) for single-column detail pages — concepts and articles.
4
+ // (StackDetail has its own tab-aware rail.)
5
+ import Toc from './Toc.astro';
6
+ import ProjectRelatedTools, { type RelatedTool } from './ProjectRelatedTools.astro';
7
+ import ProjectFileList from './ProjectFileList.astro';
8
+ import { useTranslations, type Lang } from '../i18n/ui';
9
+
10
+ interface Item {
11
+ slug: string;
12
+ text: string;
13
+ depth: number;
14
+ }
15
+ interface Props {
16
+ lang: Lang;
17
+ items: Item[];
18
+ // Optional: when an article embeds a runnable sample, surface its related
19
+ // tools + file list here, mirroring a stack's implementation rail. The file
20
+ // buttons wire to the body's <SampleProject/> viewer (matched by folder).
21
+ relatedTools?: RelatedTool[];
22
+ projectFiles?: { folder: string; files: { path: string }[] };
23
+ }
24
+
25
+ const { lang, items, relatedTools = [], projectFiles } = Astro.props;
26
+ const t = useTranslations(lang);
27
+ const hasContent = items.length > 0 || relatedTools.length > 0 || Boolean(projectFiles);
28
+ ---
29
+
30
+ {
31
+ hasContent && (
32
+ <aside class="aas-toc-rail" data-toc-rail>
33
+ <div class="aas-toc-scroll">
34
+ {items.length > 0 && <Toc items={items} title={t('toc.onThisPage')} />}
35
+ {relatedTools.length > 0 && (
36
+ <div class="mt-5">
37
+ <ProjectRelatedTools lang={lang} tools={relatedTools} />
38
+ </div>
39
+ )}
40
+ {projectFiles && (
41
+ <div class="mt-5">
42
+ <ProjectFileList lang={lang} folder={projectFiles.folder} files={projectFiles.files} />
43
+ </div>
44
+ )}
45
+ </div>
46
+ <div class="aas-toc-foot">
47
+ <button type="button" class="aas-toc-top" data-back-to-top>
48
+ ↑ {t('top.label')}
49
+ </button>
50
+ <button
51
+ type="button"
52
+ class="aas-toc-allbtn"
53
+ data-toggle-all
54
+ data-collapse={t('toc.collapseAll')}
55
+ data-expand={t('toc.expandAll')}
56
+ hidden
57
+ >
58
+ {t('toc.collapseAll')}
59
+ </button>
60
+ </div>
61
+ </aside>
62
+ )
63
+ }
64
+
65
+ <script>
66
+ import { startScrollSpy, initBackToTop, initReveals } from '../lib/toc-rail-client';
67
+
68
+ const rail = document.querySelector<HTMLElement>('[data-toc-rail]');
69
+ if (rail) {
70
+ startScrollSpy(rail);
71
+ initBackToTop(rail);
72
+ initReveals(rail);
73
+ }
74
+ </script>
@@ -0,0 +1,133 @@
1
+ ---
2
+ import { rootIdOf } from '@aas-data/categories';
3
+ import { isImageRef, resolveImageUrl } from '../lib/assets';
4
+
5
+ interface Props {
6
+ name: string;
7
+ /** Optional logo image (URL or /public path). Falls back to a category icon. */
8
+ logo?: string;
9
+ /** Optional dark-theme variant of `logo`; swapped in via CSS under data-theme. */
10
+ logoDark?: string;
11
+ category?: string;
12
+ /** Named fallback glyph when there's no logo (overrides the category icon).
13
+ * Used for non-stack avatars, e.g. glossary concept/external terms. */
14
+ fallback?: string;
15
+ size?: number;
16
+ class?: string;
17
+ }
18
+
19
+ const { name, logo, logoDark, category, fallback, size = 44, class: className } = Astro.props;
20
+
21
+ // Lucide-style line icons per category (inner SVG markup; 24×24 viewBox).
22
+ const icons: Record<string, string> = {
23
+ frameworks:
24
+ '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/>',
25
+ 'coding-agents':
26
+ '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
27
+ 'llm-providers':
28
+ '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
29
+ 'vector-stores':
30
+ '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/>',
31
+ observability: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
32
+ // A concept/idea (lightbulb), an article (file-text, matching the Writing nav
33
+ // icon), an external link (globe), and a plain term (book) — glossary
34
+ // fallbacks for avatars without a logo.
35
+ concept:
36
+ '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
37
+ article:
38
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/>',
39
+ external:
40
+ '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
41
+ term: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/>',
42
+ default:
43
+ '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
44
+ };
45
+ // Priority: an explicit `fallback` glyph, else the top-level category's icon
46
+ // (subcategory resolved to its root), else the generic default.
47
+ const iconKey = category ? rootIdOf(category) : undefined;
48
+ const icon = (fallback && icons[fallback]) || (iconKey && icons[iconKey]) || icons.default;
49
+
50
+ // A `logo` is an image when it's a URL or a /public path; otherwise it's an
51
+ // emoji/text glyph (e.g. "🚅"). Anything else falls back to the category icon.
52
+ const isImage = isImageRef(logo);
53
+ const logoSrc = isImage ? resolveImageUrl(logo) : undefined;
54
+ // Dark-theme variant (only meaningful for a non-mono image logo).
55
+ const logoDarkSrc = isImage && logoDark ? resolveImageUrl(logoDark) : undefined;
56
+ const emoji = logo && !isImage ? logo : undefined;
57
+ // A `-mono` suffix (e.g. `/logos/instructor-mono.svg`) marks a single-colour
58
+ // logo: render it as a mask tinted with the theme text colour so it stays
59
+ // visible in both light and dark — including the manual theme toggle.
60
+ const isMono = isImage && /-mono\.[a-z0-9]+$/i.test(logo!);
61
+ // A `-wbg` suffix (e.g. `/logos/lancedb-wbg.png`) sits the logo on a white,
62
+ // padded tile so a dark or transparent logo stays visible on the dark theme.
63
+ const isWbg = isImage && /-wbg\.[a-z0-9]+$/i.test(logo!);
64
+
65
+ const boxStyle = `width:${size}px;height:${size}px`;
66
+ const imgStyle = isWbg
67
+ ? `${boxStyle};background:#fff;padding:${Math.round(size * 0.12)}px`
68
+ : boxStyle;
69
+ const iconSize = Math.round(size * 0.52);
70
+ const tileClass =
71
+ 'flex shrink-0 items-center justify-center rounded-lg border border-[var(--aas-border)] bg-[var(--aas-tint)] text-[var(--aas-muted)]';
72
+ ---
73
+
74
+ {
75
+ isMono ? (
76
+ <span class:list={[tileClass, className]} style={boxStyle} aria-hidden="true">
77
+ <span
78
+ style={`display:block;width:${iconSize}px;height:${iconSize}px;background-color:var(--aas-text);-webkit-mask:url("${logoSrc}") center/contain no-repeat;mask:url("${logoSrc}") center/contain no-repeat`}
79
+ />
80
+ </span>
81
+ ) : isImage ? (
82
+ logoDarkSrc ? (
83
+ <Fragment>
84
+ <img
85
+ src={logoSrc}
86
+ alt={name}
87
+ width={size}
88
+ height={size}
89
+ loading="lazy"
90
+ class:list={['aas-logo-light shrink-0 rounded-lg object-contain', className]}
91
+ style={imgStyle}
92
+ />
93
+ <img
94
+ src={logoDarkSrc}
95
+ alt={name}
96
+ width={size}
97
+ height={size}
98
+ loading="lazy"
99
+ class:list={['aas-logo-dark shrink-0 rounded-lg object-contain', className]}
100
+ style={imgStyle}
101
+ />
102
+ </Fragment>
103
+ ) : (
104
+ <img
105
+ src={logoSrc}
106
+ alt={name}
107
+ width={size}
108
+ height={size}
109
+ loading="lazy"
110
+ class:list={['shrink-0 rounded-lg object-contain', className]}
111
+ style={imgStyle}
112
+ />
113
+ )
114
+ ) : emoji ? (
115
+ <span class:list={[tileClass, className]} style={boxStyle} aria-hidden="true">
116
+ <span style={`font-size:${Math.round(size * 0.52)}px;line-height:1`}>{emoji}</span>
117
+ </span>
118
+ ) : (
119
+ <span class:list={[tileClass, className]} style={boxStyle} aria-hidden="true">
120
+ <svg
121
+ width={iconSize}
122
+ height={iconSize}
123
+ viewBox="0 0 24 24"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ stroke-width="2"
127
+ stroke-linecap="round"
128
+ stroke-linejoin="round"
129
+ set:html={icon}
130
+ />
131
+ </span>
132
+ )
133
+ }
@@ -0,0 +1,95 @@
1
+ ---
2
+ import { pricingLabels, licenseLabel, useTranslations, type Lang } from '../i18n/ui';
3
+ import { formatStars, type Staleness } from '../lib/github';
4
+ import { pricingTags, COMPLETELY_FREE } from '../lib/pricing';
5
+ import StarIcon from './StarIcon.astro';
6
+
7
+ interface Props {
8
+ lang: Lang;
9
+ pricing: string[];
10
+ license?: string;
11
+ language?: string;
12
+ stars?: number | null;
13
+ version?: string | null;
14
+ released?: string | null; // YYYY-MM-DD of the latest release
15
+ deprecated?: boolean;
16
+ staleness?: Staleness; // 'stale' (1y+, stronger) or 'aging' (6m+, milder) warning
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ lang,
22
+ pricing,
23
+ license,
24
+ language,
25
+ stars,
26
+ version,
27
+ released,
28
+ deprecated,
29
+ staleness,
30
+ class: className,
31
+ } = Astro.props;
32
+ const t = useTranslations(lang);
33
+ // "Maintenance only" and the staleness warning are independent signals and may
34
+ // both apply (e.g. a superseded tool whose repo also went quiet).
35
+ const showStale = staleness === 'stale';
36
+ const showAging = staleness === 'aging';
37
+ const hasAny =
38
+ deprecated ||
39
+ showStale ||
40
+ showAging ||
41
+ pricing.length > 0 ||
42
+ license ||
43
+ language ||
44
+ typeof stars === 'number' ||
45
+ version ||
46
+ released;
47
+
48
+ // Filled "surface" chips with a faint outline; a touch taller for presence.
49
+ const chip = 'aas-chip rounded-lg px-2.5 py-1';
50
+ ---
51
+
52
+ {
53
+ hasAny && (
54
+ <div class:list={['flex flex-wrap items-center gap-1.5 text-xs', className]}>
55
+ {deprecated && (
56
+ <span class="aas-deprecated rounded-lg px-2.5 py-1 font-medium">{t('meta.deprecated')}</span>
57
+ )}
58
+ {showStale && (
59
+ <span title={t('meta.staleHint')} class="aas-stale rounded-lg px-2.5 py-1 font-medium">
60
+ {t('meta.stale')}
61
+ </span>
62
+ )}
63
+ {showAging && (
64
+ <span title={t('meta.agingHint')} class="aas-aging rounded-lg px-2.5 py-1 font-medium">
65
+ {t('meta.aging')}
66
+ </span>
67
+ )}
68
+ {typeof stars === 'number' && (
69
+ <span class:list={['inline-flex items-center gap-1', chip]}>
70
+ <StarIcon class="text-[var(--aas-text)]" />
71
+ {formatStars(stars)}
72
+ </span>
73
+ )}
74
+ {version && <span class:list={['font-mono', chip]}>{version}</span>}
75
+ {released && (
76
+ <span title={t('meta.released')} class:list={['inline-flex items-center gap-1', chip]}>
77
+ <span aria-hidden="true">↻</span>
78
+ {released}
79
+ </span>
80
+ )}
81
+ {language && <span class={chip}>{language}</span>}
82
+ {pricingTags(pricing).map((p) => (
83
+ <span class:list={[p === COMPLETELY_FREE ? 'aas-free' : 'aas-pricing', 'rounded-lg px-2.5 py-1']}>
84
+ {pricingLabels[lang][p] ?? p}
85
+ </span>
86
+ ))}
87
+ {license && (
88
+ <span class:list={['inline-flex items-center gap-1', chip]}>
89
+ <span aria-hidden="true">⚖</span>
90
+ {licenseLabel(lang, license)}
91
+ </span>
92
+ )}
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ import { getRelativeLocaleUrl } from 'astro:i18n';
3
+ import StackCard from './StackCard.astro';
4
+ import { getStacksByVendor, slugOf } from '../lib/stacks';
5
+ import { useTranslations, type Lang } from '../i18n/ui';
6
+
7
+ interface Props {
8
+ lang: Lang;
9
+ vendor: string; // url slug
10
+ }
11
+
12
+ const { lang, vendor } = Astro.props;
13
+ const t = useTranslations(lang);
14
+ const entries = await getStacksByVendor(lang, vendor);
15
+ // The display name is whatever the entries wrote (e.g. "Hugging Face").
16
+ const vendorName = entries[0]?.data.vendor ?? vendor;
17
+ ---
18
+
19
+ <section class="py-6">
20
+ <a href={getRelativeLocaleUrl(lang, '')} class="text-sm text-[var(--aas-accent)] no-underline"
21
+ >&larr; {t('tag.allTools')}</a
22
+ >
23
+ <h1 class="mt-3 text-3xl font-bold tracking-tight">
24
+ <span class="text-[var(--aas-muted)]">{t('vendor.heading')}</span> {vendorName}
25
+ </h1>
26
+ </section>
27
+
28
+ <div class="mt-4 grid gap-4 sm:grid-cols-2">
29
+ {
30
+ entries.map((e) => (
31
+ <StackCard
32
+ slug={slugOf(e)}
33
+ lang={lang}
34
+ name={e.data.name}
35
+ formerNames={e.data.formerNames}
36
+ vendor={e.data.vendor}
37
+ description={e.data.description}
38
+ tags={e.data.tags}
39
+ pricing={e.data.pricing}
40
+ license={e.data.license}
41
+ language={e.data.language}
42
+ category={e.data.category}
43
+ logo={e.data.logo}
44
+ logoDark={e.data.logoDark}
45
+ repo={e.data.repo}
46
+ version={e.data.version}
47
+ deprecated={e.data.deprecated}
48
+ />
49
+ ))
50
+ }
51
+ </div>
package/src/content.ts ADDED
@@ -0,0 +1,228 @@
1
+ import { defineCollection, z } from 'astro:content';
2
+ import { glob } from 'astro/loaders';
3
+
4
+ /**
5
+ * The shared content model for awesome-*-stack sites: `stacks` (the tool
6
+ * catalog), `articles`, `concepts` and `slides`. A site's content.config.ts
7
+ * stays thin:
8
+ *
9
+ * import { defineAasCollections } from 'stack-site-builder/content';
10
+ * import { categoryMap } from './data/categories';
11
+ * export const collections = defineAasCollections({ categoryMap });
12
+ *
13
+ * Content lives in the site's `src/content/<collection>/` (the glob bases
14
+ * below resolve against the site root). `categoryMap` is the site's category
15
+ * tree — stack entries are validated against it so an unknown category id
16
+ * fails the build instead of silently dropping the entry from every listing.
17
+ */
18
+ export function defineAasCollections({ categoryMap }: { categoryMap: Map<string, unknown> }) {
19
+ /**
20
+ * The `stacks` collection holds one entry per tool/service used to build
21
+ * AI agents. Each entry is an MDX file: frontmatter powers the listing and
22
+ * cards, while the body is the per-service detail page (overview + sample code).
23
+ *
24
+ * Entries are locale-partitioned: `stacks/en/<slug>.mdx`, `stacks/ko/<slug>.mdx`.
25
+ */
26
+ const stacks = defineCollection({
27
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/stacks' }),
28
+ schema: z.object({
29
+ name: z.string(),
30
+ // Prior names after a rename/rebrand, newest first. The card shows just the
31
+ // most recent (formerNames[0]); the detail page shows all of them.
32
+ formerNames: z.array(z.string()).default([]),
33
+ // Living-doc metadata for THIS catalog entry's writeup — bump on edits.
34
+ // Distinct from the tool's own software `version` (further below): docVersion
35
+ // tracks our article, `version` tracks the tool's release.
36
+ docVersion: z.string().optional(), // e.g. "1.0"
37
+ updated: z.coerce.date().optional(), // when the writeup was last revised (YYYY-MM-DD)
38
+ // The organization / company / person that makes and maintains the tool
39
+ // (e.g. "Microsoft"). Powers the per-vendor browse pages at /vendors/<slug>.
40
+ vendor: z.string().optional(),
41
+ // Must be a node id from the tree in the site's src/data/categories.ts. Validated here
42
+ // because an unknown id would not error anywhere downstream — the entry
43
+ // would just silently drop out of the homepage and every category page.
44
+ category: z.string().refine((id) => categoryMap.has(id), {
45
+ message: 'unknown category id — must match a node in the site data category tree',
46
+ }),
47
+ description: z.string(),
48
+ logo: z.string().optional(), // optional image URL/path; otherwise a monogram is shown
49
+ logoDark: z.string().optional(), // optional dark-theme variant of `logo`
50
+ website: z.string().url().optional(),
51
+ repo: z.string().url().optional(),
52
+ docs: z.string().url().optional(),
53
+ tags: z.array(z.string()).default([]),
54
+ language: z.string().optional(),
55
+ license: z.string().optional(),
56
+ version: z.string().optional(), // manual fallback; GitHub release/tag is used when available
57
+
58
+ // Composable pricing/hosting model tags (a tool can be several at once):
59
+ // open-source = source is open / free to self-host
60
+ // free-tier = vendor-hosted free tier
61
+ // paid = paid plans exist
62
+ // free = entirely free to use (non-OSS)
63
+ pricing: z.array(z.enum(['open-source', 'free-tier', 'paid', 'free'])).default([]),
64
+ // Human-readable pricing summary shown on the detail page, with its source
65
+ // and the date it was last checked. `pricingNote` is localized per file.
66
+ pricingNote: z.string().optional(), // short intro shown above the tiers table
67
+ pricingTiers: z
68
+ .array(z.object({ plan: z.string(), price: z.string(), note: z.string().optional() }))
69
+ .default([]), // localized plan / price / note rows
70
+ pricingSource: z.string().url().optional(),
71
+ pricingCheckedAt: z.string().optional(), // YYYY-MM-DD
72
+ deprecated: z.boolean().default(false), // maintenance-only / superseded
73
+ related: z.array(z.string()).default([]), // slugs of related tools (same collection)
74
+ // Sample project folders under samples/. Either a bare folder name, or an
75
+ // object pairing the folder with the tools that project uses (slugs of other
76
+ // stacks; unknown slugs render as "not in our catalog").
77
+ // projects: [langgraph_1]
78
+ // projects:
79
+ // - folder: langgraph_1
80
+ // related: [langgraph, langchain]
81
+ projects: z
82
+ .array(
83
+ z.union([
84
+ z.string(),
85
+ z.object({
86
+ folder: z.string(),
87
+ related: z.array(z.string()).default([]),
88
+ }),
89
+ ]),
90
+ )
91
+ .default([]),
92
+
93
+
94
+
95
+ // Versioned code samples for the "Code" tab. Each can carry a Mermaid
96
+ // diagram explaining the structure plus a highlighted code snippet.
97
+ samples: z
98
+ .array(
99
+ z.object({
100
+ version: z.string(), // label shown in the version selector
101
+ lang: z.string().default('ts'), // language id for syntax highlighting
102
+ description: z.string().optional(), // Markdown explanation, shown above the code
103
+ diagram: z.string().optional(), // Mermaid source
104
+ code: z.string(),
105
+ note: z.string().optional(), // short caption below the code
106
+ }),
107
+ )
108
+ .default([]),
109
+ featured: z.boolean().default(false),
110
+ }),
111
+ });
112
+
113
+ /**
114
+ * The `articles` collection is the writing space — long-form posts kept
115
+ * separate from the tool catalog. `tools` lists the stack slugs an article
116
+ * references; it powers both forward links (article → tool) and backlinks
117
+ * (tool → article, computed in src/lib/articles.ts).
118
+ *
119
+ * Locale-partitioned like stacks: `articles/en/<slug>.mdx`, `articles/ko/...`.
120
+ */
121
+ const articles = defineCollection({
122
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/articles' }),
123
+ schema: ({ image }) =>
124
+ z.object({
125
+ title: z.string(),
126
+ description: z.string(),
127
+ date: z.coerce.date(), // first published
128
+ // Living-doc metadata, mirroring concepts: bump on meaningful edits.
129
+ version: z.string().optional(), // e.g. "1.0"
130
+ updated: z.coerce.date().optional(), // last meaningful update (YYYY-MM-DD)
131
+ image: image().optional(), // hero / card image (optimized; relative to the file)
132
+ imageAlt: z.string().optional(),
133
+ // Leaf id from the site's src/data/article-categories.ts (powers the breadcrumb +
134
+ // category browse pages + grouped index, like a stack's category).
135
+ category: z.string().optional(),
136
+ tools: z.array(z.string()).default([]),
137
+ related: z.array(z.string()).default([]), // related article slugs (mutual cross-links)
138
+ // Optional runnable sample (samples/<folder>/) the article walks through.
139
+ // Embed its viewer in the body with <SampleProject folder="…"/>; this
140
+ // field powers the right-rail file list + related tools (like a stack's
141
+ // implementation tab). A bare folder name, or { folder, related: [slugs] }.
142
+ project: z
143
+ .union([
144
+ z.string(),
145
+ z.object({ folder: z.string(), related: z.array(z.string()).default([]) }),
146
+ ])
147
+ .optional(),
148
+ tags: z.array(z.string()).default([]),
149
+ draft: z.boolean().default(false),
150
+ }),
151
+ });
152
+
153
+ /**
154
+ * The `concepts` collection explains higher-level patterns that compose several
155
+ * catalog tools into a working setup (e.g. "harness engineering"). Each concept
156
+ * groups the tools it uses by the role they play, and links related articles.
157
+ *
158
+ * Locale-partitioned like the others: `concepts/<lang>/<slug>.mdx`.
159
+ */
160
+ const concepts = defineCollection({
161
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/concepts' }),
162
+ schema: ({ image }) =>
163
+ z.object({
164
+ title: z.string(),
165
+ description: z.string(),
166
+ image: image().optional(), // hero / card image (optimized; relative to the file)
167
+ imageAlt: z.string().optional(),
168
+ // Leaf id from the site's src/data/concept-categories.ts (powers the breadcrumb +
169
+ // category browse pages + grouped index, like a stack's category).
170
+ category: z.string().optional(),
171
+ // A concept is living documentation: bump `version` and `updated` on edits.
172
+ version: z.string().optional(), // e.g. "1.0"
173
+ updated: z.coerce.date().optional(), // last meaningful update (YYYY-MM-DD)
174
+ // Tools grouped by the role they play in the pattern. `role` is content, so
175
+ // it's written per-locale (e.g. "메모리" / "Memory"); `tools` are stack slugs.
176
+ tools: z
177
+ .array(z.object({ role: z.string(), tools: z.array(z.string()).default([]) }))
178
+ .default([]),
179
+ articles: z.array(z.string()).default([]), // related article slugs
180
+ related: z.array(z.string()).default([]), // related concept slugs
181
+ tags: z.array(z.string()).default([]),
182
+ order: z.number().optional(), // manual sort on the index (lower first)
183
+ draft: z.boolean().default(false),
184
+ }),
185
+ });
186
+
187
+ /**
188
+ * The `slides` collection holds presentation decks — one MDX file per deck, with
189
+ * each slide wrapped in a <Slide> component (see src/components/Slide.astro). The
190
+ * deck renders as a fullscreen scroll-snap presentation at /slides/<name>/.
191
+ *
192
+ * Decks are self-contained and language-agnostic (flat, not locale-partitioned):
193
+ * a deck's prose carries its own language, and both locale indexes link to the
194
+ * same deck. Add per-locale folders later if decks need translating.
195
+ */
196
+ const slides = defineCollection({
197
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/slides' }),
198
+ schema: z.object({
199
+ title: z.string(),
200
+ description: z.string(),
201
+ // Concept slug this deck summarizes (links back to /concept/<slug>/).
202
+ related: z.string().optional(),
203
+ order: z.number().optional(), // manual sort on the index (lower first)
204
+ // Slide-change animation: 'slide' animates the scroll between slides,
205
+ // 'none' cuts instantly. (Reduced-motion users always get an instant cut.)
206
+ transition: z.enum(['slide', 'none']).default('slide'),
207
+ // Visual style theme (typography / accent / spacing). Light vs dark still
208
+ // follows the site's color-scheme preference; this only picks the look.
209
+ theme: z.enum(['default', 'simple', 'calm', 'bold']).default('default'),
210
+ // Content aspect ratio: the content area's width tracks this ratio × height,
211
+ // so 16:9/16:10/4:3 give a proportioned (wider) content column; 'fill' uses
212
+ // the full slide width.
213
+ aspect: z.enum(['16:9', '16:10', '4:3', 'fill']).default('16:9'),
214
+ // Optional footer (bottom-left): presenter name and a date/version string.
215
+ presenter: z.string().optional(),
216
+ date: z.string().optional(),
217
+ // In-deck table of contents (top-right). `toc` toggles it; `toc_level` is the
218
+ // deepest heading level it includes (2 = ## only … up to 4 = ## ### ####);
219
+ // `toc_open` starts it expanded. Exclude a whole slide with <Slide toc={false}>.
220
+ toc: z.boolean().default(true),
221
+ toc_level: z.number().int().min(2).max(4).default(2),
222
+ toc_open: z.boolean().default(true),
223
+ draft: z.boolean().default(false),
224
+ }),
225
+ });
226
+
227
+ return { stacks, articles, concepts, slides };
228
+ }