@anglefeint/astro-theme 0.1.0-alpha.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 (33) hide show
  1. package/README.md +50 -0
  2. package/package.json +38 -0
  3. package/public/scripts/about-effects.js +535 -0
  4. package/public/scripts/blogpost-effects.js +956 -0
  5. package/public/scripts/home-matrix.js +117 -0
  6. package/public/styles/about-page.css +756 -0
  7. package/public/styles/blog-post.css +390 -0
  8. package/public/styles/home-page.css +186 -0
  9. package/src/assets/theme/placeholders/theme-placeholder-1.jpg +0 -0
  10. package/src/assets/theme/placeholders/theme-placeholder-2.jpg +0 -0
  11. package/src/assets/theme/placeholders/theme-placeholder-3.jpg +0 -0
  12. package/src/assets/theme/placeholders/theme-placeholder-4.jpg +0 -0
  13. package/src/assets/theme/placeholders/theme-placeholder-5.jpg +0 -0
  14. package/src/assets/theme/placeholders/theme-placeholder-about.jpg +0 -0
  15. package/src/assets/theme/red-queen/theme-redqueen1.webp +0 -0
  16. package/src/assets/theme/red-queen/theme-redqueen2.gif +0 -0
  17. package/src/cli-new-page.mjs +118 -0
  18. package/src/cli-new-post.mjs +139 -0
  19. package/src/components/BaseHead.astro +177 -0
  20. package/src/components/Footer.astro +74 -0
  21. package/src/components/FormattedDate.astro +17 -0
  22. package/src/components/Header.astro +328 -0
  23. package/src/components/HeaderLink.astro +35 -0
  24. package/src/consts.ts +12 -0
  25. package/src/content-schema.ts +33 -0
  26. package/src/i18n/config.ts +46 -0
  27. package/src/i18n/messages.ts +220 -0
  28. package/src/i18n/posts.ts +11 -0
  29. package/src/index.ts +5 -0
  30. package/src/layouts/BasePageLayout.astro +67 -0
  31. package/src/layouts/BlogPost.astro +279 -0
  32. package/src/layouts/HomePage.astro +73 -0
  33. package/src/styles/global.css +1867 -0
@@ -0,0 +1,328 @@
1
+ ---
2
+ import { SITE_TITLE } from '../consts';
3
+ import { SOCIAL_LINKS } from '@anglefeint/site-config/social';
4
+ import { THEME } from '@anglefeint/site-config/theme';
5
+ import HeaderLink from './HeaderLink.astro';
6
+ import {
7
+ DEFAULT_LOCALE,
8
+ LOCALE_LABELS,
9
+ type Locale,
10
+ SUPPORTED_LOCALES,
11
+ alternatePathForLocale,
12
+ isLocale,
13
+ stripLocaleFromPath,
14
+ } from '../i18n/config';
15
+
16
+ interface Props {
17
+ locale?: Locale;
18
+ homeHref?: string;
19
+ /** Optional per-locale overrides (used for existence-aware language fallback on some routes). */
20
+ localeHrefs?: Partial<Record<Locale, string>>;
21
+ /** Show Blade Runner scanlines overlay on mesh-page (header/footer only) */
22
+ scanlines?: boolean;
23
+ labels?: {
24
+ home: string;
25
+ blog: string;
26
+ about: string;
27
+ status: string;
28
+ language: string;
29
+ };
30
+ }
31
+
32
+ const props = Astro.props as Props;
33
+ const locale: Locale = props.locale && isLocale(props.locale) ? props.locale : DEFAULT_LOCALE;
34
+ const labels = {
35
+ home: props.labels?.home ?? 'Home',
36
+ blog: props.labels?.blog ?? 'Blog',
37
+ about: props.labels?.about ?? 'About',
38
+ status: props.labels?.status ?? 'system: online',
39
+ language: props.labels?.language ?? 'Language',
40
+ };
41
+ const showAbout = THEME.ENABLE_ABOUT_PAGE;
42
+ const currentSubpath = stripLocaleFromPath(Astro.url.pathname, locale);
43
+ const homeHref = props.homeHref ?? alternatePathForLocale(locale, '/');
44
+ const buildLocaleHref = (targetLocale: Locale) => {
45
+ // Language switcher: preserve current route when switching locales.
46
+ // Only fall back for About when the feature is disabled.
47
+ const sectionSubpath =
48
+ currentSubpath.startsWith('/about') && !showAbout ? '/' : currentSubpath || '/';
49
+ return alternatePathForLocale(targetLocale, sectionSubpath);
50
+ };
51
+ const localeOptions = SUPPORTED_LOCALES.map((targetLocale) => ({
52
+ locale: targetLocale,
53
+ label: LOCALE_LABELS[targetLocale],
54
+ href: props.localeHrefs?.[targetLocale] ?? buildLocaleHref(targetLocale),
55
+ }));
56
+ ---
57
+
58
+ <header>
59
+ <nav>
60
+ <div class="nav-left">
61
+ <h2><a href={homeHref}>{SITE_TITLE}</a></h2>
62
+ </div>
63
+ <div class="internal-links">
64
+ <HeaderLink href={homeHref}>{labels.home}</HeaderLink>
65
+ <HeaderLink href={alternatePathForLocale(locale, '/blog/')}>{labels.blog}</HeaderLink>
66
+ {showAbout && <HeaderLink href={alternatePathForLocale(locale, '/about/')}>{labels.about}</HeaderLink>}
67
+ </div>
68
+ <div class="nav-right">
69
+ <div class="social-links">
70
+ <div class="lang-switcher" aria-label={labels.language}>
71
+ <label class="lang-label" for="lang-select">{labels.language}</label>
72
+ <select
73
+ id="lang-select"
74
+ class="lang-select"
75
+ aria-label={labels.language}
76
+ onchange="if (this.value) window.location.href = this.value;"
77
+ >
78
+ {
79
+ localeOptions.map((option) => (
80
+ <option value={option.href} selected={option.locale === locale}>
81
+ {option.label}
82
+ </option>
83
+ ))
84
+ }
85
+ </select>
86
+ </div>
87
+ <div class="nav-status" aria-label="System status">
88
+ <span class="nav-status-dot" aria-hidden="true"></span>
89
+ <span class="nav-status-text">{labels.status}</span>
90
+ </div>
91
+ {
92
+ SOCIAL_LINKS.map((link) => (
93
+ <a href={link.href} target="_blank" rel="noopener noreferrer">
94
+ <span class="sr-only">{link.label}</span>
95
+ {link.icon === 'mastodon' && (
96
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
97
+ <path
98
+ fill="currentColor"
99
+ d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
100
+ />
101
+ </svg>
102
+ )}
103
+ {link.icon === 'twitter' && (
104
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
105
+ <path
106
+ fill="currentColor"
107
+ d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
108
+ />
109
+ </svg>
110
+ )}
111
+ {link.icon === 'github' && (
112
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
113
+ <path
114
+ fill="currentColor"
115
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
116
+ />
117
+ </svg>
118
+ )}
119
+ {!link.icon && <span>{link.label}</span>}
120
+ </a>
121
+ ))
122
+ }
123
+ </div>
124
+ </div>
125
+ </nav>
126
+ {props.scanlines && <div class="mesh-scanlines" aria-hidden="true"></div>}
127
+ </header>
128
+ <style>
129
+ header {
130
+ margin: 0;
131
+ padding: 0 1em;
132
+ background: var(--chrome-bg, var(--bg));
133
+ border-bottom: 1px solid var(--chrome-border, rgb(var(--border)));
134
+ }
135
+ h2 {
136
+ margin: 0;
137
+ font-size: 1em;
138
+ }
139
+
140
+ h2 a,
141
+ h2 a.active {
142
+ text-decoration: none;
143
+ color: var(--chrome-link, rgb(var(--text)));
144
+ border-bottom: none;
145
+ }
146
+ nav {
147
+ display: grid;
148
+ grid-template-columns: 1fr auto 1fr;
149
+ align-items: center;
150
+ gap: 0.6rem;
151
+ min-height: 56px;
152
+ width: 100%;
153
+ }
154
+ .nav-left {
155
+ justify-self: start;
156
+ }
157
+ .internal-links {
158
+ justify-self: center;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 0.1rem;
162
+ }
163
+ .nav-right {
164
+ justify-self: end;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.35rem;
168
+ min-width: max-content;
169
+ }
170
+ .internal-links :global(a) {
171
+ padding: 1em 0.5em;
172
+ color: var(--chrome-link, rgb(var(--text)));
173
+ border-bottom: 4px solid transparent;
174
+ text-decoration: none;
175
+ }
176
+ .internal-links :global(a.active) {
177
+ text-decoration: none;
178
+ border-bottom-color: var(--chrome-active, var(--accent));
179
+ }
180
+ .social-links a {
181
+ color: var(--chrome-link, rgb(var(--text)));
182
+ align-items: center;
183
+ justify-content: center;
184
+ padding: 0.6rem 0.35rem;
185
+ border-bottom: none;
186
+ }
187
+ .social-links a:hover {
188
+ color: var(--chrome-link-hover, var(--chrome-link, rgb(var(--text))));
189
+ }
190
+ .social-links,
191
+ .social-links a {
192
+ display: flex;
193
+ }
194
+ .social-links {
195
+ align-items: center;
196
+ gap: 0.4rem;
197
+ position: relative;
198
+ flex-wrap: nowrap;
199
+ }
200
+ .lang-switcher {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 0.4rem;
204
+ padding: 0.2rem 0.4rem;
205
+ margin-right: 0.1rem;
206
+ border: 1px solid rgba(132, 214, 255, 0.2);
207
+ border-radius: 999px;
208
+ background: rgba(6, 16, 30, 0.35);
209
+ flex-shrink: 0;
210
+ }
211
+ .lang-label {
212
+ display: inline-block;
213
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
214
+ font-size: 0.6rem;
215
+ text-transform: uppercase;
216
+ letter-spacing: 0.12em;
217
+ color: rgba(190, 226, 248, 0.78);
218
+ white-space: nowrap;
219
+ }
220
+ .lang-select {
221
+ appearance: none;
222
+ -webkit-appearance: none;
223
+ -moz-appearance: none;
224
+ background:
225
+ linear-gradient(45deg, transparent 50%, rgba(204, 236, 252, 0.82) 50%) calc(100% - 0.9rem) 50% / 0.35rem 0.35rem no-repeat,
226
+ linear-gradient(135deg, rgba(204, 236, 252, 0.82) 50%, transparent 50%) calc(100% - 0.7rem) 50% / 0.35rem 0.35rem no-repeat,
227
+ rgba(9, 22, 40, 0.68);
228
+ border: 1px solid rgba(132, 214, 255, 0.2);
229
+ border-radius: 999px;
230
+ padding: 0.2rem 1.45rem 0.2rem 0.55rem;
231
+ min-width: 6rem;
232
+ font-size: 0.62rem;
233
+ line-height: 1;
234
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
235
+ color: rgba(204, 236, 252, 0.86);
236
+ cursor: pointer;
237
+ }
238
+ .lang-select:focus {
239
+ outline: none;
240
+ border-color: rgba(132, 214, 255, 0.5);
241
+ box-shadow: 0 0 0 2px rgba(98, 180, 228, 0.18);
242
+ }
243
+ .lang-select option {
244
+ color: #ccecfb;
245
+ background: #0b1c32;
246
+ }
247
+ .nav-status {
248
+ display: none;
249
+ align-items: center;
250
+ gap: 0.45rem;
251
+ padding: 0.24rem 0.5rem;
252
+ border-radius: 999px;
253
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
254
+ font-size: 0.62rem;
255
+ letter-spacing: 0.12em;
256
+ text-transform: uppercase;
257
+ color: rgba(186, 232, 252, 0.86);
258
+ background: rgba(6, 16, 30, 0.52);
259
+ border: 1px solid rgba(132, 214, 255, 0.22);
260
+ box-shadow:
261
+ 0 0 0 1px rgba(132, 214, 255, 0.08),
262
+ 0 0 16px rgba(90, 180, 255, 0.16);
263
+ white-space: nowrap;
264
+ position: absolute;
265
+ right: calc(100% + 0.5rem);
266
+ top: 50%;
267
+ transform: translateY(-50%);
268
+ pointer-events: none;
269
+ }
270
+ .nav-status-dot {
271
+ width: 0.44rem;
272
+ height: 0.44rem;
273
+ border-radius: 50%;
274
+ background: rgba(150, 226, 255, 0.96);
275
+ box-shadow: 0 0 10px rgba(122, 210, 255, 0.72);
276
+ animation: nav-status-pulse 1.8s steps(1, end) infinite;
277
+ }
278
+ @keyframes nav-status-pulse {
279
+ 0%,
280
+ 78%,
281
+ 100% {
282
+ opacity: 1;
283
+ transform: scale(1);
284
+ }
285
+ 82% {
286
+ opacity: 0.2;
287
+ transform: scale(0.7);
288
+ }
289
+ 86% {
290
+ opacity: 1;
291
+ transform: scale(1.05);
292
+ }
293
+ 90% {
294
+ opacity: 0.28;
295
+ transform: scale(0.78);
296
+ }
297
+ }
298
+ body.mesh-page .nav-status {
299
+ display: inline-flex;
300
+ }
301
+ body.about-page .nav-status {
302
+ display: none;
303
+ }
304
+ @media (max-width: 720px) {
305
+ nav {
306
+ grid-template-columns: 1fr;
307
+ gap: 0;
308
+ }
309
+ .nav-left {
310
+ display: none;
311
+ }
312
+ .internal-links {
313
+ justify-self: start;
314
+ }
315
+ .nav-right {
316
+ justify-self: end;
317
+ }
318
+ .nav-status {
319
+ display: none !important;
320
+ }
321
+ .social-links {
322
+ display: none;
323
+ }
324
+ .lang-switcher {
325
+ margin-right: 0;
326
+ }
327
+ }
328
+ </style>
@@ -0,0 +1,35 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+
4
+ type Props = HTMLAttributes<'a'>;
5
+
6
+ const { href, class: className, ...props } = Astro.props;
7
+ const basePath = import.meta.env.BASE_URL;
8
+ const stripBasePath = (value: string) => {
9
+ if (!basePath || basePath === '/') return value;
10
+ const normalizedBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
11
+ if (value === normalizedBase) return '/';
12
+ if (value.startsWith(`${normalizedBase}/`)) return value.slice(normalizedBase.length);
13
+ return value;
14
+ };
15
+ const normalize = (value: string) => {
16
+ const withSlash = value.startsWith('/') ? value : `/${value}`;
17
+ return withSlash.replace(/\/+$/, '') || '/';
18
+ };
19
+ const pathname = normalize(stripBasePath(Astro.url.pathname));
20
+ const target = normalize(String(href ?? '/'));
21
+ const targetDepth = target.split('/').filter(Boolean).length;
22
+ const isSectionLink = targetDepth >= 2;
23
+ const isActive =
24
+ target === '/' ? pathname === '/' : pathname === target || (isSectionLink && pathname.startsWith(`${target}/`));
25
+ ---
26
+
27
+ <a href={href} class:list={[className, { active: isActive }]} {...props}>
28
+ <slot />
29
+ </a>
30
+ <style>
31
+ a {
32
+ display: inline-block;
33
+ text-decoration: none;
34
+ }
35
+ </style>
package/src/consts.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @deprecated Use src/config/site.ts instead.
3
+ * Re-exported for backwards compatibility.
4
+ */
5
+ export {
6
+ SITE_TITLE,
7
+ SITE_DESCRIPTION,
8
+ SITE_URL,
9
+ SITE_AUTHOR,
10
+ SITE_TAGLINE,
11
+ SITE_HERO_BY_LOCALE,
12
+ } from '@anglefeint/site-config/site';
@@ -0,0 +1,33 @@
1
+ import { defineCollection, z } from 'astro:content';
2
+ import { glob } from 'astro/loaders';
3
+
4
+ const blog = defineCollection({
5
+ // Load Markdown and MDX files in the `src/content/blog/` directory.
6
+ loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
7
+ // Type-check frontmatter using a schema
8
+ schema: ({ image }) =>
9
+ z.object({
10
+ title: z.string(),
11
+ subtitle: z.string().optional(),
12
+ description: z.string(),
13
+ // Transform string to Date object
14
+ pubDate: z.coerce.date(),
15
+ updatedDate: z.coerce.date().optional(),
16
+ heroImage: image().optional(),
17
+ context: z.string().optional(),
18
+ readMinutes: z.number().int().positive().optional(),
19
+ aiModel: z.string().optional(),
20
+ aiMode: z.string().optional(),
21
+ aiState: z.string().optional(),
22
+ aiLatencyMs: z.number().int().nonnegative().optional(),
23
+ aiConfidence: z.number().min(0).max(1).optional(),
24
+ wordCount: z.number().int().nonnegative().optional(),
25
+ tokenCount: z.number().int().nonnegative().optional(),
26
+ author: z.string().optional(),
27
+ tags: z.array(z.string()).optional(),
28
+ canonicalTopic: z.string().optional(),
29
+ sourceLinks: z.array(z.string().url()).optional(),
30
+ }),
31
+ });
32
+
33
+ export const collections = { blog };
@@ -0,0 +1,46 @@
1
+ export const SUPPORTED_LOCALES = ['en', 'ja', 'ko', 'es', 'zh'] as const;
2
+ export type Locale = (typeof SUPPORTED_LOCALES)[number];
3
+
4
+ export const DEFAULT_LOCALE: Locale = 'en';
5
+
6
+ export const LOCALE_LABELS: Record<Locale, string> = {
7
+ en: 'English',
8
+ ja: '日本語',
9
+ ko: '한국어',
10
+ es: 'Español',
11
+ zh: '中文',
12
+ };
13
+
14
+ export function isLocale(value: string): value is Locale {
15
+ return (SUPPORTED_LOCALES as readonly string[]).includes(value);
16
+ }
17
+
18
+ export function withLeadingSlash(path: string): string {
19
+ if (!path) return '/';
20
+ return path.startsWith('/') ? path : `/${path}`;
21
+ }
22
+
23
+ export function localePath(locale: Locale, path = '/'): string {
24
+ const normalized = withLeadingSlash(path);
25
+ if (normalized === '/') return `/${locale}/`;
26
+ return `/${locale}${normalized.endsWith('/') ? normalized : `${normalized}/`}`;
27
+ }
28
+
29
+ export function stripLocaleFromPath(pathname: string, locale: Locale): string {
30
+ const prefix = `/${locale}`;
31
+ if (!pathname.startsWith(prefix)) return pathname;
32
+ const withoutLocale = pathname.slice(prefix.length);
33
+ return withoutLocale || '/';
34
+ }
35
+
36
+ export function blogIdToSlugAnyLocale(id: string): string {
37
+ const parts = id.split('/');
38
+ if (parts.length > 1 && isLocale(parts[0])) return parts.slice(1).join('/');
39
+ return id;
40
+ }
41
+
42
+ /** URL path for a locale's version of a page. For default locale home, returns / instead of /en/. */
43
+ export function alternatePathForLocale(locale: Locale, subpath: string): string {
44
+ if (locale === DEFAULT_LOCALE && (subpath === '/' || subpath === '')) return '/';
45
+ return localePath(locale, subpath);
46
+ }
@@ -0,0 +1,220 @@
1
+ import type { Locale } from './config';
2
+
3
+ type Messages = {
4
+ siteTitle: string;
5
+ siteDescription: string;
6
+ langLabel: string;
7
+ nav: {
8
+ home: string;
9
+ blog: string;
10
+ about: string;
11
+ status: string;
12
+ };
13
+ home: {
14
+ hero: string;
15
+ latest: string;
16
+ viewAll: string;
17
+ noPosts: string;
18
+ };
19
+ about: {
20
+ title: string;
21
+ description: string;
22
+ who: string;
23
+ what: string;
24
+ ethos: string;
25
+ now: string;
26
+ contact: string;
27
+ regenerate: string;
28
+ };
29
+ blog: {
30
+ title: string;
31
+ pageTitle: string;
32
+ archiveDescription: string;
33
+ pageDescription: string;
34
+ previous: string;
35
+ next: string;
36
+ backToBlog: string;
37
+ related: string;
38
+ regenerate: string;
39
+ };
40
+ footer: {
41
+ tagline: string;
42
+ };
43
+ };
44
+
45
+ const MESSAGES: Record<Locale, Messages> = {
46
+ en: {
47
+ siteTitle: 'Angle Feint',
48
+ siteDescription: 'Cinematic web interfaces and AI-era engineering essays.',
49
+ langLabel: 'Language',
50
+ nav: { home: 'Home', blog: 'Blog', about: 'About', status: 'system: online' },
51
+ home: {
52
+ hero: 'Write a short introduction for your site and what readers can expect from your posts.',
53
+ latest: 'Latest Posts',
54
+ viewAll: 'View all posts',
55
+ noPosts: 'No posts available in this language yet.',
56
+ },
57
+ about: {
58
+ title: 'About — Hacker Ethos',
59
+ description: 'Who I am, what I build, and the hacker ethos behind my work.',
60
+ who: 'Who I Am',
61
+ what: 'What I Build',
62
+ ethos: 'Hacker Ethos',
63
+ now: 'Now',
64
+ contact: 'Contact',
65
+ regenerate: 'Regenerate',
66
+ },
67
+ blog: {
68
+ title: 'Blog',
69
+ pageTitle: 'Blog - Page',
70
+ archiveDescription: 'Essays on AI-era craft, web engineering, and system architecture.',
71
+ pageDescription: 'Blog archive page',
72
+ previous: 'Previous',
73
+ next: 'Next',
74
+ backToBlog: 'Back to blog',
75
+ related: 'Related',
76
+ regenerate: 'Regenerate',
77
+ },
78
+ footer: { tagline: 'Built with Astro.' },
79
+ },
80
+ ja: {
81
+ siteTitle: 'Angle Feint',
82
+ siteDescription: '映画的なWebインターフェースとAI時代のエンジニアリング考察。',
83
+ langLabel: '言語',
84
+ nav: { home: 'ホーム', blog: 'ブログ', about: 'プロフィール', status: 'system: online' },
85
+ home: {
86
+ hero: 'このサイトの紹介文と、読者がどんな記事を期待できるかを書いてください。',
87
+ latest: '最新記事',
88
+ viewAll: 'すべての記事を見る',
89
+ noPosts: 'この言語の記事はまだありません。',
90
+ },
91
+ about: {
92
+ title: 'About — Hacker Ethos',
93
+ description: '私について、作るもの、そしてハッカー精神。',
94
+ who: '私について',
95
+ what: '作るもの',
96
+ ethos: 'ハッカー精神',
97
+ now: '現在',
98
+ contact: '連絡先',
99
+ regenerate: '再生成',
100
+ },
101
+ blog: {
102
+ title: 'ブログ',
103
+ pageTitle: 'ブログ - ページ',
104
+ archiveDescription: 'AI時代のクラフト、Web開発、システム設計に関する記事。',
105
+ pageDescription: 'ブログ一覧ページ',
106
+ previous: '前へ',
107
+ next: '次へ',
108
+ backToBlog: 'ブログへ戻る',
109
+ related: '関連記事',
110
+ regenerate: '再生成',
111
+ },
112
+ footer: { tagline: 'Astro で構築。' },
113
+ },
114
+ ko: {
115
+ siteTitle: 'Angle Feint',
116
+ siteDescription: '시네마틱 웹 인터페이스와 AI 시대 엔지니어링 에세이.',
117
+ langLabel: '언어',
118
+ nav: { home: '홈', blog: '블로그', about: '소개', status: 'system: online' },
119
+ home: {
120
+ hero: '사이트 소개와 방문자가 어떤 글을 기대할 수 있는지 간단히 작성하세요.',
121
+ latest: '최신 글',
122
+ viewAll: '모든 글 보기',
123
+ noPosts: '이 언어에는 아직 게시물이 없습니다.',
124
+ },
125
+ about: {
126
+ title: 'About — Hacker Ethos',
127
+ description: '나와 내가 만드는 것, 그리고 해커 정신.',
128
+ who: '나는 누구인가',
129
+ what: '무엇을 만드는가',
130
+ ethos: '해커 정신',
131
+ now: '지금',
132
+ contact: '연락처',
133
+ regenerate: '재생성',
134
+ },
135
+ blog: {
136
+ title: '블로그',
137
+ pageTitle: '블로그 - 페이지',
138
+ archiveDescription: 'AI 시대의 개발 감각, 웹 엔지니어링, 시스템 아키텍처 에세이.',
139
+ pageDescription: '블로그 아카이브 페이지',
140
+ previous: '이전',
141
+ next: '다음',
142
+ backToBlog: '블로그로 돌아가기',
143
+ related: '관련 글',
144
+ regenerate: '재생성',
145
+ },
146
+ footer: { tagline: 'Astro로 제작됨.' },
147
+ },
148
+ es: {
149
+ siteTitle: 'Angle Feint',
150
+ siteDescription: 'Interfaces web cinematográficas y ensayos de ingeniería en la era de IA.',
151
+ langLabel: 'Idioma',
152
+ nav: { home: 'Inicio', blog: 'Blog', about: 'Sobre mí', status: 'system: online' },
153
+ home: {
154
+ hero: 'Escribe una breve presentación del sitio y qué tipo de contenido encontrarán tus lectores.',
155
+ latest: 'Últimas publicaciones',
156
+ viewAll: 'Ver todas las publicaciones',
157
+ noPosts: 'Aún no hay publicaciones en este idioma.',
158
+ },
159
+ about: {
160
+ title: 'About — Hacker Ethos',
161
+ description: 'Quién soy, qué construyo y el ethos hacker detrás de mi trabajo.',
162
+ who: 'Quién soy',
163
+ what: 'Qué construyo',
164
+ ethos: 'Ethos hacker',
165
+ now: 'Ahora',
166
+ contact: 'Contacto',
167
+ regenerate: 'Regenerar',
168
+ },
169
+ blog: {
170
+ title: 'Blog',
171
+ pageTitle: 'Blog - Página',
172
+ archiveDescription: 'Ensayos sobre oficio en la era de IA, ingeniería web y arquitectura de sistemas.',
173
+ pageDescription: 'Página del archivo del blog',
174
+ previous: 'Anterior',
175
+ next: 'Siguiente',
176
+ backToBlog: 'Volver al blog',
177
+ related: 'Relacionados',
178
+ regenerate: 'Regenerar',
179
+ },
180
+ footer: { tagline: 'Construido con Astro.' },
181
+ },
182
+ zh: {
183
+ siteTitle: 'Angle Feint',
184
+ siteDescription: '电影感网页界面与 AI 时代工程实践文章。',
185
+ langLabel: '语言',
186
+ nav: { home: '首页', blog: '博客', about: '关于', status: 'system: online' },
187
+ home: {
188
+ hero: '在这里写一段站点简介,并告诉读者你将发布什么类型的内容。',
189
+ latest: '最新文章',
190
+ viewAll: '查看全部文章',
191
+ noPosts: '该语言暂时没有文章。',
192
+ },
193
+ about: {
194
+ title: 'About — Hacker Ethos',
195
+ description: '我是谁、我在做什么,以及背后的黑客精神。',
196
+ who: '我是谁',
197
+ what: '我在构建什么',
198
+ ethos: '黑客精神',
199
+ now: '现在',
200
+ contact: '联系',
201
+ regenerate: '重新生成',
202
+ },
203
+ blog: {
204
+ title: '博客',
205
+ pageTitle: '博客 - 第',
206
+ archiveDescription: '关于 AI 时代开发、Web 工程与系统架构的文章。',
207
+ pageDescription: '博客归档页',
208
+ previous: '上一页',
209
+ next: '下一页',
210
+ backToBlog: '返回博客',
211
+ related: '相关文章',
212
+ regenerate: '重新生成',
213
+ },
214
+ footer: { tagline: 'Built with Astro.' },
215
+ },
216
+ };
217
+
218
+ export function getMessages(locale: Locale): Messages {
219
+ return MESSAGES[locale];
220
+ }
@@ -0,0 +1,11 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import type { Locale } from './config';
3
+ import { THEME } from '@anglefeint/site-config/theme';
4
+
5
+ export const BLOG_PAGE_SIZE = THEME.BLOG_PAGE_SIZE;
6
+
7
+ export function postsForLocale(posts: CollectionEntry<'blog'>[], locale: Locale): CollectionEntry<'blog'>[] {
8
+ return posts
9
+ .filter((post) => post.id.startsWith(`${locale}/`))
10
+ .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './consts';
2
+ export * from './i18n/config';
3
+ export * from './i18n/posts';
4
+ export { getMessages } from './i18n/messages';
5
+ export { collections } from './content-schema';