create-eziwiki 0.1.0 → 0.1.1
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/package.json +1 -1
- package/template/app/globals.css +8 -4
- package/template/app/layout.tsx +43 -23
- package/template/app/robots.ts +11 -3
- package/template/components/search/SearchTrigger.tsx +11 -4
- package/template/lib/markdown/rehype-plugins.ts +19 -2
- package/template/lib/markdown/render.ts +21 -3
- package/template/public/fonts/Pretendard/pretendard-400-korean.woff2 +0 -0
- package/template/public/fonts/Pretendard/pretendard-400-latin.woff2 +0 -0
- package/template/public/fonts/Pretendard/pretendard-600-korean.woff2 +0 -0
- package/template/public/fonts/Pretendard/pretendard-600-latin.woff2 +0 -0
- package/template/public/fonts/Pretendard/pretendard-700-korean.woff2 +0 -0
- package/template/public/fonts/Pretendard/pretendard-700-latin.woff2 +0 -0
- package/template/styles/markdown.css +2 -2
- package/template/styles/theme.css +7 -0
- package/template/vercel.json +31 -0
- package/template/public/fonts/Pretandard/Pretendard-Bold.woff2 +0 -0
- package/template/public/fonts/Pretandard/Pretendard-Regular.woff2 +0 -0
- package/template/public/fonts/Pretandard/Pretendard-SemiBold.woff2 +0 -0
- package/template/public/fonts/SUITE/SUITE-Bold.woff2 +0 -0
- package/template/public/fonts/SUITE/SUITE-Regular.woff2 +0 -0
- package/template/public/fonts/SUITE/SUITE-SemiBold.woff2 +0 -0
package/package.json
CHANGED
package/template/app/globals.css
CHANGED
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
@import '../styles/markdown.css';
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
|
-
* `@font-face` for Pretendard
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `@font-face` for Pretendard is declared in `app/layout.tsx`, not here: the
|
|
13
|
+
* font URLs need the deployment base path prefixed, which a stylesheet cannot
|
|
14
|
+
* read. See FONT_FACES there, which also splits each weight by `unicode-range`
|
|
15
|
+
* so a page fetches only the character sets it uses.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
body {
|
|
19
|
+
/* SUITE used to sit between Pretendard and the system stack. Pretendard
|
|
20
|
+
covers Latin and Korean, so nothing ever reached it except the emoji in
|
|
21
|
+
section names — which it has no glyphs for either, and which the platform
|
|
22
|
+
emoji font renders. It was 274 kB nobody saw. */
|
|
18
23
|
font-family:
|
|
19
24
|
'Pretendard',
|
|
20
|
-
'SUITE',
|
|
21
25
|
-apple-system,
|
|
22
26
|
BlinkMacSystemFont,
|
|
23
27
|
'Segoe UI',
|
package/template/app/layout.tsx
CHANGED
|
@@ -74,6 +74,23 @@ export const metadata: Metadata = {
|
|
|
74
74
|
: undefined,
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Character ranges each font file covers.
|
|
79
|
+
*
|
|
80
|
+
* Pretendard carries the full Korean syllabary, which is most of its weight:
|
|
81
|
+
* one weight is 750 kB whole, 27 kB once the Hangul is taken out. Splitting it
|
|
82
|
+
* and declaring what each half covers lets the browser fetch only the halves a
|
|
83
|
+
* page actually uses — an English page never asks for the Korean file, and a
|
|
84
|
+
* Korean one pays for it once and keeps it, since `public/` is served
|
|
85
|
+
* immutable.
|
|
86
|
+
*/
|
|
87
|
+
const RANGES = {
|
|
88
|
+
latin:
|
|
89
|
+
'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,' +
|
|
90
|
+
'U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
|
|
91
|
+
korean: 'U+1100-11FF,U+3000-303F,U+3130-318F,U+A960-A97F,U+AC00-D7A3,U+D7B0-D7FF,U+FF00-FFEF',
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
77
94
|
/**
|
|
78
95
|
* Web fonts, declared here rather than in `globals.css`.
|
|
79
96
|
*
|
|
@@ -83,31 +100,34 @@ export const metadata: Metadata = {
|
|
|
83
100
|
* the declarations from here puts them through `asset()` like every other file
|
|
84
101
|
* in `public/`.
|
|
85
102
|
*
|
|
86
|
-
*
|
|
103
|
+
* Only the Latin halves are preloaded. Preloading the Korean ones would undo
|
|
104
|
+
* the split by fetching them before anything has asked for a Korean glyph.
|
|
87
105
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
file: '/fonts/Pretandard/Pretendard-SemiBold.woff2',
|
|
102
|
-
preload: true,
|
|
103
|
-
},
|
|
104
|
-
{ family: 'Pretendard', weight: 700, file: '/fonts/Pretandard/Pretendard-Bold.woff2' },
|
|
106
|
+
interface FontFace {
|
|
107
|
+
weight: number;
|
|
108
|
+
subset: keyof typeof RANGES;
|
|
109
|
+
preload?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const FONT_FACES: FontFace[] = [
|
|
113
|
+
{ weight: 400, subset: 'latin', preload: true },
|
|
114
|
+
{ weight: 400, subset: 'korean' },
|
|
115
|
+
{ weight: 600, subset: 'latin', preload: true },
|
|
116
|
+
{ weight: 600, subset: 'korean' },
|
|
117
|
+
{ weight: 700, subset: 'latin' },
|
|
118
|
+
{ weight: 700, subset: 'korean' },
|
|
105
119
|
];
|
|
106
120
|
|
|
121
|
+
/** Where a weight-and-subset pair is served from. */
|
|
122
|
+
function fontFile({ weight, subset }: FontFace): string {
|
|
123
|
+
return `/fonts/Pretendard/pretendard-${weight}-${subset}.woff2`;
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
const fontFaceCss = FONT_FACES.map(
|
|
108
|
-
(
|
|
109
|
-
`@font-face{font-family:'
|
|
110
|
-
`src:url('${asset(
|
|
127
|
+
(font) =>
|
|
128
|
+
`@font-face{font-family:'Pretendard';font-weight:${font.weight};` +
|
|
129
|
+
`src:url('${asset(fontFile(font))}') format('woff2');font-display:swap;` +
|
|
130
|
+
`unicode-range:${RANGES[font.subset]}}`,
|
|
111
131
|
).join('');
|
|
112
132
|
|
|
113
133
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
@@ -119,9 +139,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
119
139
|
<head>
|
|
120
140
|
{FONT_FACES.filter((font) => font.preload).map((font) => (
|
|
121
141
|
<link
|
|
122
|
-
key={font
|
|
142
|
+
key={fontFile(font)}
|
|
123
143
|
rel="preload"
|
|
124
|
-
href={asset(font
|
|
144
|
+
href={asset(fontFile(font))}
|
|
125
145
|
as="font"
|
|
126
146
|
type="font/woff2"
|
|
127
147
|
crossOrigin="anonymous"
|
package/template/app/robots.ts
CHANGED
|
@@ -3,8 +3,17 @@ import { payload } from '@/payload/config';
|
|
|
3
3
|
import { fileUrl } from '@/lib/basePath';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Generates robots.txt.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is disallowed. Search engines render a page before judging it, so
|
|
9
|
+
* blocking `/_next/` — where the stylesheet and scripts live — leaves the
|
|
10
|
+
* crawler looking at an unstyled document and reporting the assets as blocked
|
|
11
|
+
* resources. There is no `/api/` either: the site is a static export.
|
|
12
|
+
*
|
|
13
|
+
* Pages that should stay out of the index carry `noindex` in their own
|
|
14
|
+
* metadata, which is the mechanism that actually keeps them out. A path
|
|
15
|
+
* disallowed here would still be indexable if something linked to it, since a
|
|
16
|
+
* crawler that is told not to fetch a page cannot read the `noindex` on it.
|
|
8
17
|
*
|
|
9
18
|
* @returns Robots configuration
|
|
10
19
|
*/
|
|
@@ -13,7 +22,6 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
13
22
|
rules: {
|
|
14
23
|
userAgent: '*',
|
|
15
24
|
allow: '/',
|
|
16
|
-
disallow: ['/api/', '/_next/'],
|
|
17
25
|
},
|
|
18
26
|
sitemap: fileUrl('/sitemap.xml', payload.global.baseUrl),
|
|
19
27
|
};
|
|
@@ -22,17 +22,24 @@ export function SearchTrigger({ className = '' }: { className?: string }) {
|
|
|
22
22
|
}, []);
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
+
// No aria-label: it read "Search documentation" while the control visibly
|
|
26
|
+
// says "Search…", so the spoken name did not contain the written one and
|
|
27
|
+
// voice control had nothing to match. The text names the button instead;
|
|
28
|
+
// the icon and the shortcut are hidden from the name, the latter because
|
|
29
|
+
// `aria-keyshortcuts` already announces it.
|
|
25
30
|
<button
|
|
26
31
|
type="button"
|
|
27
32
|
onClick={open}
|
|
28
|
-
aria-label="Search documentation"
|
|
29
33
|
aria-keyshortcuts="Meta+K Control+K"
|
|
30
|
-
className={`flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-
|
|
34
|
+
className={`flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-500 transition-colors hover:border-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-gray-600 ${className}`}
|
|
31
35
|
>
|
|
32
|
-
<Search className="h-4 w-4 flex-shrink-0" />
|
|
36
|
+
<Search className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
|
|
33
37
|
<span className="min-w-0 flex-1 truncate text-left">Search…</span>
|
|
34
38
|
{shortcut && (
|
|
35
|
-
<kbd
|
|
39
|
+
<kbd
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
className="hidden flex-shrink-0 rounded border border-gray-300 px-1 py-0.5 text-[10px] text-gray-500 sm:block dark:border-gray-600 dark:text-gray-400"
|
|
42
|
+
>
|
|
36
43
|
{shortcut}
|
|
37
44
|
</kbd>
|
|
38
45
|
)}
|
|
@@ -221,15 +221,32 @@ export function rehypeBasePath(basePath: string) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* Adds
|
|
224
|
+
* Adds loading hints and consistent styling hooks to content images.
|
|
225
|
+
*
|
|
226
|
+
* Every image was deferred, including the first one. On a page that opens with
|
|
227
|
+
* a figure that image is what the browser measures as the largest contentful
|
|
228
|
+
* paint, and `loading="lazy"` keeps it out of the preload scan — the request
|
|
229
|
+
* only starts once layout has reached it, so the headline metric waits on a
|
|
230
|
+
* round trip that need not have been late. The first image is therefore
|
|
231
|
+
* fetched eagerly and marked high priority; the rest, which are further down,
|
|
232
|
+
* keep deferring.
|
|
225
233
|
*/
|
|
226
234
|
export function rehypeImages() {
|
|
227
235
|
return (tree: Root) => {
|
|
236
|
+
let seen = 0;
|
|
237
|
+
|
|
228
238
|
visit(tree, 'element', (node: Element) => {
|
|
229
239
|
if (node.tagName !== 'img') return;
|
|
230
240
|
|
|
231
241
|
node.properties ??= {};
|
|
232
|
-
|
|
242
|
+
const isFirst = seen++ === 0;
|
|
243
|
+
|
|
244
|
+
if (isFirst) {
|
|
245
|
+
node.properties.loading ??= 'eager';
|
|
246
|
+
node.properties.fetchPriority ??= 'high';
|
|
247
|
+
} else {
|
|
248
|
+
node.properties.loading ??= 'lazy';
|
|
249
|
+
}
|
|
233
250
|
node.properties.decoding ??= 'async';
|
|
234
251
|
|
|
235
252
|
const className = node.properties.className;
|
|
@@ -43,14 +43,32 @@ export interface RenderedMarkdown {
|
|
|
43
43
|
headings: Heading[];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Syntax highlighting themes, applied as CSS variables for light and dark.
|
|
48
|
+
*
|
|
49
|
+
* The high-contrast variants, because the plain ones are tuned for GitHub's
|
|
50
|
+
* near-white code background: against the slightly darker grey used here their
|
|
51
|
+
* strings, keywords and comments land between 4.15:1 and 4.37:1, under the
|
|
52
|
+
* 4.5:1 that body-sized text needs. Lightening the background would fix the
|
|
53
|
+
* ratios too, but a code block that matches the page around it stops reading
|
|
54
|
+
* as a code block.
|
|
55
|
+
*/
|
|
56
|
+
const SHIKI_THEMES = {
|
|
57
|
+
light: 'github-light-high-contrast',
|
|
58
|
+
dark: 'github-dark-high-contrast',
|
|
59
|
+
} as const;
|
|
48
60
|
|
|
49
61
|
let processor: Processor | null = null;
|
|
50
62
|
|
|
51
63
|
/**
|
|
52
64
|
* Resolves a wiki-link target to a destination in this site.
|
|
53
65
|
*
|
|
66
|
+
* The trailing slash is applied here rather than downstream. Under the `path`
|
|
67
|
+
* strategy `rehypeInternalLinks` sees a content path it can resolve and adds
|
|
68
|
+
* one, but under `hash` the URL is already a digest, which does not map back
|
|
69
|
+
* to a document — so the link was left without it and every wiki link cost a
|
|
70
|
+
* redirect.
|
|
71
|
+
*
|
|
54
72
|
* @param target - Raw target text from inside the brackets
|
|
55
73
|
* @returns The destination, or null when the target does not resolve
|
|
56
74
|
*/
|
|
@@ -59,7 +77,7 @@ function resolveWikiLink(target: string): WikiLinkTarget | null {
|
|
|
59
77
|
if (!doc) return null;
|
|
60
78
|
|
|
61
79
|
const url = docPathToUrl(getUrlMap(), doc.path);
|
|
62
|
-
return url ? { url: `/${url}
|
|
80
|
+
return url ? { url: `/${url}/`, title: doc.title } : null;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
/**
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
font-size: 0.75rem;
|
|
32
32
|
text-transform: uppercase;
|
|
33
33
|
letter-spacing: 0.03em;
|
|
34
|
-
color: var(--color-
|
|
34
|
+
color: var(--color-code-muted);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.ezw-code__copy {
|
|
38
38
|
font-size: 0.75rem;
|
|
39
39
|
padding: 0.25rem 0.5rem;
|
|
40
40
|
border-radius: 0.25rem;
|
|
41
|
-
color: var(--color-
|
|
41
|
+
color: var(--color-code-muted);
|
|
42
42
|
background: transparent;
|
|
43
43
|
transition:
|
|
44
44
|
color 0.15s ease,
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
/* Code block colors */
|
|
29
29
|
--color-code-bg: #f3f4f6;
|
|
30
30
|
--color-code-text: #1f2937;
|
|
31
|
+
/* Secondary text inside a code block. Darker than --color-text-muted, which
|
|
32
|
+
reaches only 4.39:1 against the code background and so falls short of the
|
|
33
|
+
4.5:1 that the small type used for the language label and the copy button
|
|
34
|
+
needs. */
|
|
35
|
+
--color-code-muted: #4b5563;
|
|
31
36
|
|
|
32
37
|
/* Border colors */
|
|
33
38
|
--color-border: #e5e7eb;
|
|
@@ -59,6 +64,8 @@
|
|
|
59
64
|
/* Code block colors */
|
|
60
65
|
--color-code-bg: #374151;
|
|
61
66
|
--color-code-text: #f9fafb;
|
|
67
|
+
/* See the light theme: the muted grey reaches only 4.06:1 here. */
|
|
68
|
+
--color-code-muted: #d1d5db;
|
|
62
69
|
|
|
63
70
|
/* Border colors */
|
|
64
71
|
--color-border: #374151;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
3
|
+
"headers": [
|
|
4
|
+
{
|
|
5
|
+
"source": "/fonts/(.*)",
|
|
6
|
+
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"source": "/images/(.*)",
|
|
10
|
+
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"source": "/(search-index.json|favicon.svg|og-image.svg)",
|
|
14
|
+
"headers": [
|
|
15
|
+
{
|
|
16
|
+
"key": "Cache-Control",
|
|
17
|
+
"value": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"source": "/(.*)",
|
|
23
|
+
"headers": [
|
|
24
|
+
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
|
25
|
+
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
|
|
26
|
+
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
|
|
27
|
+
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|