@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.
Files changed (37) hide show
  1. package/.devcontainer/Dockerfile +6 -6
  2. package/.editorconfig +43 -43
  3. package/.github/workflows/package.yml +72 -73
  4. package/.nuxtignore +5 -5
  5. package/.releaserc.js +1 -0
  6. package/CHANGELOG.md +159 -141
  7. package/LICENSE +77 -77
  8. package/README.md +7 -7
  9. package/app/app.vue +1 -1
  10. package/app/components/view/NotFound.vue +2 -2
  11. package/app/composables/useMultiLang.ts +8 -0
  12. package/app/locales/en/error.json +4 -4
  13. package/app/locales/en/outdated.json +5 -5
  14. package/app/locales/en/times.json +18 -18
  15. package/app/locales/pl/times.json +18 -18
  16. package/app/locales/ru/error.json +4 -4
  17. package/app/locales/ru/outdated.json +5 -5
  18. package/app/locales/ru/times.json +18 -18
  19. package/app/middleware/blog-layout.ts +3 -1
  20. package/app/modules/default-blog/index.ts +1 -1
  21. package/app/modules/htaccess/.htaccess +38 -38
  22. package/app/modules/myelophone-config/module.ts +56 -1
  23. package/app/pages/post/[...slug].server.vue +32 -0
  24. package/app/plugins/open-graph-only.ts +36 -0
  25. package/app/plugins/url-helper.client.ts +0 -1
  26. package/app/server/middleware/security-headers.ts +5 -0
  27. package/app/server/middleware/url-helper.ts +7 -0
  28. package/app/types/blog-content.d.ts +11 -0
  29. package/app/utils/blog.server.ts +3 -129
  30. package/biome.json +1 -1
  31. package/nuxt.config.ts +55 -24
  32. package/package.json +13 -10
  33. package/vite/plugins/blog-content.ts +126 -0
  34. package/vite/plugins/clean-css.ts +20 -1
  35. package/vite/plugins/nitro-profile.ts +86 -0
  36. package/app/components/ui/PostContent.server.vue +0 -23
  37. package/app/pages/post/[...slug].vue +0 -30
@@ -2,7 +2,6 @@ import { type Pinia } from 'pinia';
2
2
 
3
3
  export default defineNuxtPlugin((nuxtApp) => {
4
4
  const router = useRouter();
5
-
6
5
  const pinia = useNuxtApp().$pinia as Pinia;
7
6
  const settingsStore = useSettingsStore(pinia);
8
7
 
@@ -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, '')
@@ -0,0 +1,11 @@
1
+ declare module 'virtual:myelophone-blog-content' {
2
+ const posts: Record<
3
+ string,
4
+ {
5
+ meta: Record<string, string>;
6
+ html: string;
7
+ }
8
+ >;
9
+
10
+ export { posts };
11
+ }
@@ -1,5 +1,4 @@
1
- import { parse } from 'yaml';
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
- const loader = posts[slug];
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.11/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json",
3
3
  "assist": {
4
4
  "actions": {
5
5
  "source": {
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-08-31';
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: 150,
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.platform !== 'win32') {
426
- return;
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 resolver = {
431
- name: 'resolve-nuxt-cache-driver-file-url',
432
- resolveId(source: string) {
433
- if (
434
- source.startsWith('file:') &&
435
- source.endsWith('/runtime/utils/cache-driver.mjs')
436
- ) {
437
- return fileURLToPath(source);
438
- }
439
- },
440
- };
441
- const plugins = nitroConfig.rollupConfig.plugins;
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 = plugins
444
- ? Array.isArray(plugins)
445
- ? [...plugins, resolver]
446
- : [plugins, resolver]
447
- : [resolver];
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.18.2",
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.11",
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.2",
48
- "@pinia/colada-nuxt": "^1.0.2",
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.0",
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.4.3",
73
+ "nuxt-seo-utils": "^8.5.0",
72
74
  "nuxt-vitalizer": "^3.0.0",
73
75
  "pinia": "^4.0.3",
74
- "postcss": "^8.5.26",
76
+ "postcss": "^8.5.27",
75
77
  "postcss-no-important": "^11.0.0",
76
78
  "purgecss": "^8.0.0",
77
- "rolldown": "^1.2.6",
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/myelophone/nuxt.git"
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} B → ${Buffer.byteLength(css, 'utf8')} B)`,
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>