@myelophone/nuxt 0.18.2 → 0.20.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/.devcontainer/Dockerfile +6 -6
- package/.editorconfig +43 -43
- package/.github/workflows/package.yml +72 -73
- package/.nuxtignore +5 -5
- package/.releaserc.js +1 -0
- package/CHANGELOG.md +159 -141
- package/LICENSE +77 -77
- package/README.md +7 -7
- package/app/app.vue +1 -1
- package/app/components/view/NotFound.vue +2 -2
- package/app/composables/useMultiLang.ts +8 -0
- package/app/locales/en/error.json +4 -4
- package/app/locales/en/outdated.json +5 -5
- package/app/locales/en/times.json +18 -18
- package/app/locales/pl/times.json +18 -18
- package/app/locales/ru/error.json +4 -4
- package/app/locales/ru/outdated.json +5 -5
- package/app/locales/ru/times.json +18 -18
- package/app/middleware/blog-layout.ts +3 -1
- package/app/modules/default-blog/index.ts +1 -1
- package/app/modules/htaccess/.htaccess +38 -38
- package/app/modules/myelophone-config/module.ts +56 -1
- package/app/pages/post/[...slug].server.vue +32 -0
- package/app/plugins/open-graph-only.ts +36 -0
- package/app/plugins/url-helper.client.ts +0 -1
- package/app/server/middleware/security-headers.ts +5 -0
- package/app/server/middleware/url-helper.ts +7 -0
- package/app/types/blog-content.d.ts +11 -0
- package/app/utils/blog.server.ts +3 -129
- package/biome.json +1 -1
- package/nuxt.config.ts +55 -24
- package/package.json +13 -10
- package/vite/plugins/blog-content.ts +126 -0
- package/vite/plugins/clean-css.ts +20 -1
- package/vite/plugins/nitro-profile.ts +86 -0
- package/app/components/ui/PostContent.server.vue +0 -23
- package/app/pages/post/[...slug].vue +0 -30
|
@@ -112,6 +112,11 @@ export default defineEventHandler((event) => {
|
|
|
112
112
|
|
|
113
113
|
for (const [rawKey, rawValue] of Object.entries(rules.headers)) {
|
|
114
114
|
const key = rawKey as SecurityHeaderKey;
|
|
115
|
+
|
|
116
|
+
if (key === 'contentSecurityPolicy') {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
const headerName =
|
|
116
121
|
key === 'contentSecurityPolicy' &&
|
|
117
122
|
rules.contentSecurityPolicyReportOnly
|
|
@@ -9,6 +9,13 @@ export default defineEventHandler((event) => {
|
|
|
9
9
|
`http://${event.node.req.headers.host}`,
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
+
if (
|
|
13
|
+
url.pathname.startsWith('/_nuxt/') ||
|
|
14
|
+
url.pathname.startsWith('/__nuxt_')
|
|
15
|
+
) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
const sanitize = (path: string) => {
|
|
13
20
|
let p = path
|
|
14
21
|
.replace(/[<>{}|\\^`]/g, '')
|
package/app/utils/blog.server.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import MarkdownIt from 'markdown-it';
|
|
1
|
+
import { posts as generatedPosts } from 'virtual:myelophone-blog-content';
|
|
3
2
|
|
|
4
3
|
export interface PostMeta {
|
|
5
4
|
title?: string;
|
|
@@ -8,135 +7,10 @@ export interface PostMeta {
|
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export interface Post {
|
|
11
|
-
content: string;
|
|
12
10
|
meta: PostMeta;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type PostLoader = () => Promise<string>;
|
|
16
|
-
|
|
17
|
-
const postCache = new Map<string, Promise<Post>>();
|
|
18
|
-
const markdown = new MarkdownIt({
|
|
19
|
-
html: false,
|
|
20
|
-
linkify: true,
|
|
21
|
-
typographer: true,
|
|
22
|
-
});
|
|
23
|
-
const blockTagPattern =
|
|
24
|
-
/(<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b[^>]*>)\s+(?=<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b)/gi;
|
|
25
|
-
|
|
26
|
-
function normalizePosts(
|
|
27
|
-
items: Record<string, PostLoader>,
|
|
28
|
-
): Record<string, PostLoader> {
|
|
29
|
-
return Object.fromEntries(
|
|
30
|
-
Object.entries(items).flatMap(([path, loader]) => {
|
|
31
|
-
const normalized = path.replace(/\\/g, '/');
|
|
32
|
-
|
|
33
|
-
const marker = '/content/';
|
|
34
|
-
const index = normalized.lastIndexOf(marker);
|
|
35
|
-
if (index === -1) {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const slug = normalized
|
|
40
|
-
.slice(index + marker.length)
|
|
41
|
-
.replace(/\.md$/, '');
|
|
42
|
-
|
|
43
|
-
return [[slug, loader]];
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const postsAlias = import.meta.glob<string>('#myelophone/app/content/**/*.md', {
|
|
49
|
-
query: '?raw',
|
|
50
|
-
import: 'default',
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const postsApp = import.meta.glob<string>('~/content/**/*.md', {
|
|
54
|
-
query: '?raw',
|
|
55
|
-
import: 'default',
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const posts = {
|
|
59
|
-
...normalizePosts(postsAlias),
|
|
60
|
-
...normalizePosts(postsApp),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
function parseMarkdown(source: string): Post {
|
|
64
|
-
if (!source.startsWith('---')) {
|
|
65
|
-
return {
|
|
66
|
-
meta: {},
|
|
67
|
-
content: source,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
72
|
-
|
|
73
|
-
if (!match) {
|
|
74
|
-
return {
|
|
75
|
-
meta: {},
|
|
76
|
-
content: source,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
meta: parsePostMeta(parse(match[1] ?? '')),
|
|
82
|
-
content: source.slice(match[0].length),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function parsePostMeta(value: unknown): PostMeta {
|
|
87
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
88
|
-
return {};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const source = value as Record<string, unknown>;
|
|
92
|
-
const meta: PostMeta = {};
|
|
93
|
-
|
|
94
|
-
for (const key of ['title', 'description', 'image'] as const) {
|
|
95
|
-
if (typeof source[key] === 'string') {
|
|
96
|
-
meta[key] = source[key];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return meta;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function renderMarkdown(source: string): string {
|
|
104
|
-
return markdown
|
|
105
|
-
.render(source)
|
|
106
|
-
.split(/(<pre\b[\s\S]*?<\/pre>)/gi)
|
|
107
|
-
.map((fragment, index) => {
|
|
108
|
-
if (index % 2 === 1) {
|
|
109
|
-
return fragment;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return fragment
|
|
113
|
-
.replace(/\s*\r?\n\s*/g, ' ')
|
|
114
|
-
.replace(blockTagPattern, '$1')
|
|
115
|
-
.trim();
|
|
116
|
-
})
|
|
117
|
-
.join('');
|
|
11
|
+
html: string;
|
|
118
12
|
}
|
|
119
13
|
|
|
120
14
|
export async function getPost(slug: string): Promise<Post | null> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!loader) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const cached = postCache.get(slug);
|
|
128
|
-
|
|
129
|
-
if (cached) {
|
|
130
|
-
return cached;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const promise = loader().then(parseMarkdown);
|
|
134
|
-
postCache.set(slug, promise);
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
return await promise;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
postCache.delete(slug);
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
15
|
+
return generatedPosts[slug] ?? null;
|
|
142
16
|
}
|
package/biome.json
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -10,6 +10,10 @@ import defaultBlog from './app/modules/default-blog';
|
|
|
10
10
|
import defaultSitemap from './app/modules/default-sitemap';
|
|
11
11
|
|
|
12
12
|
import { cleanEmptyCssPlugin } from './vite/plugins/clean-css';
|
|
13
|
+
import {
|
|
14
|
+
blogContentPlugin,
|
|
15
|
+
defaultBlogContentDirectories,
|
|
16
|
+
} from './vite/plugins/blog-content';
|
|
13
17
|
import postcssViewportFallback from './vite/plugins/postcss-viewport-fallback';
|
|
14
18
|
import noImportant from 'postcss-no-important';
|
|
15
19
|
import postcssAddViewportUnits from './vite/plugins/postcss-add-viewportunits';
|
|
@@ -24,7 +28,7 @@ const pkg = JSON.parse(
|
|
|
24
28
|
|
|
25
29
|
const version = pkg.version;
|
|
26
30
|
|
|
27
|
-
const dateActuality = '2026-
|
|
31
|
+
const dateActuality = '2026-09-05';
|
|
28
32
|
const banner = `/* © 2025 Aliaksandr Ivanou (https://aleksivanov.me/). All rights reserved. @MyelophOne/Nuxt v${version}. This app bundle licenses: /_nuxt/licenses.md */\n`;
|
|
29
33
|
const defaultSeo = {
|
|
30
34
|
title: 'Our Nuxt WebSite | by MyelophOne/Nuxt',
|
|
@@ -131,6 +135,10 @@ export default defineNuxtConfig({
|
|
|
131
135
|
appConfig: {
|
|
132
136
|
defaultSeo,
|
|
133
137
|
},
|
|
138
|
+
seo: {
|
|
139
|
+
automaticTwitterTags: false,
|
|
140
|
+
automaticDefaults: false,
|
|
141
|
+
},
|
|
134
142
|
ogImage: {
|
|
135
143
|
enabled: process.env.NODE_ENV !== 'test',
|
|
136
144
|
zeroRuntime: true,
|
|
@@ -184,6 +192,12 @@ export default defineNuxtConfig({
|
|
|
184
192
|
},
|
|
185
193
|
plugins: [
|
|
186
194
|
tailwindcss(),
|
|
195
|
+
blogContentPlugin(
|
|
196
|
+
defaultBlogContentDirectories(
|
|
197
|
+
process.cwd(),
|
|
198
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
199
|
+
),
|
|
200
|
+
),
|
|
187
201
|
{
|
|
188
202
|
name: 'suppress-plugin-timings-warning',
|
|
189
203
|
configResolved(config) {
|
|
@@ -324,7 +338,7 @@ export default defineNuxtConfig({
|
|
|
324
338
|
throwError: true,
|
|
325
339
|
},
|
|
326
340
|
rateLimiter: {
|
|
327
|
-
tokensPerInterval:
|
|
341
|
+
tokensPerInterval: 300,
|
|
328
342
|
interval: 300000,
|
|
329
343
|
headers: true,
|
|
330
344
|
driver: {
|
|
@@ -406,9 +420,6 @@ export default defineNuxtConfig({
|
|
|
406
420
|
},
|
|
407
421
|
'/_ipx/**': { swr: false },
|
|
408
422
|
},
|
|
409
|
-
seo: {
|
|
410
|
-
automaticDefaults: false,
|
|
411
|
-
},
|
|
412
423
|
multi18n: {
|
|
413
424
|
locales: ['en'],
|
|
414
425
|
defaultLocale: 'en',
|
|
@@ -422,29 +433,49 @@ export default defineNuxtConfig({
|
|
|
422
433
|
},
|
|
423
434
|
hooks: {
|
|
424
435
|
'nitro:config'(nitroConfig) {
|
|
425
|
-
if (process.
|
|
426
|
-
|
|
436
|
+
if (process.env.NUXT_PROFILE === '1') {
|
|
437
|
+
nitroConfig.plugins ||= [];
|
|
438
|
+
nitroConfig.plugins.push(
|
|
439
|
+
resolve('./vite/plugins/nitro-profile'),
|
|
440
|
+
);
|
|
427
441
|
}
|
|
428
442
|
|
|
429
443
|
nitroConfig.rollupConfig ||= {};
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
444
|
+
const plugins = [
|
|
445
|
+
blogContentPlugin(
|
|
446
|
+
defaultBlogContentDirectories(
|
|
447
|
+
process.cwd(),
|
|
448
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
if (process.platform === 'win32') {
|
|
454
|
+
plugins.push({
|
|
455
|
+
name: 'resolve-nuxt-cache-driver-file-url',
|
|
456
|
+
resolveId(source: string) {
|
|
457
|
+
if (
|
|
458
|
+
source.startsWith('file:') &&
|
|
459
|
+
source.endsWith('/runtime/utils/cache-driver.mjs')
|
|
460
|
+
) {
|
|
461
|
+
return fileURLToPath(source);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
442
466
|
|
|
443
|
-
nitroConfig.rollupConfig.plugins
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
467
|
+
const currentPlugins = nitroConfig.rollupConfig.plugins;
|
|
468
|
+
const mergedPlugins = [
|
|
469
|
+
...(Array.isArray(currentPlugins)
|
|
470
|
+
? currentPlugins
|
|
471
|
+
: currentPlugins
|
|
472
|
+
? [currentPlugins]
|
|
473
|
+
: []),
|
|
474
|
+
...plugins,
|
|
475
|
+
];
|
|
476
|
+
nitroConfig.rollupConfig.plugins = mergedPlugins as NonNullable<
|
|
477
|
+
typeof nitroConfig.rollupConfig.plugins
|
|
478
|
+
>;
|
|
448
479
|
},
|
|
449
480
|
'build:manifest': (manifest) => {
|
|
450
481
|
for (const key in manifest) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myelophone/nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Aliaksandr Ivanou <aleksivanov.me@gmail.com> (https://github.com/aleksivanou)",
|
|
6
6
|
"maintainers": [
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"generate": "NODE_OPTIONS='--no-webstorage' NUXT_TELEMETRY_DEBUG=1 NODE_ENV=production NUXT_STATIC=true NUXT_PLAYGROUND=true nuxt generate playground",
|
|
18
18
|
"preview": "NODE_OPTIONS='--no-webstorage' NUXT_TELEMETRY_DEBUG=1 NUXT_PLAYGROUND=true nuxt preview playground",
|
|
19
19
|
"server": "node --no-deprecation --no-warnings --no-webstorage playground/.output/server/index.mjs",
|
|
20
|
+
"static": "npx serve playground/.output/public",
|
|
21
|
+
"profile": "NODE_OPTIONS='--no-deprecation --no-webstorage' NUXT_TELEMETRY_DEBUG=1 NODE_ENV=production NUXT_PLAYGROUND=true NUXT_PROFILE=1 nuxt build playground",
|
|
20
22
|
"prepare": "nuxt prepare && nuxt prepare playground",
|
|
21
23
|
"analyze": "NODE_OPTIONS='--no-deprecation --no-webstorage' yarn nuxi analyze playground",
|
|
22
24
|
"test": "yarn test:types && yarn playwright",
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
"lock-update": "yarn dedupe --strategy highest"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@biomejs/biome": "^2.5.
|
|
39
|
+
"@biomejs/biome": "^2.5.12",
|
|
38
40
|
"@commitlint/cli": "^21.2.2",
|
|
39
41
|
"@commitlint/config-conventional": "^21.2.2",
|
|
40
42
|
"@nuxt/fonts": "^0.14.0",
|
|
@@ -44,8 +46,8 @@
|
|
|
44
46
|
"@nuxt/types": "^2.18.1",
|
|
45
47
|
"@nuxtjs/fontaine": "^0.5.0",
|
|
46
48
|
"@nuxtjs/robots": "^6.2.0",
|
|
47
|
-
"@pinia/colada": "^1.4.
|
|
48
|
-
"@pinia/colada-nuxt": "^1.0
|
|
49
|
+
"@pinia/colada": "^1.4.4",
|
|
50
|
+
"@pinia/colada-nuxt": "^1.1.0",
|
|
49
51
|
"@pinia/nuxt": "^1.0.2",
|
|
50
52
|
"@playwright/test": "^1.62.1",
|
|
51
53
|
"@semantic-release/changelog": "^7.0.0",
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"@semantic-release/npm": "^13.1.5",
|
|
56
58
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
57
59
|
"@tailwindcss/vite": "^4.3.3",
|
|
58
|
-
"@types/node": "^26.4.
|
|
60
|
+
"@types/node": "^26.4.1",
|
|
59
61
|
"@unhead/vue": "^3.4.0",
|
|
60
62
|
"@vue/devtools-api": "^8.2.1",
|
|
61
63
|
"@vueuse/core": "^14.4.0",
|
|
@@ -68,13 +70,13 @@
|
|
|
68
70
|
"nuxt-og-image": "^6.7.8",
|
|
69
71
|
"nuxt-payload-analyzer": "^0.1.4",
|
|
70
72
|
"nuxt-security": "^2.6.0",
|
|
71
|
-
"nuxt-seo-utils": "^8.
|
|
73
|
+
"nuxt-seo-utils": "^8.5.0",
|
|
72
74
|
"nuxt-vitalizer": "^3.0.0",
|
|
73
75
|
"pinia": "^4.0.3",
|
|
74
|
-
"postcss": "^8.5.
|
|
76
|
+
"postcss": "^8.5.27",
|
|
75
77
|
"postcss-no-important": "^11.0.0",
|
|
76
78
|
"purgecss": "^8.0.0",
|
|
77
|
-
"rolldown": "^1.2.
|
|
79
|
+
"rolldown": "^1.2.7",
|
|
78
80
|
"semantic-release": "^25.0.9",
|
|
79
81
|
"tailwindcss": "^4.3.3",
|
|
80
82
|
"typescript": "^7.0.2",
|
|
@@ -90,10 +92,11 @@
|
|
|
90
92
|
"packageManager": "yarn@4.18.0",
|
|
91
93
|
"repository": {
|
|
92
94
|
"type": "git",
|
|
93
|
-
"url": "git+https://github.com/
|
|
95
|
+
"url": "git+https://github.com/MyelophOne/nuxt.git"
|
|
94
96
|
},
|
|
95
97
|
"publishConfig": {
|
|
96
|
-
"access": "public"
|
|
98
|
+
"access": "public",
|
|
99
|
+
"provenance": true
|
|
97
100
|
},
|
|
98
101
|
"dependencies": {
|
|
99
102
|
"fuse.js": "^7.5.0"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
3
|
+
import type { Plugin } from 'vite';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import MarkdownIt, { type MarkdownIt as MarkdownItInstance } from 'markdown-it';
|
|
6
|
+
|
|
7
|
+
const VIRTUAL_MODULE_ID = 'virtual:myelophone-blog-content';
|
|
8
|
+
const RESOLVED_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
9
|
+
const blockTagPattern =
|
|
10
|
+
/(<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b[^>]*>)\s+(?=<\/?(?:address|article|aside|blockquote|div|dl|dt|dd|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|tfoot|th|thead|tr|ul)\b)/gi;
|
|
11
|
+
|
|
12
|
+
interface GeneratedPost {
|
|
13
|
+
meta: Record<string, string>;
|
|
14
|
+
html: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function collectMarkdownFiles(directory: string): string[] {
|
|
18
|
+
try {
|
|
19
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap(
|
|
20
|
+
(entry) => {
|
|
21
|
+
const file = join(directory, entry.name);
|
|
22
|
+
return entry.isDirectory()
|
|
23
|
+
? collectMarkdownFiles(file)
|
|
24
|
+
: extname(file).toLowerCase() === '.md'
|
|
25
|
+
? [file]
|
|
26
|
+
: [];
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderMarkdown(source: string, markdown: MarkdownItInstance): string {
|
|
35
|
+
return markdown
|
|
36
|
+
.render(source)
|
|
37
|
+
.split(/(<pre\b[\s\S]*?<\/pre>)/gi)
|
|
38
|
+
.map((fragment: string, index: number) => {
|
|
39
|
+
if (index % 2 === 1) {
|
|
40
|
+
return fragment;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return fragment
|
|
44
|
+
.replace(/\s*\r?\n\s*/g, ' ')
|
|
45
|
+
.replace(blockTagPattern, '$1')
|
|
46
|
+
.trim();
|
|
47
|
+
})
|
|
48
|
+
.join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parsePost(
|
|
52
|
+
source: string,
|
|
53
|
+
markdown: MarkdownItInstance,
|
|
54
|
+
): GeneratedPost {
|
|
55
|
+
let meta: Record<string, string> = {};
|
|
56
|
+
let content = source;
|
|
57
|
+
|
|
58
|
+
if (source.startsWith('---')) {
|
|
59
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
60
|
+
|
|
61
|
+
if (match) {
|
|
62
|
+
const parsed = parse(match[1] ?? '');
|
|
63
|
+
if (
|
|
64
|
+
parsed &&
|
|
65
|
+
typeof parsed === 'object' &&
|
|
66
|
+
!Array.isArray(parsed)
|
|
67
|
+
) {
|
|
68
|
+
for (const key of ['title', 'description', 'image']) {
|
|
69
|
+
const value = (parsed as Record<string, unknown>)[key];
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
meta[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
content = source.slice(match[0].length);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { meta, html: renderMarkdown(content, markdown) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generatePosts(directories: string[]): Record<string, GeneratedPost> {
|
|
83
|
+
const markdown = new MarkdownIt({
|
|
84
|
+
html: false,
|
|
85
|
+
linkify: true,
|
|
86
|
+
typographer: true,
|
|
87
|
+
});
|
|
88
|
+
const posts: Record<string, GeneratedPost> = {};
|
|
89
|
+
|
|
90
|
+
for (const directory of directories) {
|
|
91
|
+
for (const file of collectMarkdownFiles(directory)) {
|
|
92
|
+
const slug = relative(directory, file)
|
|
93
|
+
.replace(/\\/g, '/')
|
|
94
|
+
.replace(/\.md$/i, '');
|
|
95
|
+
posts[slug] = parsePost(readFileSync(file, 'utf8'), markdown);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return posts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function blogContentPlugin(directories: string[]): Plugin {
|
|
103
|
+
return {
|
|
104
|
+
name: 'myelophone-blog-content',
|
|
105
|
+
resolveId(id) {
|
|
106
|
+
return id === VIRTUAL_MODULE_ID ? RESOLVED_MODULE_ID : undefined;
|
|
107
|
+
},
|
|
108
|
+
load(id) {
|
|
109
|
+
if (id !== RESOLVED_MODULE_ID) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `export const posts = ${JSON.stringify(generatePosts(directories))};`;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const defaultBlogContentDirectories = (
|
|
119
|
+
projectRoot: string,
|
|
120
|
+
packageRoot = projectRoot,
|
|
121
|
+
) => [
|
|
122
|
+
resolve(packageRoot, 'app/content'),
|
|
123
|
+
resolve(projectRoot, 'app/content'),
|
|
124
|
+
resolve(projectRoot, 'content'),
|
|
125
|
+
resolve(projectRoot, 'playground/content'),
|
|
126
|
+
];
|
|
@@ -10,6 +10,21 @@ interface CleanCssPluginOptions {
|
|
|
10
10
|
purgeTailwindUtilities?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function formatBytes(bytes: number): string {
|
|
14
|
+
if (bytes === 0) {
|
|
15
|
+
return '0 B';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
19
|
+
const unitIndex = Math.min(
|
|
20
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
21
|
+
units.length - 1,
|
|
22
|
+
);
|
|
23
|
+
const value = bytes / 1024 ** unitIndex;
|
|
24
|
+
|
|
25
|
+
return `${parseFloat(value.toFixed(2))} ${units[unitIndex]}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
export function cleanEmptyCssPlugin(
|
|
14
29
|
options: CleanCssPluginOptions = {},
|
|
15
30
|
): Plugin {
|
|
@@ -175,12 +190,16 @@ export function cleanEmptyCssPlugin(
|
|
|
175
190
|
}
|
|
176
191
|
|
|
177
192
|
await writeFile(file, css);
|
|
193
|
+
const sizeAfter = Buffer.byteLength(css, 'utf8');
|
|
178
194
|
reports.push(
|
|
179
|
-
`.../${basename(file)} (${sizeBefore}
|
|
195
|
+
`.../${basename(file)} (${formatBytes(sizeBefore)} → ${formatBytes(sizeAfter)})`,
|
|
180
196
|
);
|
|
181
197
|
}
|
|
182
198
|
},
|
|
183
199
|
closeBundle() {
|
|
200
|
+
if (this.environment?.name !== 'client') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
184
203
|
logger.info('Purging css...');
|
|
185
204
|
for (const report of reports) {
|
|
186
205
|
logger.info(report);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface TimingState {
|
|
2
|
+
requestStart: number;
|
|
3
|
+
renderStart?: number;
|
|
4
|
+
renderEnd?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
8
|
+
const timings = new WeakMap<object, TimingState>();
|
|
9
|
+
|
|
10
|
+
const getTiming = (event: { context: object }) =>
|
|
11
|
+
timings.get(event.context);
|
|
12
|
+
|
|
13
|
+
const writeTiming = (event: any) => {
|
|
14
|
+
const timing = getTiming(event);
|
|
15
|
+
if (!timing) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const now = performance.now();
|
|
20
|
+
const serverTiming: string[] = [];
|
|
21
|
+
|
|
22
|
+
if (timing.renderStart) {
|
|
23
|
+
serverTiming.push(
|
|
24
|
+
`nuxt-pre-render;dur=${(timing.renderStart - timing.requestStart).toFixed(2)}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (timing.renderStart && timing.renderEnd) {
|
|
29
|
+
serverTiming.push(
|
|
30
|
+
`nuxt-render;dur=${(timing.renderEnd - timing.renderStart).toFixed(2)}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (timing.renderEnd) {
|
|
35
|
+
serverTiming.push(
|
|
36
|
+
`nuxt-post-render;dur=${(now - timing.renderEnd).toFixed(2)}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cache = event.context.cache;
|
|
41
|
+
if (cache) {
|
|
42
|
+
serverTiming.push(
|
|
43
|
+
`nuxt-cache;desc="${timing.renderStart ? 'miss' : 'hit'}"`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
serverTiming.push(
|
|
48
|
+
`nuxt-total;dur=${(now - timing.requestStart).toFixed(2)}`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
setResponseHeader(event, 'Server-Timing', serverTiming.join(', '));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
55
|
+
timings.set(event.context, { requestStart: performance.now() });
|
|
56
|
+
|
|
57
|
+
const response = event.node?.res;
|
|
58
|
+
if (!response) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const end = response.end.bind(response);
|
|
63
|
+
response.end = ((...args: Parameters<typeof response.end>) => {
|
|
64
|
+
writeTiming(event);
|
|
65
|
+
return end(...args);
|
|
66
|
+
}) as typeof response.end;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
nitroApp.hooks.hook('render:before', ({ event }) => {
|
|
70
|
+
const timing = getTiming(event);
|
|
71
|
+
if (timing) {
|
|
72
|
+
timing.renderStart = performance.now();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
nitroApp.hooks.hook('render:html', (_html, { event }) => {
|
|
77
|
+
const timing = getTiming(event);
|
|
78
|
+
if (timing) {
|
|
79
|
+
timing.renderEnd = performance.now();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
nitroApp.hooks.hook('beforeResponse', (event) => {
|
|
84
|
+
writeTiming(event);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { getPost, renderMarkdown } from "#myelophone/app/utils/blog.server";
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{
|
|
5
|
-
slug: string;
|
|
6
|
-
}>();
|
|
7
|
-
|
|
8
|
-
const post = await getPost(props.slug);
|
|
9
|
-
const html = post ? renderMarkdown(post.content) : "";
|
|
10
|
-
|
|
11
|
-
if (post) {
|
|
12
|
-
useAppSeo({
|
|
13
|
-
title: post.meta.title,
|
|
14
|
-
description: post.meta.description,
|
|
15
|
-
image: post.meta.image,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<ViewNotFound v-if="!post" :set-response-status="false" />
|
|
22
|
-
<article v-else class="post-content" v-html="html" />
|
|
23
|
-
</template>
|