@guzhongren/sha 0.1.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/package.json +41 -0
  4. package/src/avatar.ts +3 -0
  5. package/src/components/Avatar.astro +23 -0
  6. package/src/components/CodeCopyEnhancer.astro +31 -0
  7. package/src/components/DiagramEnhancer.astro +96 -0
  8. package/src/components/EChartsEnhancer.astro +31 -0
  9. package/src/components/EmojiEnhancer.astro +30 -0
  10. package/src/components/Footer.astro +12 -0
  11. package/src/components/Header.astro +30 -0
  12. package/src/components/Pagination.astro +29 -0
  13. package/src/components/PostCard.astro +44 -0
  14. package/src/components/PostList.astro +15 -0
  15. package/src/components/ProfileIntro.astro +17 -0
  16. package/src/components/SearchEnhancer.astro +183 -0
  17. package/src/components/TableOfContents.astro +35 -0
  18. package/src/components/TagList.astro +19 -0
  19. package/src/components/ThemeToggle.astro +25 -0
  20. package/src/config.ts +62 -0
  21. package/src/content.ts +20 -0
  22. package/src/emoji.ts +9 -0
  23. package/src/env.d.ts +32 -0
  24. package/src/index.ts +136 -0
  25. package/src/layouts/BaseLayout.astro +59 -0
  26. package/src/pages/about.astro +25 -0
  27. package/src/pages/categories/[category].astro +30 -0
  28. package/src/pages/categories/index.astro +18 -0
  29. package/src/pages/index.astro +28 -0
  30. package/src/pages/posts/[...slug].astro +49 -0
  31. package/src/pages/posts/index.astro +24 -0
  32. package/src/pages/posts/page/[page].astro +37 -0
  33. package/src/pages/search.astro +34 -0
  34. package/src/pages/tags/[tag].astro +30 -0
  35. package/src/pages/tags/index.astro +18 -0
  36. package/src/shortcodes.ts +8 -0
  37. package/src/styles/global.css +579 -0
  38. package/src/types.ts +94 -0
  39. package/src/utils.ts +35 -0
@@ -0,0 +1,579 @@
1
+ @import "tailwindcss";
2
+ @source "../**/*.{astro,ts,md,mdx}";
3
+ @source "../../example/src/**/*.{astro,ts,md,mdx}";
4
+
5
+ @theme {
6
+ --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
7
+ --font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
8
+ }
9
+
10
+ :root {
11
+ color-scheme: light dark;
12
+ --page-bg: var(--color-white);
13
+ --panel-bg: color-mix(in oklab, var(--color-gray-950) 2.5%, transparent);
14
+ --panel-strong: color-mix(in oklab, var(--color-gray-950) 4.5%, transparent);
15
+ --border-soft: color-mix(in oklab, var(--color-gray-950) 8%, transparent);
16
+ --border-hairline: color-mix(in oklab, var(--color-gray-950) 5%, transparent);
17
+ --text-strong: var(--color-gray-950);
18
+ --text-body: var(--color-gray-700);
19
+ --text-muted: var(--color-gray-500);
20
+ --accent: var(--color-sky-500);
21
+ }
22
+
23
+ .dark {
24
+ color-scheme: dark;
25
+ --page-bg: var(--color-gray-950);
26
+ --panel-bg: color-mix(in oklab, var(--color-white) 5%, transparent);
27
+ --panel-strong: color-mix(in oklab, var(--color-white) 8%, transparent);
28
+ --border-soft: color-mix(in oklab, var(--color-white) 10%, transparent);
29
+ --border-hairline: color-mix(in oklab, var(--color-white) 6%, transparent);
30
+ --text-strong: var(--color-white);
31
+ --text-body: var(--color-gray-300);
32
+ --text-muted: var(--color-gray-400);
33
+ --accent: var(--color-sky-400);
34
+ }
35
+
36
+ html {
37
+ background: var(--page-bg);
38
+ color: var(--text-body);
39
+ font-family: var(--font-sans);
40
+ }
41
+
42
+ body {
43
+ margin: 0;
44
+ min-width: 320px;
45
+ background:
46
+ radial-gradient(70rem 36rem at 50% -16rem, color-mix(in oklab, var(--accent) 14%, transparent), transparent 62%),
47
+ linear-gradient(var(--page-bg), var(--page-bg)),
48
+ radial-gradient(color-mix(in oklab, var(--accent) 10%, transparent) 1px, transparent 1px);
49
+ background-size: auto, auto, 24px 24px;
50
+ }
51
+
52
+ a {
53
+ color: inherit;
54
+ text-decoration: none;
55
+ }
56
+
57
+ :focus-visible {
58
+ outline: 2px solid var(--accent);
59
+ outline-offset: 3px;
60
+ }
61
+
62
+ .line-y {
63
+ border-block: 1px solid var(--border-soft);
64
+ }
65
+
66
+ .line-t {
67
+ border-top: 1px solid var(--border-soft);
68
+ }
69
+
70
+ .line-b {
71
+ border-bottom: 1px solid var(--border-soft);
72
+ }
73
+
74
+ .line-x {
75
+ border-inline: 1px solid var(--border-soft);
76
+ }
77
+
78
+ .rule-fade {
79
+ height: 1px;
80
+ background: linear-gradient(90deg, transparent, var(--border-soft) 18%, var(--border-soft) 82%, transparent);
81
+ }
82
+
83
+ .rule-dashed {
84
+ border-top: 1px dashed color-mix(in oklab, var(--text-strong) 16%, transparent);
85
+ }
86
+
87
+ .section-frame {
88
+ position: relative;
89
+ border: 1px solid var(--border-hairline);
90
+ background:
91
+ linear-gradient(var(--panel-bg), var(--panel-bg)),
92
+ radial-gradient(color-mix(in oklab, var(--text-strong) 10%, transparent) 1px, transparent 1px);
93
+ background-size: auto, 14px 14px;
94
+ }
95
+
96
+ .section-frame::before,
97
+ .section-frame::after {
98
+ position: absolute;
99
+ width: 0.45rem;
100
+ height: 0.45rem;
101
+ border-color: var(--accent);
102
+ opacity: 0.65;
103
+ content: "";
104
+ }
105
+
106
+ .section-frame::before {
107
+ top: -1px;
108
+ left: -1px;
109
+ border-top: 1px solid;
110
+ border-left: 1px solid;
111
+ }
112
+
113
+ .section-frame::after {
114
+ right: -1px;
115
+ bottom: -1px;
116
+ border-right: 1px solid;
117
+ border-bottom: 1px solid;
118
+ }
119
+
120
+ .surface-block {
121
+ border: 1px solid var(--border-soft);
122
+ border-radius: 0.75rem;
123
+ background: var(--panel-bg);
124
+ box-shadow: inset 0 1px 0 color-mix(in oklab, var(--color-white) 8%, transparent);
125
+ }
126
+
127
+ .gutter-stripes {
128
+ background-image: repeating-linear-gradient(
129
+ 315deg,
130
+ color-mix(in oklab, var(--text-strong) 8%, transparent) 0,
131
+ color-mix(in oklab, var(--text-strong) 8%, transparent) 1px,
132
+ transparent 0,
133
+ transparent 50%
134
+ );
135
+ background-size: 10px 10px;
136
+ background-attachment: fixed;
137
+ }
138
+
139
+ .utility-note {
140
+ font-family: var(--font-mono);
141
+ color: color-mix(in oklab, var(--text-strong) 22%, transparent);
142
+ }
143
+
144
+ .avatar-mark {
145
+ border: 1px solid var(--border-soft);
146
+ }
147
+
148
+ .profile-mark {
149
+ box-shadow:
150
+ 0 0 0 1px var(--border-soft),
151
+ 0 0 0 0.5rem var(--panel-bg),
152
+ 0 0 7rem color-mix(in oklab, var(--accent) 18%, transparent);
153
+ }
154
+
155
+ .post-item {
156
+ position: relative;
157
+ transition:
158
+ border-color 150ms ease,
159
+ background-color 150ms ease,
160
+ transform 150ms ease;
161
+ }
162
+
163
+ .post-item::before {
164
+ position: absolute;
165
+ top: -1px;
166
+ left: 0;
167
+ width: 2.5rem;
168
+ height: 1px;
169
+ background: var(--accent);
170
+ opacity: 0;
171
+ content: "";
172
+ transition: opacity 150ms ease;
173
+ }
174
+
175
+ .post-item:hover {
176
+ background: linear-gradient(90deg, color-mix(in oklab, var(--accent) 5%, transparent), transparent 45%);
177
+ }
178
+
179
+ .post-item:hover::before {
180
+ opacity: 1;
181
+ }
182
+
183
+ .header-nav {
184
+ gap: 0;
185
+ }
186
+
187
+ .header-nav > * {
188
+ position: relative;
189
+ }
190
+
191
+ .header-nav > * + * {
192
+ margin-left: 1.1rem;
193
+ }
194
+
195
+ .header-nav > * + *::before {
196
+ position: absolute;
197
+ top: 50%;
198
+ left: -0.55rem;
199
+ width: 1px;
200
+ height: 1rem;
201
+ background: linear-gradient(180deg, transparent, var(--border-soft), transparent);
202
+ content: "";
203
+ transform: translateY(-50%);
204
+ }
205
+
206
+ .search-trigger {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ border: 1px solid var(--border-soft);
211
+ border-radius: 999px;
212
+ background: var(--panel-bg);
213
+ padding: 0.25rem 0.35rem 0.25rem 0.75rem;
214
+ color: var(--text-muted);
215
+ cursor: pointer;
216
+ }
217
+
218
+ .search-trigger:hover {
219
+ color: var(--text-strong);
220
+ border-color: color-mix(in oklab, var(--accent) 35%, var(--border-soft));
221
+ }
222
+
223
+ .search-trigger kbd {
224
+ border: 1px solid var(--border-hairline);
225
+ border-radius: 999px;
226
+ background: var(--page-bg);
227
+ padding: 0.1rem 0.45rem;
228
+ color: var(--text-muted);
229
+ font-family: var(--font-mono);
230
+ font-size: 0.6875rem;
231
+ line-height: 1.4;
232
+ }
233
+
234
+ .search-dialog {
235
+ position: fixed;
236
+ top: clamp(4rem, 12vh, 7rem);
237
+ right: auto;
238
+ bottom: auto;
239
+ left: 50%;
240
+ width: min(42rem, calc(100vw - 2rem));
241
+ max-height: min(42rem, calc(100dvh - 8rem));
242
+ border: 0;
243
+ margin: 0;
244
+ background: transparent;
245
+ padding: 0;
246
+ color: inherit;
247
+ transform: translateX(-50%);
248
+ }
249
+
250
+ .search-dialog::backdrop {
251
+ background:
252
+ radial-gradient(42rem 28rem at 50% 8rem, color-mix(in oklab, var(--accent) 16%, transparent), transparent 70%),
253
+ color-mix(in oklab, var(--color-gray-950) 48%, transparent);
254
+ backdrop-filter: blur(8px);
255
+ }
256
+
257
+ .search-modal,
258
+ .search-page {
259
+ border: 1px solid var(--border-soft);
260
+ border-radius: 0.75rem;
261
+ background:
262
+ linear-gradient(var(--page-bg), var(--page-bg)),
263
+ radial-gradient(color-mix(in oklab, var(--accent) 8%, transparent) 1px, transparent 1px);
264
+ background-size: auto, 20px 20px;
265
+ box-shadow:
266
+ inset 0 1px 0 color-mix(in oklab, var(--color-white) 8%, transparent),
267
+ 0 1.5rem 5rem color-mix(in oklab, var(--color-gray-950) 18%, transparent);
268
+ }
269
+
270
+ .search-modal {
271
+ padding: 1rem;
272
+ }
273
+
274
+ .search-page {
275
+ padding: 1rem;
276
+ }
277
+
278
+ @media (min-width: 640px) {
279
+ .search-modal,
280
+ .search-page {
281
+ padding: 1.25rem;
282
+ }
283
+ }
284
+
285
+ .search-modal-header {
286
+ display: flex;
287
+ align-items: center;
288
+ justify-content: space-between;
289
+ gap: 1rem;
290
+ margin-bottom: 0.75rem;
291
+ }
292
+
293
+ .search-close {
294
+ border: 1px solid var(--border-soft);
295
+ border-radius: 999px;
296
+ background: var(--panel-bg);
297
+ padding: 0.25rem 0.55rem;
298
+ color: var(--text-muted);
299
+ font-family: var(--font-mono);
300
+ font-size: 0.75rem;
301
+ cursor: pointer;
302
+ }
303
+
304
+ .search-close:hover {
305
+ color: var(--text-strong);
306
+ border-color: color-mix(in oklab, var(--accent) 35%, var(--border-soft));
307
+ }
308
+
309
+ .search-field {
310
+ display: block;
311
+ }
312
+
313
+ .search-field input {
314
+ width: 100%;
315
+ border: 1px solid var(--border-soft);
316
+ border-radius: 0.75rem;
317
+ background: var(--panel-bg);
318
+ padding: 0.9rem 1rem;
319
+ color: var(--text-strong);
320
+ font: inherit;
321
+ font-size: 1rem;
322
+ line-height: 1.5;
323
+ }
324
+
325
+ .search-field input::placeholder {
326
+ color: var(--text-muted);
327
+ }
328
+
329
+ .search-status {
330
+ margin-top: 0.75rem;
331
+ color: var(--text-muted);
332
+ font-family: var(--font-mono);
333
+ font-size: 0.75rem;
334
+ line-height: 1.5;
335
+ }
336
+
337
+ .search-results {
338
+ display: grid;
339
+ gap: 0.5rem;
340
+ max-height: min(24rem, calc(100dvh - 18rem));
341
+ margin-top: 1rem;
342
+ overflow: auto;
343
+ padding: 0;
344
+ list-style: none;
345
+ }
346
+
347
+ .search-result {
348
+ display: grid;
349
+ gap: 0.35rem;
350
+ border: 1px solid var(--border-hairline);
351
+ border-radius: 0.75rem;
352
+ background: var(--panel-bg);
353
+ padding: 0.9rem 1rem;
354
+ }
355
+
356
+ .search-result:hover,
357
+ .search-result:focus-visible {
358
+ border-color: color-mix(in oklab, var(--accent) 45%, var(--border-soft));
359
+ background: linear-gradient(90deg, color-mix(in oklab, var(--accent) 7%, transparent), var(--panel-bg));
360
+ }
361
+
362
+ .search-result-meta {
363
+ color: var(--text-muted);
364
+ font-family: var(--font-mono);
365
+ font-size: 0.72rem;
366
+ line-height: 1.5;
367
+ text-transform: uppercase;
368
+ }
369
+
370
+ .search-result-title {
371
+ color: var(--text-strong);
372
+ font-weight: 650;
373
+ line-height: 1.4;
374
+ }
375
+
376
+ .search-result-excerpt {
377
+ color: var(--text-muted);
378
+ font-size: 0.875rem;
379
+ line-height: 1.7;
380
+ }
381
+
382
+ .search-result mark {
383
+ border-radius: 0.25rem;
384
+ background: color-mix(in oklab, var(--accent) 24%, transparent);
385
+ color: var(--text-strong);
386
+ padding-inline: 0.12rem;
387
+ }
388
+
389
+ .prose {
390
+ color: var(--text-body);
391
+ font-size: 0.9375rem;
392
+ line-height: 1.9;
393
+ }
394
+
395
+ .prose > * + * {
396
+ margin-top: 1.5rem;
397
+ }
398
+
399
+ .prose h2,
400
+ .prose h3 {
401
+ color: var(--text-strong);
402
+ font-weight: 650;
403
+ letter-spacing: -0.02em;
404
+ scroll-margin-top: 5rem;
405
+ }
406
+
407
+ .prose h2 {
408
+ margin-top: 3.5rem;
409
+ font-size: 1.35rem;
410
+ line-height: 1.35;
411
+ }
412
+
413
+ .prose h3 {
414
+ margin-top: 2.5rem;
415
+ font-size: 1.1rem;
416
+ line-height: 1.45;
417
+ }
418
+
419
+ .prose a {
420
+ color: var(--text-strong);
421
+ font-weight: 650;
422
+ text-decoration: underline;
423
+ text-decoration-color: var(--accent);
424
+ text-underline-offset: 3px;
425
+ }
426
+
427
+ .prose code {
428
+ color: var(--text-strong);
429
+ font-family: var(--font-mono);
430
+ font-size: 0.875em;
431
+ font-weight: 600;
432
+ }
433
+
434
+ .prose :not(pre) > code::before,
435
+ .prose :not(pre) > code::after {
436
+ content: "`";
437
+ }
438
+
439
+ .prose pre {
440
+ overflow-x: auto;
441
+ border-radius: 0.75rem;
442
+ border: 1px solid color-mix(in oklab, var(--color-white) 12%, transparent);
443
+ background: var(--color-gray-950);
444
+ padding: 1rem;
445
+ color: var(--color-gray-100);
446
+ font-family: var(--font-mono);
447
+ font-size: 0.875rem;
448
+ line-height: 1.75;
449
+ }
450
+
451
+ .prose hr {
452
+ height: 1px;
453
+ border: 0;
454
+ margin-block: 2.5rem;
455
+ background: linear-gradient(90deg, transparent, var(--border-soft), transparent);
456
+ }
457
+
458
+ .code-copy-frame {
459
+ position: relative;
460
+ }
461
+
462
+ .code-copy-frame pre {
463
+ padding-top: 2.75rem;
464
+ }
465
+
466
+ .code-copy-button {
467
+ position: absolute;
468
+ top: 0.65rem;
469
+ right: 0.65rem;
470
+ z-index: 1;
471
+ border: 1px solid color-mix(in oklab, var(--color-white) 15%, transparent);
472
+ border-radius: 999px;
473
+ background: color-mix(in oklab, var(--color-gray-950) 82%, transparent);
474
+ padding: 0.25rem 0.65rem;
475
+ color: var(--color-gray-300);
476
+ font-family: var(--font-mono);
477
+ font-size: 0.75rem;
478
+ line-height: 1.4;
479
+ cursor: pointer;
480
+ }
481
+
482
+ .code-copy-button:hover {
483
+ color: var(--color-white);
484
+ border-color: color-mix(in oklab, var(--color-white) 30%, transparent);
485
+ }
486
+
487
+ .prose blockquote {
488
+ border: 1px solid var(--border-soft);
489
+ border-left: 3px solid var(--accent);
490
+ border-radius: 0.75rem;
491
+ background: var(--panel-bg);
492
+ padding: 1rem 1.25rem;
493
+ color: var(--text-muted);
494
+ }
495
+
496
+ .prose table {
497
+ width: 100%;
498
+ border-collapse: collapse;
499
+ overflow: hidden;
500
+ border-radius: 0.75rem;
501
+ font-size: 0.875rem;
502
+ }
503
+
504
+ .prose th,
505
+ .prose td {
506
+ border-bottom: 1px solid var(--border-hairline);
507
+ padding: 0.75rem;
508
+ text-align: left;
509
+ }
510
+
511
+ .prose th {
512
+ color: var(--text-strong);
513
+ font-weight: 650;
514
+ background: var(--panel-bg);
515
+ }
516
+
517
+ .prose .block,
518
+ .prose .note,
519
+ .prose .callout {
520
+ border: 1px solid var(--border-soft);
521
+ border-radius: 0.75rem;
522
+ background: var(--panel-bg);
523
+ padding: 1rem 1.25rem;
524
+ }
525
+
526
+ .prose ul,
527
+ .prose ol {
528
+ padding-left: 1.5rem;
529
+ }
530
+
531
+ .prose li + li {
532
+ margin-top: 0.5rem;
533
+ }
534
+
535
+ .diagram {
536
+ overflow-x: auto;
537
+ border: 1px solid var(--border-soft);
538
+ border-radius: 0.75rem;
539
+ background: var(--panel-bg);
540
+ padding: 1rem;
541
+ box-shadow: inset 0 1px 0 color-mix(in oklab, var(--color-white) 8%, transparent);
542
+ }
543
+
544
+ .diagram svg,
545
+ .diagram img {
546
+ margin-inline: auto;
547
+ max-width: 100%;
548
+ }
549
+
550
+ .diagram-plantuml img {
551
+ min-height: 8rem;
552
+ }
553
+
554
+ .echarts-frame {
555
+ overflow: hidden;
556
+ border: 1px solid var(--border-soft);
557
+ border-radius: 0.75rem;
558
+ background:
559
+ linear-gradient(var(--panel-bg), var(--panel-bg)),
560
+ radial-gradient(color-mix(in oklab, var(--text-strong) 8%, transparent) 1px, transparent 1px);
561
+ background-size: auto, 18px 18px;
562
+ padding: 0.75rem;
563
+ box-shadow: inset 0 1px 0 color-mix(in oklab, var(--color-white) 8%, transparent);
564
+ }
565
+
566
+ .echarts-canvas {
567
+ min-height: 24rem;
568
+ width: 100%;
569
+ }
570
+
571
+ .echarts-error {
572
+ margin-top: 0.75rem;
573
+ }
574
+
575
+ @media (max-width: 640px) {
576
+ .echarts-canvas {
577
+ min-height: 19rem;
578
+ }
579
+ }
package/src/types.ts ADDED
@@ -0,0 +1,94 @@
1
+ export type ThemeMode = "system" | "light" | "dark";
2
+ export type AccentColor = "sky" | "teal" | "violet" | "pink";
3
+ export type SocialIcon = "github" | "x" | "rss" | "mail" | "link";
4
+
5
+ export type BlogThemeOptions = {
6
+ site: {
7
+ name: string;
8
+ title?: string;
9
+ description?: string;
10
+ lang?: string;
11
+ };
12
+ author: {
13
+ name: string;
14
+ headline?: string;
15
+ bio?: string;
16
+ avatar?: string;
17
+ };
18
+ nav?: Array<{
19
+ label: string;
20
+ href: string;
21
+ }>;
22
+ socialLinks?: Array<{
23
+ label: string;
24
+ href: string;
25
+ icon?: SocialIcon;
26
+ }>;
27
+ postsPerPage?: number;
28
+ theme?: {
29
+ defaultMode?: ThemeMode;
30
+ accent?: AccentColor;
31
+ };
32
+ diagrams?: {
33
+ mermaid?: boolean;
34
+ plantuml?:
35
+ | boolean
36
+ | {
37
+ serverUrl?: string;
38
+ };
39
+ };
40
+ routes?:
41
+ | false
42
+ | {
43
+ home?: boolean;
44
+ posts?: boolean;
45
+ tags?: boolean;
46
+ categories?: boolean;
47
+ about?: boolean;
48
+ search?: boolean;
49
+ };
50
+ };
51
+
52
+ export type NormalizedBlogThemeOptions = {
53
+ site: {
54
+ name: string;
55
+ title: string;
56
+ description: string;
57
+ lang: string;
58
+ };
59
+ author: {
60
+ name: string;
61
+ headline: string;
62
+ bio: string;
63
+ avatar?: string;
64
+ };
65
+ nav: Array<{
66
+ label: string;
67
+ href: string;
68
+ }>;
69
+ socialLinks: Array<{
70
+ label: string;
71
+ href: string;
72
+ icon: SocialIcon;
73
+ }>;
74
+ postsPerPage: number;
75
+ theme: {
76
+ defaultMode: ThemeMode;
77
+ accent: AccentColor;
78
+ };
79
+ diagrams: {
80
+ mermaid: boolean;
81
+ plantuml: {
82
+ enabled: boolean;
83
+ serverUrl: string;
84
+ };
85
+ };
86
+ routes: {
87
+ home: boolean;
88
+ posts: boolean;
89
+ tags: boolean;
90
+ categories: boolean;
91
+ about: boolean;
92
+ search: boolean;
93
+ };
94
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { CollectionEntry } from "astro:content";
2
+
3
+ export type Post = CollectionEntry<"posts">;
4
+
5
+ export function isPublished(post: Post) {
6
+ return !post.data.draft;
7
+ }
8
+
9
+ export function sortPosts(posts: Post[]) {
10
+ return [...posts].sort((a, b) => (b.data.publishDate?.getTime() ?? 0) - (a.data.publishDate?.getTime() ?? 0));
11
+ }
12
+
13
+ export function formatDate(date: Date, locale = "zh-CN") {
14
+ return new Intl.DateTimeFormat(locale, {
15
+ year: "numeric",
16
+ month: "2-digit",
17
+ day: "2-digit",
18
+ }).format(date);
19
+ }
20
+
21
+ export function slugify(input: string) {
22
+ return input
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ }
28
+
29
+ export function uniqueSorted(values: string[]) {
30
+ return [...new Set(values)].sort((a, b) => a.localeCompare(b));
31
+ }
32
+
33
+ export function postHref(post: Post) {
34
+ return `/posts/${post.id}`;
35
+ }