@myelophone/nuxt 0.19.1 → 0.20.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/CHANGELOG.md +11 -0
- 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/middleware/blog-layout.ts +3 -1
- package/app/modules/default-blog/index.ts +1 -1
- 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 +27 -25
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 📦 Release 0.20.1 (2026-09-23)
|
|
2
|
+
### 🐛 Bug Fixes
|
|
3
|
+
- update dependencies
|
|
4
|
+
|
|
5
|
+
## 📦 Release 0.20.0 (2026-09-04)
|
|
6
|
+
### ✨ Features
|
|
7
|
+
- add ssr stream
|
|
8
|
+
|
|
9
|
+
### 📚 Documentation
|
|
10
|
+
- update readme
|
|
11
|
+
|
|
1
12
|
## 📦 Release 0.19.1 (2026-08-30)
|
|
2
13
|
### 🐛 Bug Fixes
|
|
3
14
|
- update github actions
|
package/README.md
CHANGED
|
@@ -100,17 +100,17 @@ export default defineNuxtConfig({
|
|
|
100
100
|
|
|
101
101
|
### Extend it as a Nuxt layer
|
|
102
102
|
|
|
103
|
-
The layer
|
|
103
|
+
The layer is published on [npm](https://www.npmjs.com/package/@myelophone/nuxt). Add it to the consuming application's `package.json`:
|
|
104
104
|
|
|
105
105
|
```json
|
|
106
106
|
{
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@myelophone/nuxt": "
|
|
108
|
+
"@myelophone/nuxt": "^0.19.1"
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Install dependencies and prepare Nuxt types:
|
|
113
|
+
Install dependencies from npm and prepare Nuxt types:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
yarn install
|
|
@@ -542,7 +542,7 @@ const props = withDefaults(
|
|
|
542
542
|
|
|
543
543
|
const english: ProductPageContent = {
|
|
544
544
|
seo: {
|
|
545
|
-
title: "Acme
|
|
545
|
+
title: "Acme - a clearer way to work",
|
|
546
546
|
description: "A short description of the complete product page.",
|
|
547
547
|
},
|
|
548
548
|
brand: "Acme",
|
|
@@ -558,7 +558,7 @@ const defaultContentByLocale: Record<string, ProductPageContent> = {
|
|
|
558
558
|
en: english,
|
|
559
559
|
pl: mergeReadyContent(english, {
|
|
560
560
|
seo: {
|
|
561
|
-
title: "Acme
|
|
561
|
+
title: "Acme - prostszy sposób pracy",
|
|
562
562
|
description: "Krótki opis kompletnej strony produktu.",
|
|
563
563
|
},
|
|
564
564
|
hero: {
|
|
@@ -995,7 +995,7 @@ Use `SeoProfileInfo` only when the page primarily describes one person or organi
|
|
|
995
995
|
<SeoProfileInfo
|
|
996
996
|
id="https://example.com/team/ada#profile-page"
|
|
997
997
|
url="https://example.com/team/ada"
|
|
998
|
-
name="Ada Lovelace
|
|
998
|
+
name="Ada Lovelace - profile"
|
|
999
999
|
date-created="2024-01-10T09:00:00Z"
|
|
1000
1000
|
date-modified="2026-08-16T12:00:00Z"
|
|
1001
1001
|
entity-id="https://example.com/team/ada#person"
|
|
@@ -2977,7 +2977,7 @@ Commits follow Conventional Commits and are checked with Commitlint. The `main`
|
|
|
2977
2977
|
- `BREAKING CHANGE:` → major release;
|
|
2978
2978
|
- documentation, tests, refactors, build, CI, style, and chores do not release by default.
|
|
2979
2979
|
|
|
2980
|
-
The release workflow updates `CHANGELOG.md` and `package.json`, publishes to
|
|
2980
|
+
The release workflow updates `CHANGELOG.md` and `package.json`, publishes to [npm](https://www.npmjs.com/package/@myelophone/nuxt) with provenance, creates a GitHub release, and commits release metadata back to the repository.
|
|
2981
2981
|
|
|
2982
2982
|
### Compatibility checklist
|
|
2983
2983
|
|
package/app/app.vue
CHANGED
|
@@ -354,7 +354,7 @@ useCommand({
|
|
|
354
354
|
<NuxtLayout>
|
|
355
355
|
<NuxtErrorBoundary>
|
|
356
356
|
<main id="main" tabindex="-1">
|
|
357
|
-
<NuxtPage
|
|
357
|
+
<NuxtPage />
|
|
358
358
|
</main>
|
|
359
359
|
<template #error="{ error, clearError }">
|
|
360
360
|
<SeoNoIndex :status-code="error?.statusCode" />
|
|
@@ -59,8 +59,8 @@ onUnmounted(deactivateNotFound);
|
|
|
59
59
|
const { t } = useMultiLang(["interface"]);
|
|
60
60
|
|
|
61
61
|
useAppSeo({
|
|
62
|
-
title: () => `404
|
|
63
|
-
description: () => `404
|
|
62
|
+
title: () => `404 - ${t("interface.pageNotFound")}`,
|
|
63
|
+
description: () => `404 - ${t("interface.pageNotFoundDescription")}`,
|
|
64
64
|
noIndex: true,
|
|
65
65
|
});
|
|
66
66
|
</script>
|
|
@@ -174,6 +174,14 @@ export function useMultiLang(namespaces: string | string[]) {
|
|
|
174
174
|
{ watch: [activeLang] },
|
|
175
175
|
);
|
|
176
176
|
|
|
177
|
+
if (import.meta.client) {
|
|
178
|
+
onMounted(() => {
|
|
179
|
+
void Promise.all(
|
|
180
|
+
nsArray.map((ns) => loadNamespace(activeLang.value, ns)),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
const t = (
|
|
178
186
|
key: string,
|
|
179
187
|
arg1?: InterpolationParams | number,
|
|
@@ -21,7 +21,9 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
21
21
|
if (!post) {
|
|
22
22
|
const event = useRequestEvent();
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const isIslandRequest = Boolean(useNuxtApp().ssrContext?.islandContext);
|
|
25
|
+
|
|
26
|
+
if (event && !isIslandRequest) {
|
|
25
27
|
setResponseStatus(event, 404, 'Not Found');
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -20,7 +20,7 @@ function processPages(pages: NuxtPage[], blogEnabled: boolean) {
|
|
|
20
20
|
for (let i = pages.length - 1; i >= 0; i--) {
|
|
21
21
|
const page = pages[i]!;
|
|
22
22
|
|
|
23
|
-
if (page.file?.endsWith('/post/[...slug].vue')) {
|
|
23
|
+
if (page.file?.endsWith('/post/[...slug].server.vue')) {
|
|
24
24
|
if (!blogEnabled) {
|
|
25
25
|
pages.splice(i, 1);
|
|
26
26
|
continue;
|
|
@@ -33,6 +33,52 @@ const enableDefaultRouteStreaming = (routeRules: Record<string, any>) => {
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const disablePostRouteStreaming = (
|
|
37
|
+
routeRules: Record<string, any>,
|
|
38
|
+
locales: string[],
|
|
39
|
+
) => {
|
|
40
|
+
const postRoutes = new Set([
|
|
41
|
+
'/post/**',
|
|
42
|
+
...locales.map((locale) => `/${locale}/post/**`),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
for (const route of postRoutes) {
|
|
46
|
+
routeRules[route] = {
|
|
47
|
+
...(routeRules[route] && typeof routeRules[route] === 'object'
|
|
48
|
+
? routeRules[route]
|
|
49
|
+
: {}),
|
|
50
|
+
streaming: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const addSWRPayloadRoutes = (routeRules: Record<string, any>) => {
|
|
56
|
+
for (const [route, rule] of Object.entries(routeRules)) {
|
|
57
|
+
if (!rule || typeof rule !== 'object' || rule.swr === false) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ('swr' in rule) {
|
|
62
|
+
const payloadRoute = `${route === '/' ? '' : route.replace(/\/$/, '')}/_payload.json`;
|
|
63
|
+
const current = routeRules[payloadRoute];
|
|
64
|
+
const cache =
|
|
65
|
+
rule.cache && typeof rule.cache === 'object'
|
|
66
|
+
? rule.cache
|
|
67
|
+
: {
|
|
68
|
+
swr: true,
|
|
69
|
+
...(typeof rule.swr === 'number'
|
|
70
|
+
? { maxAge: rule.swr }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
routeRules[payloadRoute] = {
|
|
75
|
+
...(current && typeof current === 'object' ? current : {}),
|
|
76
|
+
cache,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
36
82
|
const defaultCookieScripts = {
|
|
37
83
|
necessary: [],
|
|
38
84
|
analytics: [],
|
|
@@ -179,6 +225,10 @@ export default defineNuxtModule<MyelophoneConfig>({
|
|
|
179
225
|
if (isStreamingSSR) {
|
|
180
226
|
nuxt.options.routeRules ||= {};
|
|
181
227
|
enableDefaultRouteStreaming(nuxt.options.routeRules);
|
|
228
|
+
disablePostRouteStreaming(
|
|
229
|
+
nuxt.options.routeRules,
|
|
230
|
+
myelophone.multi18n.locales,
|
|
231
|
+
);
|
|
182
232
|
}
|
|
183
233
|
|
|
184
234
|
nuxt.hook('build:before', () => {
|
|
@@ -193,10 +243,15 @@ export default defineNuxtModule<MyelophoneConfig>({
|
|
|
193
243
|
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
194
244
|
nitroConfig.runtimeConfig ||= {};
|
|
195
245
|
nitroConfig.runtimeConfig.myelophone = myelophone;
|
|
246
|
+
nitroConfig.routeRules ||= {};
|
|
247
|
+
addSWRPayloadRoutes(nitroConfig.routeRules);
|
|
196
248
|
|
|
197
249
|
if (isStreamingSSR) {
|
|
198
|
-
nitroConfig.routeRules ||= {};
|
|
199
250
|
enableDefaultRouteStreaming(nitroConfig.routeRules);
|
|
251
|
+
disablePostRouteStreaming(
|
|
252
|
+
nitroConfig.routeRules,
|
|
253
|
+
myelophone.multi18n.locales,
|
|
254
|
+
);
|
|
200
255
|
}
|
|
201
256
|
});
|
|
202
257
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { getPost } from "#myelophone/app/utils/blog.server";
|
|
3
|
+
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
const param = "slug" in route.params ? route.params.slug : undefined;
|
|
6
|
+
const slug = Array.isArray(param) ? param.join("/") : (param ?? "");
|
|
7
|
+
const post = await getPost(slug);
|
|
8
|
+
|
|
9
|
+
if (!post) {
|
|
10
|
+
const event = useRequestEvent();
|
|
11
|
+
const isIslandRequest = Boolean(useNuxtApp().ssrContext?.islandContext);
|
|
12
|
+
|
|
13
|
+
if (event && !isIslandRequest) {
|
|
14
|
+
setResponseStatus(event, 404, "Not Found");
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
useAppSeo({
|
|
18
|
+
title: post.meta.title,
|
|
19
|
+
description: post.meta.description,
|
|
20
|
+
image: post.meta.image,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const html = post?.html ?? "";
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="post-content-page">
|
|
29
|
+
<ViewNotFound v-if="!post" :set-response-status="false" />
|
|
30
|
+
<article v-else class="post-content" v-html="html" />
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default defineNuxtPlugin(() => {
|
|
2
|
+
const head = injectHead();
|
|
3
|
+
|
|
4
|
+
if (!head) return;
|
|
5
|
+
const hooks = head.hooks;
|
|
6
|
+
|
|
7
|
+
if (!hooks) return;
|
|
8
|
+
|
|
9
|
+
hooks.hook('entries:normalize', ({ tags }) => {
|
|
10
|
+
for (let index = tags.length - 1; index >= 0; index--) {
|
|
11
|
+
const tag = tags[index];
|
|
12
|
+
if (!tag) continue;
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
tag.tag === 'meta' &&
|
|
16
|
+
tag.props.name?.toLowerCase() === 'twitter:card'
|
|
17
|
+
) {
|
|
18
|
+
tags.splice(index, 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
hooks.hook('tags:beforeResolve', ({ tags }) => {
|
|
24
|
+
for (let index = tags.length - 1; index >= 0; index--) {
|
|
25
|
+
const tag = tags[index];
|
|
26
|
+
if (!tag) continue;
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
tag.tag === 'meta' &&
|
|
30
|
+
tag.props.name?.toLowerCase() === 'twitter:card'
|
|
31
|
+
) {
|
|
32
|
+
tags.splice(index, 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -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-23';
|
|
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.1",
|
|
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,58 +36,58 @@
|
|
|
34
36
|
"lock-update": "yarn dedupe --strategy highest"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@biomejs/biome": "^2.5.
|
|
38
|
-
"@commitlint/cli": "^21.2.
|
|
39
|
-
"@commitlint/config-conventional": "^21.2.
|
|
39
|
+
"@biomejs/biome": "^2.5.14",
|
|
40
|
+
"@commitlint/cli": "^21.2.3",
|
|
41
|
+
"@commitlint/config-conventional": "^21.2.3",
|
|
40
42
|
"@nuxt/fonts": "^0.14.0",
|
|
41
43
|
"@nuxt/image": "^2.1.0",
|
|
42
44
|
"@nuxt/schema": "^4.5.2",
|
|
43
|
-
"@nuxt/scripts": "^1.3.
|
|
45
|
+
"@nuxt/scripts": "^1.3.9",
|
|
44
46
|
"@nuxt/types": "^2.18.1",
|
|
45
47
|
"@nuxtjs/fontaine": "^0.5.0",
|
|
46
|
-
"@nuxtjs/robots": "^6.2.
|
|
47
|
-
"@pinia/colada": "^1.4.
|
|
48
|
-
"@pinia/colada-nuxt": "^1.0
|
|
48
|
+
"@nuxtjs/robots": "^6.2.3",
|
|
49
|
+
"@pinia/colada": "^1.4.5",
|
|
50
|
+
"@pinia/colada-nuxt": "^1.2.0",
|
|
49
51
|
"@pinia/nuxt": "^1.0.2",
|
|
50
|
-
"@playwright/test": "^1.
|
|
52
|
+
"@playwright/test": "^1.63.0",
|
|
51
53
|
"@semantic-release/changelog": "^7.0.0",
|
|
52
54
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
53
55
|
"@semantic-release/git": "^11.0.1",
|
|
54
|
-
"@semantic-release/github": "^12.0.
|
|
55
|
-
"@semantic-release/npm": "^13.
|
|
56
|
+
"@semantic-release/github": "^12.0.10",
|
|
57
|
+
"@semantic-release/npm": "^13.2.0",
|
|
56
58
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
57
59
|
"@tailwindcss/vite": "^4.3.3",
|
|
58
|
-
"@types/node": "^26.
|
|
59
|
-
"@unhead/vue": "^3.4.
|
|
60
|
+
"@types/node": "^26.6.2",
|
|
61
|
+
"@unhead/vue": "^3.4.1",
|
|
60
62
|
"@vue/devtools-api": "^8.2.1",
|
|
61
|
-
"@vueuse/core": "^
|
|
62
|
-
"commitlint": "^21.2.
|
|
63
|
+
"@vueuse/core": "^15.0.0",
|
|
64
|
+
"commitlint": "^21.2.3",
|
|
63
65
|
"conventional-changelog-conventionalcommits": "^10.4.0",
|
|
64
|
-
"markdown-it": "^15.0.
|
|
66
|
+
"markdown-it": "^15.0.2",
|
|
65
67
|
"nuxt": "^4.5.2",
|
|
66
|
-
"nuxt-api-party": "^4.0.
|
|
68
|
+
"nuxt-api-party": "^4.0.1",
|
|
67
69
|
"nuxt-link-checker": "^5.3.0",
|
|
68
|
-
"nuxt-og-image": "^6.
|
|
70
|
+
"nuxt-og-image": "^6.8.0",
|
|
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.1",
|
|
72
74
|
"nuxt-vitalizer": "^3.0.0",
|
|
73
75
|
"pinia": "^4.0.3",
|
|
74
|
-
"postcss": "^8.5.
|
|
76
|
+
"postcss": "^8.5.28",
|
|
75
77
|
"postcss-no-important": "^11.0.0",
|
|
76
78
|
"purgecss": "^8.0.0",
|
|
77
|
-
"rolldown": "^1.2.
|
|
79
|
+
"rolldown": "^1.2.9",
|
|
78
80
|
"semantic-release": "^25.0.9",
|
|
79
81
|
"tailwindcss": "^4.3.3",
|
|
80
82
|
"typescript": "^7.0.2",
|
|
81
83
|
"unstorage": "^1.17.5",
|
|
82
|
-
"valibot": "^1.
|
|
83
|
-
"vite": "^8.
|
|
84
|
-
"vue": "^3.5.
|
|
84
|
+
"valibot": "^1.5.0",
|
|
85
|
+
"vite": "^8.3.0",
|
|
86
|
+
"vue": "^3.5.43",
|
|
85
87
|
"vue-easy-lightbox": "^1.19.0",
|
|
86
88
|
"vue-sonner": "^2.0.9",
|
|
87
89
|
"vue-tsc": "^3.3.11",
|
|
88
|
-
"yaml": "^2.9.
|
|
90
|
+
"yaml": "^2.9.1"
|
|
89
91
|
},
|
|
90
92
|
"packageManager": "yarn@4.18.0",
|
|
91
93
|
"repository": {
|
|
@@ -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>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const route = useRoute();
|
|
3
|
-
|
|
4
|
-
const param = "slug" in route.params ? route.params.slug : undefined;
|
|
5
|
-
|
|
6
|
-
const slug = Array.isArray(param) ? param.join("/") : (param ?? "");
|
|
7
|
-
|
|
8
|
-
if (import.meta.server) {
|
|
9
|
-
const { getPost } = await import("#myelophone/app/utils/blog.server");
|
|
10
|
-
const post = await getPost(slug);
|
|
11
|
-
|
|
12
|
-
if (!post) {
|
|
13
|
-
const event = useRequestEvent();
|
|
14
|
-
|
|
15
|
-
if (event) {
|
|
16
|
-
setResponseStatus(event, 404);
|
|
17
|
-
}
|
|
18
|
-
} else {
|
|
19
|
-
useAppSeo({
|
|
20
|
-
title: post.meta.title,
|
|
21
|
-
description: post.meta.description,
|
|
22
|
-
image: post.meta.image,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<template>
|
|
29
|
-
<UiPostContent :slug="slug" />
|
|
30
|
-
</template>
|