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.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dev/local-samples-client.js +284 -0
- package/dev/local-samples.mjs +594 -0
- package/index.d.ts +12 -0
- package/index.mjs +124 -0
- package/markdown.mjs +394 -0
- package/package.json +52 -0
- package/src/components/ArrowUpRight.astro +24 -0
- package/src/components/ArticleCard.astro +56 -0
- package/src/components/ArticleDetail.astro +100 -0
- package/src/components/ArticleLink.astro +45 -0
- package/src/components/BackToTop.astro +47 -0
- package/src/components/BlogIndex.astro +137 -0
- package/src/components/Breadcrumb.astro +37 -0
- package/src/components/CategoryIndex.astro +115 -0
- package/src/components/ChevronDown.astro +23 -0
- package/src/components/CodeSamples.astro +141 -0
- package/src/components/CollapsibleGrid.astro +50 -0
- package/src/components/ConceptCard.astro +59 -0
- package/src/components/ConceptDetail.astro +102 -0
- package/src/components/ConceptIndex.astro +143 -0
- package/src/components/ConceptLink.astro +43 -0
- package/src/components/ConceptTools.astro +49 -0
- package/src/components/DeckView.astro +772 -0
- package/src/components/DetailTabs.astro +219 -0
- package/src/components/FilterControls.astro +407 -0
- package/src/components/Glossary.astro +264 -0
- package/src/components/Home.astro +90 -0
- package/src/components/LanguageSwitcher.astro +86 -0
- package/src/components/ListControls.astro +182 -0
- package/src/components/MermaidLoader.astro +96 -0
- package/src/components/PricingSection.astro +100 -0
- package/src/components/ProjectFileList.astro +68 -0
- package/src/components/ProjectFileTree.astro +95 -0
- package/src/components/ProjectRelatedTools.astro +70 -0
- package/src/components/ProjectViewer.astro +431 -0
- package/src/components/RelatedArticles.astro +52 -0
- package/src/components/RelatedConcepts.astro +60 -0
- package/src/components/RelatedTools.astro +55 -0
- package/src/components/SampleIndex.astro +464 -0
- package/src/components/SamplePage.astro +95 -0
- package/src/components/SampleProject.astro +56 -0
- package/src/components/Slide.astro +36 -0
- package/src/components/SlidesIndex.astro +60 -0
- package/src/components/StackCard.astro +183 -0
- package/src/components/StackDetail.astro +507 -0
- package/src/components/StarIcon.astro +21 -0
- package/src/components/TagIndex.astro +51 -0
- package/src/components/ThemeToggle.astro +106 -0
- package/src/components/Toc.astro +39 -0
- package/src/components/TocRail.astro +74 -0
- package/src/components/ToolAvatar.astro +133 -0
- package/src/components/ToolMeta.astro +95 -0
- package/src/components/VendorIndex.astro +51 -0
- package/src/content.ts +228 -0
- package/src/i18n/ui.ts +337 -0
- package/src/layouts/BaseLayout.astro +287 -0
- package/src/lib/articles.ts +27 -0
- package/src/lib/assets.ts +19 -0
- package/src/lib/category-tree.ts +64 -0
- package/src/lib/concepts.ts +35 -0
- package/src/lib/dates.ts +21 -0
- package/src/lib/facets.ts +57 -0
- package/src/lib/github.ts +186 -0
- package/src/lib/inline-md.ts +30 -0
- package/src/lib/pricing.ts +22 -0
- package/src/lib/project.ts +309 -0
- package/src/lib/samples.ts +88 -0
- package/src/lib/slides.ts +25 -0
- package/src/lib/stacks.ts +129 -0
- package/src/lib/toc-rail-client.ts +79 -0
- package/src/pages/article/[...id].astro +24 -0
- package/src/pages/article/category/[id].astro +17 -0
- package/src/pages/article/index.astro +12 -0
- package/src/pages/categories/[id].astro +25 -0
- package/src/pages/concept/[...id].astro +24 -0
- package/src/pages/concept/category/[id].astro +17 -0
- package/src/pages/concept/index.astro +12 -0
- package/src/pages/glossary.astro +12 -0
- package/src/pages/index.astro +9 -0
- package/src/pages/ko/article/[...id].astro +24 -0
- package/src/pages/ko/article/category/[id].astro +17 -0
- package/src/pages/ko/article/index.astro +12 -0
- package/src/pages/ko/categories/[id].astro +24 -0
- package/src/pages/ko/concept/[...id].astro +24 -0
- package/src/pages/ko/concept/category/[id].astro +17 -0
- package/src/pages/ko/concept/index.astro +12 -0
- package/src/pages/ko/glossary.astro +12 -0
- package/src/pages/ko/index.astro +9 -0
- package/src/pages/ko/sample/[folder].astro +16 -0
- package/src/pages/ko/sample/index.astro +12 -0
- package/src/pages/ko/slides/[deck].astro +14 -0
- package/src/pages/ko/slides/index.astro +12 -0
- package/src/pages/ko/stack/[...id].astro +51 -0
- package/src/pages/ko/tags/[tag].astro +18 -0
- package/src/pages/ko/vendors/[vendor].astro +18 -0
- package/src/pages/sample/[folder].astro +16 -0
- package/src/pages/sample/index.astro +12 -0
- package/src/pages/slides/[deck].astro +14 -0
- package/src/pages/slides/index.astro +12 -0
- package/src/pages/stack/[...id].astro +51 -0
- package/src/pages/tags/[tag].astro +18 -0
- package/src/pages/vendors/[vendor].astro +18 -0
- package/src/styles/global.css +2350 -0
package/src/i18n/ui.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central place for everything language-related.
|
|
3
|
+
*
|
|
4
|
+
* Locale routing (configured in astro.config.mjs): `en` is the default and is
|
|
5
|
+
* served at the root, `ko` is served under `/ko/`. Tool content lives in
|
|
6
|
+
* `src/content/stacks/<lang>/`. Use `astro:i18n`'s `getRelativeLocaleUrl` to
|
|
7
|
+
* build locale-aware links so the base path and `/ko` prefix are handled for us.
|
|
8
|
+
*/
|
|
9
|
+
import { site } from '@aas-data/site';
|
|
10
|
+
|
|
11
|
+
export const languages = {
|
|
12
|
+
en: 'English',
|
|
13
|
+
ko: '한국어',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type Lang = keyof typeof languages;
|
|
17
|
+
|
|
18
|
+
export const defaultLang: Lang = 'en';
|
|
19
|
+
|
|
20
|
+
/** UI chrome strings, keyed by a dotted id. */
|
|
21
|
+
export const ui = {
|
|
22
|
+
en: {
|
|
23
|
+
'site.tagline':
|
|
24
|
+
'A curated stack of the tools and services you actually use to build AI systems — each with a detail page and runnable sample code.',
|
|
25
|
+
'nav.browse': 'Browse',
|
|
26
|
+
'nav.concepts': 'Concepts',
|
|
27
|
+
'nav.blog': 'Writing',
|
|
28
|
+
'nav.samples': 'Samples',
|
|
29
|
+
'nav.slides': 'Slides',
|
|
30
|
+
'nav.backToTop': 'Back to top',
|
|
31
|
+
'nav.glossary': 'Glossary',
|
|
32
|
+
'slides.title': 'Slides',
|
|
33
|
+
'slides.tagline': 'Concept decks — the same ideas as the concept pages, in slide form.',
|
|
34
|
+
'slides.deck': 'Deck',
|
|
35
|
+
'slides.open': 'Open deck',
|
|
36
|
+
'slides.readConcept': 'Read the concept',
|
|
37
|
+
'slides.prev': 'Previous slide',
|
|
38
|
+
'slides.next': 'Next slide',
|
|
39
|
+
'slides.contents': 'Contents',
|
|
40
|
+
'slides.menu': 'Menu',
|
|
41
|
+
'slides.print': 'Print / PDF',
|
|
42
|
+
'slides.fullscreen': 'Fullscreen',
|
|
43
|
+
'slides.zoom': 'Enlarge',
|
|
44
|
+
'slides.close': 'Close',
|
|
45
|
+
'slides.overview': 'Overview',
|
|
46
|
+
'sample.title': 'Samples',
|
|
47
|
+
'sample.tagline':
|
|
48
|
+
'The runnable mini-projects behind the catalog — download a folder and run it with Docker.',
|
|
49
|
+
'sample.usedBy': 'Used by',
|
|
50
|
+
'sample.related': 'Related sample',
|
|
51
|
+
'sample.search': 'Search samples…',
|
|
52
|
+
'sample.searchAll': 'Title + body',
|
|
53
|
+
'sample.searchTitle': 'Title',
|
|
54
|
+
'sample.searchBody': 'Body',
|
|
55
|
+
'sample.tools': 'Source tools',
|
|
56
|
+
'sample.noResults': 'No matching samples.',
|
|
57
|
+
'glossary.title': 'Glossary',
|
|
58
|
+
'glossary.tagline': 'Terms used across the catalog and concepts, each linked to its page.',
|
|
59
|
+
'glossary.tool': 'Tools',
|
|
60
|
+
'glossary.concept': 'Concepts',
|
|
61
|
+
'glossary.article': 'Writing',
|
|
62
|
+
'glossary.term': 'Terms',
|
|
63
|
+
'glossary.external': 'External',
|
|
64
|
+
'glossary.search': 'Search terms…',
|
|
65
|
+
'glossary.noResults': 'No matching terms.',
|
|
66
|
+
'search.placeholder': 'Search tools…',
|
|
67
|
+
'concept.title': 'Concepts',
|
|
68
|
+
'concept.tagline': 'Patterns that compose catalog tools into a working AI stack.',
|
|
69
|
+
'concept.empty': 'No concepts yet.',
|
|
70
|
+
'concept.usedTools': 'Tools it uses',
|
|
71
|
+
'concept.relatedConcepts': 'Related concepts',
|
|
72
|
+
'concept.learnMore': 'Learn more',
|
|
73
|
+
'concept.backToConcepts': 'All concepts',
|
|
74
|
+
'concept.updated': 'Updated',
|
|
75
|
+
'detail.usedInConcepts': 'Used in concepts',
|
|
76
|
+
'sort.label': 'Sort',
|
|
77
|
+
'sort.alpha': 'A–Z',
|
|
78
|
+
'sort.recent': 'Recently updated',
|
|
79
|
+
'sort.stars': 'Stars',
|
|
80
|
+
'list.seeAll': 'See all {n}',
|
|
81
|
+
'filter.clear': 'Clear',
|
|
82
|
+
'view.label': 'View',
|
|
83
|
+
'view.standard': 'Standard',
|
|
84
|
+
'view.gallery': 'Gallery',
|
|
85
|
+
'filter.updated': 'Last updated',
|
|
86
|
+
'updated.all': 'Show all',
|
|
87
|
+
'updated.6m': 'Within 6 months',
|
|
88
|
+
'updated.1y': 'Within 1 year',
|
|
89
|
+
'updated.2y': 'Within 2 years',
|
|
90
|
+
'results.none': 'No tools match.',
|
|
91
|
+
'detail.allTools': 'All tools',
|
|
92
|
+
'detail.website': 'Website',
|
|
93
|
+
'detail.repository': 'Repository',
|
|
94
|
+
'detail.docs': 'Docs',
|
|
95
|
+
'detail.language': 'Language',
|
|
96
|
+
'detail.license': 'License',
|
|
97
|
+
'detail.pricing': 'Pricing',
|
|
98
|
+
'detail.source': 'Source',
|
|
99
|
+
'detail.asOf': 'as of',
|
|
100
|
+
'detail.plan': 'Plan',
|
|
101
|
+
'detail.price': 'Price',
|
|
102
|
+
'detail.notes': 'Notes',
|
|
103
|
+
'pricing.stale': 'Prices not checked in 1+ year',
|
|
104
|
+
'pricing.staleHint': 'Pricing was last verified more than a year ago — may be out of date',
|
|
105
|
+
'pricing.aging': 'Prices not checked in 6+ months',
|
|
106
|
+
'pricing.agingHint': 'Pricing was last verified more than six months ago — may be out of date',
|
|
107
|
+
'detail.relatedWriting': 'Related writing',
|
|
108
|
+
'detail.relatedTools': 'Related tools',
|
|
109
|
+
'blog.title': 'Writing',
|
|
110
|
+
'blog.tagline': 'Notes and comparisons on building AI systems, linked to the tools they discuss.',
|
|
111
|
+
'blog.empty': 'No posts yet.',
|
|
112
|
+
'article.backToBlog': 'All writing',
|
|
113
|
+
'article.referencedTools': 'Tools in this article',
|
|
114
|
+
'theme.label': 'Theme',
|
|
115
|
+
'theme.system': 'System',
|
|
116
|
+
'theme.light': 'Light',
|
|
117
|
+
'theme.dark': 'Dark',
|
|
118
|
+
'tab.overview': 'Overview',
|
|
119
|
+
'tab.pricing': 'Pricing',
|
|
120
|
+
'tab.code': 'Code samples',
|
|
121
|
+
'tab.impl': 'Implementation',
|
|
122
|
+
'project.intro': 'A real, runnable mini-project. Download it and run with Docker.',
|
|
123
|
+
// Rendered around the folder path: "View samples/<folder> on GitHub".
|
|
124
|
+
'project.viewOnGitHubPre': 'View',
|
|
125
|
+
'project.viewOnGitHubPost': 'on GitHub',
|
|
126
|
+
// Dev-only twin: "View samples/<folder> locally".
|
|
127
|
+
'project.viewLocallyPre': 'View',
|
|
128
|
+
'project.viewLocallyPost': 'locally',
|
|
129
|
+
'project.example': 'Example',
|
|
130
|
+
'project.files': 'Files',
|
|
131
|
+
'project.relatedTools': 'Related tools',
|
|
132
|
+
'project.unregistered': 'Not in our catalog',
|
|
133
|
+
'project.readmeExpand': 'Show README',
|
|
134
|
+
'project.readmeCollapse': 'Hide README',
|
|
135
|
+
'sample.example': 'Example',
|
|
136
|
+
'sample.diagram': 'Structure',
|
|
137
|
+
'tag.heading': 'Tag',
|
|
138
|
+
'tag.allTools': 'All tools',
|
|
139
|
+
'detail.vendor': 'Vendor',
|
|
140
|
+
'vendor.heading': 'By',
|
|
141
|
+
'toc.onThisPage': 'On this page',
|
|
142
|
+
'toc.collapseAll': 'Collapse all',
|
|
143
|
+
'toc.expandAll': 'Expand all',
|
|
144
|
+
'toc.versions': 'Versions',
|
|
145
|
+
'meta.released': 'Last release',
|
|
146
|
+
'meta.updated': 'Updated',
|
|
147
|
+
'meta.docVersion': 'Doc version',
|
|
148
|
+
'collapse.more': '+{n} more',
|
|
149
|
+
'collapse.less': 'Show less',
|
|
150
|
+
'meta.deprecated': 'Maintenance only',
|
|
151
|
+
'meta.stale': 'No updates in 1+ year',
|
|
152
|
+
'meta.staleHint': 'The latest release is more than a year old',
|
|
153
|
+
'meta.formerly': 'Formerly',
|
|
154
|
+
'meta.aging': 'No updates in 6+ months',
|
|
155
|
+
'meta.agingHint': 'The latest release is more than six months old',
|
|
156
|
+
'top.label': 'Back to top',
|
|
157
|
+
'footer.builtWith': 'Built with',
|
|
158
|
+
'footer.contribute': 'Contributions welcome — add a tool by dropping an MDX file.',
|
|
159
|
+
},
|
|
160
|
+
ko: {
|
|
161
|
+
'site.tagline':
|
|
162
|
+
'AI 시스템을 만들 때 실제로 쓰는 도구와 서비스를 모았습니다 — 각 항목마다 상세 페이지와 바로 실행 가능한 샘플 코드를 제공합니다.',
|
|
163
|
+
'nav.browse': '둘러보기',
|
|
164
|
+
'nav.concepts': '개념',
|
|
165
|
+
'nav.blog': '글',
|
|
166
|
+
'nav.samples': '샘플',
|
|
167
|
+
'nav.slides': '슬라이드',
|
|
168
|
+
'nav.backToTop': '맨 위로',
|
|
169
|
+
'nav.glossary': '용어집',
|
|
170
|
+
'slides.title': '슬라이드',
|
|
171
|
+
'slides.tagline': '개념 덱 — 개념 페이지와 같은 내용을 슬라이드로 옮겼습니다.',
|
|
172
|
+
'slides.deck': '덱',
|
|
173
|
+
'slides.open': '덱 열기',
|
|
174
|
+
'slides.readConcept': '개념 문서 보기',
|
|
175
|
+
'slides.prev': '이전 슬라이드',
|
|
176
|
+
'slides.next': '다음 슬라이드',
|
|
177
|
+
'slides.contents': '목차',
|
|
178
|
+
'slides.menu': '메뉴',
|
|
179
|
+
'slides.print': '인쇄 / PDF',
|
|
180
|
+
'slides.fullscreen': '전체화면',
|
|
181
|
+
'slides.zoom': '크게 보기',
|
|
182
|
+
'slides.close': '닫기',
|
|
183
|
+
'slides.overview': '한눈에 보기',
|
|
184
|
+
'sample.title': '샘플',
|
|
185
|
+
'sample.tagline': '카탈로그 뒤의 실행 가능한 미니 프로젝트 — 폴더를 받아 Docker로 실행해 보세요.',
|
|
186
|
+
'sample.usedBy': '이 샘플을 쓰는 곳',
|
|
187
|
+
'sample.related': '관련 샘플',
|
|
188
|
+
'sample.search': '샘플 검색…',
|
|
189
|
+
'sample.searchAll': '제목+내용',
|
|
190
|
+
'sample.searchTitle': '제목',
|
|
191
|
+
'sample.searchBody': '내용',
|
|
192
|
+
'sample.tools': '사용 도구',
|
|
193
|
+
'sample.noResults': '일치하는 샘플이 없습니다.',
|
|
194
|
+
'glossary.title': '용어집',
|
|
195
|
+
'glossary.tagline': '카탈로그와 개념 전반에서 쓰는 용어, 그리고 각 용어가 가리키는 페이지.',
|
|
196
|
+
'glossary.tool': '도구',
|
|
197
|
+
'glossary.concept': '개념',
|
|
198
|
+
'glossary.article': '글',
|
|
199
|
+
'glossary.term': '용어',
|
|
200
|
+
'glossary.external': '외부',
|
|
201
|
+
'glossary.search': '용어 검색…',
|
|
202
|
+
'glossary.noResults': '일치하는 용어가 없습니다.',
|
|
203
|
+
'search.placeholder': '도구 검색…',
|
|
204
|
+
'concept.title': '개념',
|
|
205
|
+
'concept.tagline': '카탈로그의 도구를 엮어 동작하는 AI 스택을 구성하는 패턴.',
|
|
206
|
+
'concept.empty': '아직 개념이 없습니다.',
|
|
207
|
+
'concept.usedTools': '쓰는 도구',
|
|
208
|
+
'concept.relatedConcepts': '관련 개념',
|
|
209
|
+
'concept.learnMore': '더 알아보기',
|
|
210
|
+
'concept.backToConcepts': '개념 전체',
|
|
211
|
+
'concept.updated': '업데이트',
|
|
212
|
+
'detail.usedInConcepts': '이 도구가 쓰이는 개념',
|
|
213
|
+
'sort.label': '정렬',
|
|
214
|
+
'sort.alpha': '이름순',
|
|
215
|
+
'sort.recent': '최근 업데이트순',
|
|
216
|
+
'list.seeAll': '전체 {n}개',
|
|
217
|
+
'sort.stars': 'Star순',
|
|
218
|
+
'filter.clear': '초기화',
|
|
219
|
+
'view.label': '보기',
|
|
220
|
+
'view.standard': '표준',
|
|
221
|
+
'view.gallery': '갤러리',
|
|
222
|
+
'filter.updated': '최신 업데이트',
|
|
223
|
+
'updated.all': '전체보기',
|
|
224
|
+
'updated.6m': '최근 6개월 이내',
|
|
225
|
+
'updated.1y': '최근 1년 이내',
|
|
226
|
+
'updated.2y': '최근 2년 이내',
|
|
227
|
+
'results.none': '조건에 맞는 도구가 없습니다.',
|
|
228
|
+
'detail.allTools': '전체 도구',
|
|
229
|
+
'detail.website': '웹사이트',
|
|
230
|
+
'detail.repository': '저장소',
|
|
231
|
+
'detail.docs': '문서',
|
|
232
|
+
'detail.language': '언어',
|
|
233
|
+
'detail.license': '라이선스',
|
|
234
|
+
'detail.pricing': '가격',
|
|
235
|
+
'detail.source': '출처',
|
|
236
|
+
'detail.asOf': '기준일',
|
|
237
|
+
'detail.plan': '플랜',
|
|
238
|
+
'detail.price': '가격',
|
|
239
|
+
'detail.notes': '비고',
|
|
240
|
+
'pricing.stale': '가격 1년 이상 미확인',
|
|
241
|
+
'pricing.staleHint': '가격을 마지막으로 확인한 지 1년이 지나, 현재가와 다를 수 있습니다',
|
|
242
|
+
'pricing.aging': '가격 6개월 이상 미확인',
|
|
243
|
+
'pricing.agingHint': '가격을 마지막으로 확인한 지 6개월이 지나, 현재가와 다를 수 있습니다',
|
|
244
|
+
'detail.relatedWriting': '관련 글',
|
|
245
|
+
'detail.relatedTools': '관련 도구',
|
|
246
|
+
'blog.title': '글',
|
|
247
|
+
'blog.tagline': 'AI 시스템 구축에 관한 노트와 비교 — 다루는 도구로 바로 연결됩니다.',
|
|
248
|
+
'blog.empty': '아직 글이 없습니다.',
|
|
249
|
+
'article.backToBlog': '전체 글',
|
|
250
|
+
'article.referencedTools': '이 글에서 다루는 도구',
|
|
251
|
+
'theme.label': '테마',
|
|
252
|
+
'theme.system': '시스템',
|
|
253
|
+
'theme.light': '라이트',
|
|
254
|
+
'theme.dark': '다크',
|
|
255
|
+
'tab.overview': '설명',
|
|
256
|
+
'tab.pricing': '가격',
|
|
257
|
+
'tab.code': '코드 샘플',
|
|
258
|
+
'tab.impl': '구현 샘플',
|
|
259
|
+
'project.intro': '실제로 실행 가능한 미니 프로젝트입니다. 받아서 Docker로 실행해 보세요.',
|
|
260
|
+
// 폴더 경로를 감싸 렌더됨: "GitHub에서 samples/<folder> 보기".
|
|
261
|
+
'project.viewOnGitHubPre': 'GitHub에서',
|
|
262
|
+
'project.viewOnGitHubPost': '보기',
|
|
263
|
+
// dev 전용 쌍둥이: "로컬에서 samples/<folder> 보기".
|
|
264
|
+
'project.viewLocallyPre': '로컬에서',
|
|
265
|
+
'project.viewLocallyPost': '보기',
|
|
266
|
+
'project.example': '예제',
|
|
267
|
+
'project.files': '파일',
|
|
268
|
+
'project.relatedTools': '관련 도구',
|
|
269
|
+
'project.unregistered': '목록에 없는 도구',
|
|
270
|
+
'project.readmeExpand': '본문 펼치기',
|
|
271
|
+
'project.readmeCollapse': '본문 접기',
|
|
272
|
+
'sample.example': '예제',
|
|
273
|
+
'sample.diagram': '구조',
|
|
274
|
+
'tag.heading': '태그',
|
|
275
|
+
'tag.allTools': '전체 도구',
|
|
276
|
+
'detail.vendor': '만든 곳',
|
|
277
|
+
'vendor.heading': '만든 곳',
|
|
278
|
+
'toc.onThisPage': '목차',
|
|
279
|
+
'toc.collapseAll': '모두 접기',
|
|
280
|
+
'toc.expandAll': '모두 펼치기',
|
|
281
|
+
'toc.versions': '버전',
|
|
282
|
+
'meta.released': '최신 릴리스',
|
|
283
|
+
'meta.updated': '업데이트',
|
|
284
|
+
'meta.docVersion': '문서버전',
|
|
285
|
+
'collapse.more': '+{n}개 더 보기',
|
|
286
|
+
'collapse.less': '접기',
|
|
287
|
+
'meta.deprecated': '유지보수 전용',
|
|
288
|
+
'meta.stale': '1년 이상 업데이트 없음',
|
|
289
|
+
'meta.staleHint': '최신 릴리스가 1년 이상 지났습니다',
|
|
290
|
+
'meta.formerly': '이전',
|
|
291
|
+
'meta.aging': '6개월 이상 업데이트 없음',
|
|
292
|
+
'meta.agingHint': '최신 릴리스가 6개월 이상 지났습니다',
|
|
293
|
+
'top.label': '맨 위로',
|
|
294
|
+
'footer.builtWith': '제작 도구:',
|
|
295
|
+
'footer.contribute': '기여를 환영합니다 — MDX 파일 하나만 추가하면 도구가 등록됩니다.',
|
|
296
|
+
},
|
|
297
|
+
} as const;
|
|
298
|
+
|
|
299
|
+
export type UIKey = keyof (typeof ui)['en'];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Returns a `t(key)` translator bound to the given language. A site can
|
|
303
|
+
* override any string (taglines, headings…) via `site.ui.<lang>` in its
|
|
304
|
+
* `src/data/site.ts` — overrides win over the theme defaults here.
|
|
305
|
+
*/
|
|
306
|
+
export function useTranslations(lang: Lang) {
|
|
307
|
+
return function t(key: UIKey): string {
|
|
308
|
+
return site.ui?.[lang]?.[key] ?? ui[lang][key] ?? ui[defaultLang][key];
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Human labels for the `pricing` frontmatter enum, per locale. */
|
|
313
|
+
export const pricingLabels: Record<Lang, Record<string, string>> = {
|
|
314
|
+
en: {
|
|
315
|
+
'completely-free': 'Completely free',
|
|
316
|
+
'open-source': 'Open source',
|
|
317
|
+
'free-tier': 'Free tier',
|
|
318
|
+
paid: 'Paid',
|
|
319
|
+
free: 'Free',
|
|
320
|
+
},
|
|
321
|
+
ko: {
|
|
322
|
+
'completely-free': '완전 무료',
|
|
323
|
+
'open-source': '오픈소스',
|
|
324
|
+
'free-tier': '무료 티어',
|
|
325
|
+
paid: '유료',
|
|
326
|
+
free: '무료',
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/** Descriptive (non-name) licenses get localized; real license names pass through. */
|
|
331
|
+
const licenseLabels: Record<Lang, Record<string, string>> = {
|
|
332
|
+
en: { proprietary: 'Proprietary' },
|
|
333
|
+
ko: { proprietary: '독점' },
|
|
334
|
+
};
|
|
335
|
+
export function licenseLabel(lang: Lang, value: string): string {
|
|
336
|
+
return licenseLabels[lang][value] ?? value;
|
|
337
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/global.css';
|
|
3
|
+
import { site } from '@aas-data/site';
|
|
4
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
|
+
import { useTranslations, languages, type Lang } from '../i18n/ui';
|
|
6
|
+
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
|
|
7
|
+
import ThemeToggle from '../components/ThemeToggle.astro';
|
|
8
|
+
import BackToTop from '../components/BackToTop.astro';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
lang: Lang;
|
|
14
|
+
/** Locale-relative path of this page, e.g. '' (home) or 'stack/litellm/'. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** Wider container — used by detail pages that have a right-rail TOC. */
|
|
17
|
+
wide?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { title, lang, path, wide = false } = Astro.props;
|
|
21
|
+
const t = useTranslations(lang);
|
|
22
|
+
// Collapse any authored line breaks (used for on-page wrapping) to a single
|
|
23
|
+
// space so the SEO meta description stays one clean line.
|
|
24
|
+
const description = (Astro.props.description ?? t('site.tagline')).replace(/\s*\n\s*/g, ' ');
|
|
25
|
+
const container = wide ? 'max-w-[81rem]' : 'max-w-[75rem]';
|
|
26
|
+
|
|
27
|
+
const home = getRelativeLocaleUrl(lang, '');
|
|
28
|
+
const base = import.meta.env.BASE_URL;
|
|
29
|
+
|
|
30
|
+
// Home URL per locale, for the language auto-detect redirect on the root home.
|
|
31
|
+
const localeHomes = Object.fromEntries(
|
|
32
|
+
(Object.keys(languages) as Lang[]).map((c) => [c, getRelativeLocaleUrl(c, '')]),
|
|
33
|
+
);
|
|
34
|
+
const isDefaultHome = path === '' && lang === 'en';
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<!doctype html>
|
|
38
|
+
<html lang={lang}>
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="utf-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
42
|
+
<meta name="generator" content={Astro.generator} />
|
|
43
|
+
<title>{title}</title>
|
|
44
|
+
<meta name="description" content={description} />
|
|
45
|
+
<link rel="icon" type="image/svg+xml" href={`${base.replace(/\/$/, '')}/favicon.svg`} />
|
|
46
|
+
{/* On the root (default-locale) home, send first-time visitors to the
|
|
47
|
+
locale matching their browser — unless they've already picked one (the
|
|
48
|
+
language switcher stores `aas:lang`). Runs before paint, no history entry. */}
|
|
49
|
+
{
|
|
50
|
+
isDefaultHome && (
|
|
51
|
+
<script is:inline define:vars={{ curLang: lang, localeHomes }}>
|
|
52
|
+
(function () {
|
|
53
|
+
try {
|
|
54
|
+
var supported = Object.keys(localeHomes);
|
|
55
|
+
var target = localStorage.getItem('aas:lang');
|
|
56
|
+
if (!target || supported.indexOf(target) === -1) {
|
|
57
|
+
target = null;
|
|
58
|
+
var navs = navigator.languages || [navigator.language || ''];
|
|
59
|
+
for (var i = 0; i < navs.length; i++) {
|
|
60
|
+
var b = (navs[i] || '').toLowerCase().split('-')[0];
|
|
61
|
+
if (supported.indexOf(b) !== -1) {
|
|
62
|
+
target = b;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (target && target !== curLang && localeHomes[target]) location.replace(localeHomes[target]);
|
|
68
|
+
} catch (e) {}
|
|
69
|
+
})();
|
|
70
|
+
</script>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
{/* Set the theme before first paint to avoid a flash of the wrong theme. */}
|
|
74
|
+
<script is:inline>
|
|
75
|
+
(function () {
|
|
76
|
+
try {
|
|
77
|
+
var pref = localStorage.getItem('theme') || 'system';
|
|
78
|
+
var dark =
|
|
79
|
+
pref === 'dark' ||
|
|
80
|
+
(pref === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
81
|
+
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
|
82
|
+
} catch (e) {}
|
|
83
|
+
})();
|
|
84
|
+
</script>
|
|
85
|
+
{/* Listing density (standard / gallery), persisted in localStorage. Gallery
|
|
86
|
+
is the default; only an explicit 'grid' choice falls back to standard.
|
|
87
|
+
Set before paint so the layout doesn't flash. */}
|
|
88
|
+
<script is:inline>
|
|
89
|
+
(function () {
|
|
90
|
+
try {
|
|
91
|
+
if (localStorage.getItem('aas:view') !== 'grid')
|
|
92
|
+
document.documentElement.dataset.view = 'gallery';
|
|
93
|
+
} catch (e) {}
|
|
94
|
+
})();
|
|
95
|
+
</script>
|
|
96
|
+
</head>
|
|
97
|
+
<body class="min-h-screen">
|
|
98
|
+
<header class="border-b border-[var(--aas-border)]">
|
|
99
|
+
<div class:list={['mx-auto flex items-center justify-between gap-3 px-5 py-4', container]}>
|
|
100
|
+
<a
|
|
101
|
+
href={home}
|
|
102
|
+
data-keep-filters
|
|
103
|
+
aria-label={site.name}
|
|
104
|
+
class="flex min-w-0 items-center gap-1.5 text-base font-bold tracking-tight whitespace-nowrap no-underline sm:text-lg"
|
|
105
|
+
>
|
|
106
|
+
<span aria-hidden="true">⚡</span>
|
|
107
|
+
{/* Below 600px only the bolt remains, so the narrow header never truncates the wordmark. */}
|
|
108
|
+
<span class="hidden truncate min-[600px]:inline">{site.name}</span>
|
|
109
|
+
</a>
|
|
110
|
+
{/* Nav links are icon-only at every width (labels live in aria-label). */}
|
|
111
|
+
<nav class="flex shrink-0 items-center gap-3 text-sm text-[var(--aas-muted)] sm:gap-4">
|
|
112
|
+
<a
|
|
113
|
+
href={`${home}#categories`}
|
|
114
|
+
data-keep-filters
|
|
115
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
116
|
+
aria-label={t('nav.browse')}
|
|
117
|
+
>
|
|
118
|
+
<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"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg>
|
|
119
|
+
</a>
|
|
120
|
+
<a
|
|
121
|
+
href={getRelativeLocaleUrl(lang, 'concept/')}
|
|
122
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
123
|
+
aria-label={t('nav.concepts')}
|
|
124
|
+
>
|
|
125
|
+
<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="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>
|
|
126
|
+
</a>
|
|
127
|
+
<a
|
|
128
|
+
href={getRelativeLocaleUrl(lang, 'article/')}
|
|
129
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
130
|
+
aria-label={t('nav.blog')}
|
|
131
|
+
>
|
|
132
|
+
<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="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"/></svg>
|
|
133
|
+
</a>
|
|
134
|
+
<a
|
|
135
|
+
href={getRelativeLocaleUrl(lang, 'sample/')}
|
|
136
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
137
|
+
aria-label={t('nav.samples')}
|
|
138
|
+
>
|
|
139
|
+
<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"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
|
|
140
|
+
</a>
|
|
141
|
+
<a
|
|
142
|
+
href={getRelativeLocaleUrl(lang, 'slides/')}
|
|
143
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
144
|
+
aria-label={t('nav.slides')}
|
|
145
|
+
>
|
|
146
|
+
<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="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"/></svg>
|
|
147
|
+
</a>
|
|
148
|
+
<a
|
|
149
|
+
href={getRelativeLocaleUrl(lang, 'glossary/')}
|
|
150
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
151
|
+
aria-label={t('nav.glossary')}
|
|
152
|
+
>
|
|
153
|
+
<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="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
|
154
|
+
</a>
|
|
155
|
+
<a
|
|
156
|
+
href={site.repoUrl}
|
|
157
|
+
target="_blank"
|
|
158
|
+
rel="noopener noreferrer"
|
|
159
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
160
|
+
aria-label="GitHub"
|
|
161
|
+
>
|
|
162
|
+
<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="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C4 2 3 2 3 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 2 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
|
163
|
+
</a>
|
|
164
|
+
<ThemeToggle lang={lang} />
|
|
165
|
+
<LanguageSwitcher lang={lang} path={path} />
|
|
166
|
+
</nav>
|
|
167
|
+
</div>
|
|
168
|
+
</header>
|
|
169
|
+
|
|
170
|
+
<main class:list={['mx-auto px-5 py-10', container]}>
|
|
171
|
+
<slot />
|
|
172
|
+
</main>
|
|
173
|
+
|
|
174
|
+
<footer
|
|
175
|
+
class="border-t border-[var(--aas-border)] py-8 text-center text-sm text-[var(--aas-muted)]"
|
|
176
|
+
>
|
|
177
|
+
<p>
|
|
178
|
+
{t('footer.builtWith')}
|
|
179
|
+
<a
|
|
180
|
+
href="https://astro.build"
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
class="text-[var(--aas-accent)]">Astro</a
|
|
184
|
+
>. {
|
|
185
|
+
t('footer.contribute')
|
|
186
|
+
}
|
|
187
|
+
</p>
|
|
188
|
+
</footer>
|
|
189
|
+
|
|
190
|
+
<BackToTop lang={lang} />
|
|
191
|
+
|
|
192
|
+
<script>
|
|
193
|
+
// Clicking a heading's "#" anchor copies that section's URL to the
|
|
194
|
+
// clipboard (and updates the address bar) instead of just navigating.
|
|
195
|
+
document.addEventListener('click', (e) => {
|
|
196
|
+
const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a.aas-anchor');
|
|
197
|
+
if (!anchor) return;
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
const hash = anchor.getAttribute('href') ?? '';
|
|
200
|
+
const url = location.origin + location.pathname + location.search + hash;
|
|
201
|
+
history.replaceState(null, '', hash);
|
|
202
|
+
navigator.clipboard?.writeText(url).catch(() => {});
|
|
203
|
+
anchor.classList.add('copied');
|
|
204
|
+
setTimeout(() => anchor.classList.remove('copied'), 1200);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Mount a copy-to-clipboard button on every code block: README/overview
|
|
208
|
+
// prose (.prose pre), the file viewer (.aas-projfile), and the Code tab
|
|
209
|
+
// (.aas-code). The button is pinned to a non-scrolling host so it stays in
|
|
210
|
+
// place on long, horizontally-scrolling code.
|
|
211
|
+
(function mountCopyButtons() {
|
|
212
|
+
const koLang = document.documentElement.lang === 'ko';
|
|
213
|
+
const LABEL = koLang ? '코드 복사' : 'Copy code';
|
|
214
|
+
const DONE = koLang ? '복사됨' : 'Copied';
|
|
215
|
+
const COPY_ICON =
|
|
216
|
+
'<svg class="aas-ico-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="13" height="13" x="9" y="9" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
217
|
+
const CHECK_ICON =
|
|
218
|
+
'<svg class="aas-ico-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
|
|
219
|
+
|
|
220
|
+
function wrap(el: Element): HTMLElement {
|
|
221
|
+
const w = document.createElement('div');
|
|
222
|
+
w.className = 'aas-copyhost';
|
|
223
|
+
el.parentNode?.insertBefore(w, el);
|
|
224
|
+
w.appendChild(el);
|
|
225
|
+
return w;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function mount(source: HTMLElement, host: HTMLElement) {
|
|
229
|
+
if (host.querySelector(':scope > .aas-copy-btn')) return; // already mounted
|
|
230
|
+
host.classList.add('aas-copyhost');
|
|
231
|
+
const btn = document.createElement('button');
|
|
232
|
+
btn.type = 'button';
|
|
233
|
+
btn.className = 'aas-copy-btn';
|
|
234
|
+
btn.setAttribute('aria-label', LABEL);
|
|
235
|
+
btn.title = LABEL;
|
|
236
|
+
btn.innerHTML = COPY_ICON + CHECK_ICON;
|
|
237
|
+
let timer = 0;
|
|
238
|
+
btn.addEventListener('click', () => {
|
|
239
|
+
const text = source.innerText.replace(/\n$/, '');
|
|
240
|
+
navigator.clipboard?.writeText(text).then(() => {
|
|
241
|
+
btn.classList.add('copied');
|
|
242
|
+
btn.setAttribute('aria-label', DONE);
|
|
243
|
+
window.clearTimeout(timer);
|
|
244
|
+
timer = window.setTimeout(() => {
|
|
245
|
+
btn.classList.remove('copied');
|
|
246
|
+
btn.setAttribute('aria-label', LABEL);
|
|
247
|
+
}, 1400);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
host.appendChild(btn);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Prose code blocks (READMEs, overviews, articles): wrap each <pre>.
|
|
254
|
+
document
|
|
255
|
+
.querySelectorAll<HTMLElement>('.prose pre:not(.mermaid)')
|
|
256
|
+
.forEach((pre) => mount(pre, wrap(pre)));
|
|
257
|
+
// Code tab: the .aas-code div is the scroll container — wrap it.
|
|
258
|
+
document
|
|
259
|
+
.querySelectorAll<HTMLElement>('.aas-code')
|
|
260
|
+
.forEach((el) => mount(el, wrap(el)));
|
|
261
|
+
// File viewer: anchor the button on the figure (above the scroll area).
|
|
262
|
+
document.querySelectorAll<HTMLElement>('.aas-projfile').forEach((el) => {
|
|
263
|
+
const fig = el.closest('figure');
|
|
264
|
+
mount(el, (fig as HTMLElement) ?? wrap(el));
|
|
265
|
+
});
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
// Close any open <details> dropdown when clicking outside it (or on Esc).
|
|
269
|
+
// Only nav dropdowns — leave file-tree folders (.aas-tree-dir) and prose
|
|
270
|
+
// content reveals (inside .prose, e.g. "구체적인 예시") alone; those toggle
|
|
271
|
+
// only via their own summary.
|
|
272
|
+
const isDropdown = (d: Element) => !d.closest('.aas-tree-dir') && !d.closest('.prose');
|
|
273
|
+
document.addEventListener('click', (e) => {
|
|
274
|
+
const target = e.target as Node;
|
|
275
|
+
document.querySelectorAll<HTMLDetailsElement>('details[open]').forEach((d) => {
|
|
276
|
+
if (isDropdown(d) && !d.contains(target)) d.open = false;
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
document.addEventListener('keydown', (e) => {
|
|
280
|
+
if (e.key === 'Escape')
|
|
281
|
+
document
|
|
282
|
+
.querySelectorAll<HTMLDetailsElement>('details[open]')
|
|
283
|
+
.forEach((d) => isDropdown(d) && (d.open = false));
|
|
284
|
+
});
|
|
285
|
+
</script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { Lang } from '../i18n/ui';
|
|
3
|
+
|
|
4
|
+
export type ArticleEntry = CollectionEntry<'articles'>;
|
|
5
|
+
|
|
6
|
+
/** The url slug of an article, i.e. its id with the `<lang>/` prefix removed. */
|
|
7
|
+
export function articleSlugOf(entry: ArticleEntry): string {
|
|
8
|
+
return entry.id.replace(/^[a-z]{2}\//, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Published articles for one locale, newest first. */
|
|
12
|
+
export async function getArticles(lang: Lang): Promise<ArticleEntry[]> {
|
|
13
|
+
const all = await getCollection('articles');
|
|
14
|
+
return all
|
|
15
|
+
.filter((e) => e.id.startsWith(`${lang}/`) && !e.data.draft)
|
|
16
|
+
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Backlinks: articles in the given locale that reference `toolSlug` in their
|
|
21
|
+
* `tools` frontmatter. Astro has no native backlinks — we compute the reverse
|
|
22
|
+
* lookup here so a tool's detail page can list the writing that mentions it.
|
|
23
|
+
*/
|
|
24
|
+
export async function getArticlesForTool(lang: Lang, toolSlug: string): Promise<ArticleEntry[]> {
|
|
25
|
+
const articles = await getArticles(lang);
|
|
26
|
+
return articles.filter((a) => a.data.tools.includes(toolSlug));
|
|
27
|
+
}
|