@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.
- package/README.md +50 -0
- package/package.json +38 -0
- package/public/scripts/about-effects.js +535 -0
- package/public/scripts/blogpost-effects.js +956 -0
- package/public/scripts/home-matrix.js +117 -0
- package/public/styles/about-page.css +756 -0
- package/public/styles/blog-post.css +390 -0
- package/public/styles/home-page.css +186 -0
- package/src/assets/theme/placeholders/theme-placeholder-1.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-2.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-3.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-4.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-5.jpg +0 -0
- package/src/assets/theme/placeholders/theme-placeholder-about.jpg +0 -0
- package/src/assets/theme/red-queen/theme-redqueen1.webp +0 -0
- package/src/assets/theme/red-queen/theme-redqueen2.gif +0 -0
- package/src/cli-new-page.mjs +118 -0
- package/src/cli-new-post.mjs +139 -0
- package/src/components/BaseHead.astro +177 -0
- package/src/components/Footer.astro +74 -0
- package/src/components/FormattedDate.astro +17 -0
- package/src/components/Header.astro +328 -0
- package/src/components/HeaderLink.astro +35 -0
- package/src/consts.ts +12 -0
- package/src/content-schema.ts +33 -0
- package/src/i18n/config.ts +46 -0
- package/src/i18n/messages.ts +220 -0
- package/src/i18n/posts.ts +11 -0
- package/src/index.ts +5 -0
- package/src/layouts/BasePageLayout.astro +67 -0
- package/src/layouts/BlogPost.astro +279 -0
- package/src/layouts/HomePage.astro +73 -0
- package/src/styles/global.css +1867 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseHead from '../components/BaseHead.astro';
|
|
3
|
+
import Footer from '../components/Footer.astro';
|
|
4
|
+
import Header from '../components/Header.astro';
|
|
5
|
+
import { type Locale, DEFAULT_LOCALE, isLocale } from '../i18n/config';
|
|
6
|
+
import { getMessages } from '../i18n/messages';
|
|
7
|
+
|
|
8
|
+
export type PageThemeVariant = 'base' | 'cyber' | 'ai' | 'hacker' | 'matrix';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
locale?: Locale;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
theme?: PageThemeVariant;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const rawLocale = Astro.props.locale ?? DEFAULT_LOCALE;
|
|
18
|
+
const locale: Locale = isLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
|
|
19
|
+
const { title, description, theme = 'base' } = Astro.props as Props;
|
|
20
|
+
const messages = getMessages(locale);
|
|
21
|
+
const bodyClassByTheme: Record<PageThemeVariant, string> = {
|
|
22
|
+
base: 'page-default',
|
|
23
|
+
cyber: 'br-page',
|
|
24
|
+
ai: 'mesh-page',
|
|
25
|
+
hacker: 'term-page',
|
|
26
|
+
matrix: 'page-home',
|
|
27
|
+
};
|
|
28
|
+
const bodyClass = bodyClassByTheme[theme];
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<!doctype html>
|
|
32
|
+
<html lang={locale}>
|
|
33
|
+
<head>
|
|
34
|
+
<BaseHead title={title} description={description} />
|
|
35
|
+
{theme === 'hacker' && <link rel="stylesheet" href="/styles/about-page.css" />}
|
|
36
|
+
</head>
|
|
37
|
+
<body class={bodyClass}>
|
|
38
|
+
{theme === 'matrix' && <canvas id="matrix-bg" aria-hidden="true"></canvas>}
|
|
39
|
+
{theme === 'cyber' && (
|
|
40
|
+
<>
|
|
41
|
+
<div class="br-load-glitch" aria-hidden="true"></div>
|
|
42
|
+
<div class="br-spotlight" aria-hidden="true">
|
|
43
|
+
<div class="br-spotlight-tr"></div>
|
|
44
|
+
<div class="br-spotlight-tl"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="br-flicker" aria-hidden="true"></div>
|
|
47
|
+
<div class="br-haze" aria-hidden="true"></div>
|
|
48
|
+
<div class="br-vignette" aria-hidden="true"></div>
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
<Header
|
|
52
|
+
locale={locale}
|
|
53
|
+
labels={{
|
|
54
|
+
home: messages.nav.home,
|
|
55
|
+
blog: messages.nav.blog,
|
|
56
|
+
about: messages.nav.about,
|
|
57
|
+
status: messages.nav.status,
|
|
58
|
+
language: messages.langLabel,
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
<main>
|
|
62
|
+
<slot />
|
|
63
|
+
</main>
|
|
64
|
+
<Footer />
|
|
65
|
+
{theme === 'matrix' && <script is:inline src="/scripts/home-matrix.js" defer></script>}
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from 'astro:assets';
|
|
3
|
+
import type { CollectionEntry } from 'astro:content';
|
|
4
|
+
import themeRedqueen1 from '../assets/theme/red-queen/theme-redqueen1.webp';
|
|
5
|
+
import themeRedqueen2 from '../assets/theme/red-queen/theme-redqueen2.gif';
|
|
6
|
+
import BaseHead from '../components/BaseHead.astro';
|
|
7
|
+
import Footer from '../components/Footer.astro';
|
|
8
|
+
import FormattedDate from '../components/FormattedDate.astro';
|
|
9
|
+
import Header from '../components/Header.astro';
|
|
10
|
+
import { SITE_AUTHOR } from '../consts';
|
|
11
|
+
import { DEFAULT_LOCALE, type Locale, isLocale, localePath, blogIdToSlugAnyLocale } from '../i18n/config';
|
|
12
|
+
import { getMessages } from '../i18n/messages';
|
|
13
|
+
|
|
14
|
+
type Props = CollectionEntry<'blog'>['data'] & {
|
|
15
|
+
locale?: string;
|
|
16
|
+
related?: CollectionEntry<'blog'>[];
|
|
17
|
+
localeHrefs?: Partial<Record<Locale, string>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
title,
|
|
22
|
+
subtitle,
|
|
23
|
+
description,
|
|
24
|
+
pubDate,
|
|
25
|
+
updatedDate,
|
|
26
|
+
heroImage,
|
|
27
|
+
context,
|
|
28
|
+
readMinutes,
|
|
29
|
+
aiModel,
|
|
30
|
+
aiMode,
|
|
31
|
+
aiState,
|
|
32
|
+
aiLatencyMs,
|
|
33
|
+
aiConfidence,
|
|
34
|
+
wordCount,
|
|
35
|
+
tokenCount,
|
|
36
|
+
author,
|
|
37
|
+
tags,
|
|
38
|
+
locale = DEFAULT_LOCALE,
|
|
39
|
+
related = [],
|
|
40
|
+
localeHrefs,
|
|
41
|
+
} = Astro.props;
|
|
42
|
+
const resolvedLocale: Locale = isLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
43
|
+
const messages = getMessages(resolvedLocale);
|
|
44
|
+
|
|
45
|
+
// 终端风格日期
|
|
46
|
+
const fmt = (d: Date) => d.toISOString().slice(0, 10);
|
|
47
|
+
const compact = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
48
|
+
const contextText = context ?? description;
|
|
49
|
+
const hasSystemMeta = Boolean(aiModel || aiMode || aiState);
|
|
50
|
+
const hasResponseMeta = aiLatencyMs !== undefined || aiConfidence !== undefined;
|
|
51
|
+
const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
|
|
52
|
+
const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
|
|
53
|
+
|
|
54
|
+
// 点云:全屏分散但不过于稀疏
|
|
55
|
+
const w = 1200; const h = 800;
|
|
56
|
+
const pts: { x: number; y: number; r: number; depth: number }[] = [];
|
|
57
|
+
const pad = 50;
|
|
58
|
+
for (let i = 0; i < 58; i++) {
|
|
59
|
+
const x = pad + Math.random() * (w - pad * 2);
|
|
60
|
+
const y = pad + Math.random() * (h - pad * 2);
|
|
61
|
+
const depth = Math.random();
|
|
62
|
+
pts.push({
|
|
63
|
+
x, y,
|
|
64
|
+
r: 1.1 + depth * 1.3,
|
|
65
|
+
depth,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const connectDist = 138;
|
|
69
|
+
const edges: [number, number][] = [];
|
|
70
|
+
for (let i = 0; i < pts.length; i++) {
|
|
71
|
+
for (let j = i + 1; j < pts.length; j++) {
|
|
72
|
+
const d = Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);
|
|
73
|
+
if (d < connectDist) edges.push([i, j]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<html lang={resolvedLocale}>
|
|
79
|
+
<head>
|
|
80
|
+
<BaseHead
|
|
81
|
+
title={title}
|
|
82
|
+
description={description}
|
|
83
|
+
image={heroImage}
|
|
84
|
+
pageType="article"
|
|
85
|
+
publishedTime={pubDate}
|
|
86
|
+
modifiedTime={updatedDate}
|
|
87
|
+
author={author ?? SITE_AUTHOR}
|
|
88
|
+
tags={tags}
|
|
89
|
+
/>
|
|
90
|
+
<link rel="stylesheet" href="/styles/blog-post.css" />
|
|
91
|
+
</head>
|
|
92
|
+
|
|
93
|
+
<body class="mesh-page">
|
|
94
|
+
<div class="mesh-bg" aria-hidden="true">
|
|
95
|
+
<div class="mesh-glow mesh-glow-shift"></div>
|
|
96
|
+
<div class="mesh-haze" aria-hidden="true"></div>
|
|
97
|
+
<div class="mesh-vignette" aria-hidden="true"></div>
|
|
98
|
+
<div class="mesh-stripe"></div>
|
|
99
|
+
<div class="mesh-noise" aria-hidden="true"></div>
|
|
100
|
+
<div class="mesh-hex-grid" aria-hidden="true"></div>
|
|
101
|
+
<div class="mesh-thought-particles" aria-hidden="true">
|
|
102
|
+
{[...Array(12)].map((_, i) => (
|
|
103
|
+
<span class="mesh-particle" style={`--x: ${20 + (i * 7) % 60}%; --y: ${10 + (i * 11) % 70}%; --d: ${3 + (i % 4)}s`}></span>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
<svg class="mesh-network" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid slice">
|
|
107
|
+
<defs>
|
|
108
|
+
<linearGradient id="mesh-line-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
109
|
+
<stop offset="0%" stop-color="rgba(180,235,255,0.26)" />
|
|
110
|
+
<stop offset="50%" stop-color="rgba(236,252,255,0.74)" />
|
|
111
|
+
<stop offset="100%" stop-color="rgba(180,235,255,0.26)" />
|
|
112
|
+
</linearGradient>
|
|
113
|
+
<filter id="mesh-dot-glow">
|
|
114
|
+
<feGaussianBlur stdDeviation="2.2" result="blur" />
|
|
115
|
+
<feMerge>
|
|
116
|
+
<feMergeNode in="blur" />
|
|
117
|
+
<feMergeNode in="SourceGraphic" />
|
|
118
|
+
</feMerge>
|
|
119
|
+
</filter>
|
|
120
|
+
</defs>
|
|
121
|
+
<g class="mesh-lines">
|
|
122
|
+
{edges.map(([i, j]) => (
|
|
123
|
+
<line
|
|
124
|
+
x1={pts[i].x}
|
|
125
|
+
y1={pts[i].y}
|
|
126
|
+
x2={pts[j].x}
|
|
127
|
+
y2={pts[j].y}
|
|
128
|
+
stroke="url(#mesh-line-grad)"
|
|
129
|
+
stroke-width="0.4"
|
|
130
|
+
/>
|
|
131
|
+
))}
|
|
132
|
+
</g>
|
|
133
|
+
<g class="mesh-dots">
|
|
134
|
+
{pts.map((p) => (
|
|
135
|
+
<circle
|
|
136
|
+
cx={p.x}
|
|
137
|
+
cy={p.y}
|
|
138
|
+
r={p.r}
|
|
139
|
+
fill={`rgba(232,255,255,${0.72 + p.depth * 0.48})`}
|
|
140
|
+
filter="url(#mesh-dot-glow)"
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</g>
|
|
144
|
+
</svg>
|
|
145
|
+
</div>
|
|
146
|
+
<Header
|
|
147
|
+
locale={resolvedLocale}
|
|
148
|
+
localeHrefs={localeHrefs}
|
|
149
|
+
scanlines
|
|
150
|
+
labels={{
|
|
151
|
+
home: messages.nav.home,
|
|
152
|
+
blog: messages.nav.blog,
|
|
153
|
+
about: messages.nav.about,
|
|
154
|
+
status: messages.nav.status,
|
|
155
|
+
language: messages.langLabel,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
<aside class="rq-tv rq-tv-collapsed">
|
|
159
|
+
<div class="rq-tv-stage" data-rq-src={themeRedqueen1.src} data-rq-src2={themeRedqueen2.src}></div>
|
|
160
|
+
<div class="rq-tv-badge">monitor feed<span class="rq-tv-dot"></span></div>
|
|
161
|
+
<button type="button" class="rq-tv-toggle" aria-label="Replay monitor feed" aria-expanded="false">▶</button>
|
|
162
|
+
</aside>
|
|
163
|
+
<div class="mesh-read-progress" aria-hidden="true"></div>
|
|
164
|
+
<button type="button" class="mesh-back-to-top" aria-label="Back to top" title="Back to top">↑</button>
|
|
165
|
+
<div class="mesh-stage-toast" aria-live="polite" aria-atomic="true"></div>
|
|
166
|
+
<div class="mesh-mouse-glow" aria-hidden="true"></div>
|
|
167
|
+
<div class="mesh-depth-blur" aria-hidden="true"></div>
|
|
168
|
+
<div class="mesh-thinking-dots" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
169
|
+
<main class="mesh-content">
|
|
170
|
+
<div class="mesh-load-scan" aria-hidden="true"></div>
|
|
171
|
+
<article class="mesh-article">
|
|
172
|
+
{heroImage && (
|
|
173
|
+
<div class="hero-shell">
|
|
174
|
+
<div class="hero-pane">
|
|
175
|
+
<div class="hero-image">
|
|
176
|
+
<div class="hero-stack">
|
|
177
|
+
<div class="hero-canvas-wrap" aria-hidden="true">
|
|
178
|
+
<canvas class="hero-canvas" data-hero-src={heroImage.src}></canvas>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="hero-frame" aria-hidden="true">
|
|
182
|
+
<span>neural monitor</span>
|
|
183
|
+
<span class="hero-frame-dot"></span>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="hero-threat-bar" aria-hidden="true">
|
|
187
|
+
<span>signal sync active</span>
|
|
188
|
+
<span>model online</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
<div class="prose">
|
|
194
|
+
<div class="title mesh-title">
|
|
195
|
+
<div class="mesh-meta-terminal">
|
|
196
|
+
$ published {fmt(pubDate)}
|
|
197
|
+
{updatedDate && <> | updated {fmt(updatedDate)}</>}
|
|
198
|
+
{readMinutes !== undefined && <> | ~{readMinutes} min read</>}
|
|
199
|
+
</div>
|
|
200
|
+
{hasSystemMeta && (
|
|
201
|
+
<div class="mesh-system-row" aria-label="Model status">
|
|
202
|
+
{aiModel && <span class="mesh-system-chip">model: {aiModel}</span>}
|
|
203
|
+
{aiMode && <span class="mesh-system-chip">mode: {aiMode}</span>}
|
|
204
|
+
{aiState && <span class="mesh-system-chip">state: {aiState}</span>}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
{contextText && (
|
|
208
|
+
<div class="mesh-prompt-line">
|
|
209
|
+
Context: <span class="mesh-prompt-topic">{contextText}</span> →
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
<h1 class="mesh-title-text">{title}</h1>
|
|
213
|
+
{subtitle && <p class="mesh-subtitle-text">{subtitle}</p>}
|
|
214
|
+
<hr />
|
|
215
|
+
<div class="mesh-title-flow" aria-hidden="true"></div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="mesh-response-wrap">
|
|
218
|
+
<div class="mesh-response-header">
|
|
219
|
+
<div class="mesh-response-avatar" aria-hidden="true"></div>
|
|
220
|
+
<span class="mesh-response-label">Output</span>
|
|
221
|
+
{hasResponseMeta && (
|
|
222
|
+
<div class="mesh-response-meta">
|
|
223
|
+
{aiLatencyMs !== undefined && <span>latency est <strong>{aiLatencyMs}</strong> ms</span>}
|
|
224
|
+
{confidenceText !== undefined && <span>confidence <strong>{confidenceText}</strong></span>}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
<div class="mesh-prose-body mesh-prose-fade">
|
|
229
|
+
<slot />
|
|
230
|
+
<span class="mesh-block-cursor" aria-hidden="true"></span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
{hasStats && (
|
|
234
|
+
<div class="mesh-stats-corner">
|
|
235
|
+
{aiModel && <span class="mesh-model-id">{aiModel}</span>}
|
|
236
|
+
{aiModel && (wordCount !== undefined || tokenCount !== undefined) && ' · '}
|
|
237
|
+
{wordCount !== undefined && <span>{compact(wordCount)} words</span>}
|
|
238
|
+
{wordCount !== undefined && tokenCount !== undefined && ' · '}
|
|
239
|
+
{tokenCount !== undefined && <span>{compact(tokenCount)} tokens</span>}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
<button type="button" class="mesh-regenerate" aria-label={messages.blog.regenerate}>{messages.blog.regenerate}</button>
|
|
243
|
+
</div>
|
|
244
|
+
</article>
|
|
245
|
+
{related.length > 0 && (
|
|
246
|
+
<section class="mesh-related" aria-label="Related posts">
|
|
247
|
+
<h2 class="mesh-related-title">{messages.blog.related}</h2>
|
|
248
|
+
<div class="mesh-related-grid">
|
|
249
|
+
{related.map((p) => (
|
|
250
|
+
<a href={localePath(resolvedLocale, `/blog/${blogIdToSlugAnyLocale(p.id)}`)} class="mesh-related-card">
|
|
251
|
+
{p.data.heroImage ? (
|
|
252
|
+
<div class="mesh-related-img">
|
|
253
|
+
<Image width={320} height={180} src={p.data.heroImage} alt={p.data.title} />
|
|
254
|
+
</div>
|
|
255
|
+
) : (
|
|
256
|
+
<div class="mesh-related-placeholder">
|
|
257
|
+
<span>{p.data.title.charAt(0)}</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
<h3 class="mesh-related-card-title">{p.data.title}</h3>
|
|
261
|
+
<p class="mesh-related-card-date">
|
|
262
|
+
<FormattedDate date={p.data.pubDate} />
|
|
263
|
+
</p>
|
|
264
|
+
</a>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</section>
|
|
268
|
+
)}
|
|
269
|
+
<nav class="mesh-back-to-blog" aria-label="Back to blog">
|
|
270
|
+
<a href={localePath(resolvedLocale, '/blog/')}>
|
|
271
|
+
<span class="mesh-back-prompt">$</span>
|
|
272
|
+
<span class="mesh-back-text">← {messages.blog.backToBlog}</span>
|
|
273
|
+
</a>
|
|
274
|
+
</nav>
|
|
275
|
+
</main>
|
|
276
|
+
<Footer scanlines />
|
|
277
|
+
<script is:inline src="/scripts/blogpost-effects.js" defer></script>
|
|
278
|
+
</body>
|
|
279
|
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import BaseHead from '../components/BaseHead.astro';
|
|
4
|
+
import Footer from '../components/Footer.astro';
|
|
5
|
+
import FormattedDate from '../components/FormattedDate.astro';
|
|
6
|
+
import Header from '../components/Header.astro';
|
|
7
|
+
import { SITE_HERO_BY_LOCALE, SITE_TITLE } from '../consts';
|
|
8
|
+
import { type Locale, blogIdToSlugAnyLocale, localePath } from '../i18n/config';
|
|
9
|
+
import { getMessages } from '../i18n/messages';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
locale: Locale;
|
|
13
|
+
latestPosts: CollectionEntry<'blog'>[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { locale, latestPosts } = Astro.props;
|
|
17
|
+
const messages = getMessages(locale);
|
|
18
|
+
const heroText = SITE_HERO_BY_LOCALE[locale] ?? messages.home.hero;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<!doctype html>
|
|
22
|
+
<html lang={locale}>
|
|
23
|
+
<head>
|
|
24
|
+
<BaseHead title={SITE_TITLE} description={messages.siteDescription} />
|
|
25
|
+
<link rel="stylesheet" href="/styles/home-page.css" />
|
|
26
|
+
<link rel="preload" href="/fonts/ff2a407fe06752facf8828a86955e988.woff2" as="font" type="font/woff2" crossorigin />
|
|
27
|
+
</head>
|
|
28
|
+
<body class="page-home">
|
|
29
|
+
<canvas id="matrix-bg" aria-hidden="true"></canvas>
|
|
30
|
+
<Header
|
|
31
|
+
locale={locale}
|
|
32
|
+
labels={{
|
|
33
|
+
home: messages.nav.home,
|
|
34
|
+
blog: messages.nav.blog,
|
|
35
|
+
about: messages.nav.about,
|
|
36
|
+
status: messages.nav.status,
|
|
37
|
+
language: messages.langLabel,
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
<main>
|
|
41
|
+
<h1>{SITE_TITLE}</h1>
|
|
42
|
+
<p class="home-hero-copy">
|
|
43
|
+
{heroText}
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<section class="home-latest">
|
|
47
|
+
<h2 class="home-section-title">{messages.home.latest}</h2>
|
|
48
|
+
<ul class="home-post-list">
|
|
49
|
+
{
|
|
50
|
+
latestPosts.length > 0 ? (
|
|
51
|
+
latestPosts.map((post) => (
|
|
52
|
+
<li>
|
|
53
|
+
<a class="home-post-title" href={localePath(locale, `/blog/${blogIdToSlugAnyLocale(post.id)}`)}>
|
|
54
|
+
{post.data.title}
|
|
55
|
+
</a>
|
|
56
|
+
<div class="home-post-meta">
|
|
57
|
+
<FormattedDate date={post.data.pubDate} />
|
|
58
|
+
{post.data.description && <> · {post.data.description}</>}
|
|
59
|
+
</div>
|
|
60
|
+
</li>
|
|
61
|
+
))
|
|
62
|
+
) : (
|
|
63
|
+
<li>{messages.home.noPosts}</li>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
</ul>
|
|
67
|
+
<p><a href={localePath(locale, '/blog/')}>{messages.home.viewAll}</a></p>
|
|
68
|
+
</section>
|
|
69
|
+
</main>
|
|
70
|
+
<Footer />
|
|
71
|
+
<script is:inline src="/scripts/home-matrix.js" defer></script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|