@myelophone/nuxt 0.18.2

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 (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,147 @@
1
+ import type { NuxtSecurityRouteRules } from 'nuxt-security';
2
+
3
+ type SecurityHeaders = NonNullable<NuxtSecurityRouteRules['headers']>;
4
+ type SecurityHeaderKey = keyof SecurityHeaders;
5
+
6
+ const headerNames: Partial<Record<SecurityHeaderKey, string>> = {
7
+ contentSecurityPolicy: 'Content-Security-Policy',
8
+ crossOriginEmbedderPolicy: 'Cross-Origin-Embedder-Policy',
9
+ crossOriginOpenerPolicy: 'Cross-Origin-Opener-Policy',
10
+ crossOriginResourcePolicy: 'Cross-Origin-Resource-Policy',
11
+ originAgentCluster: 'Origin-Agent-Cluster',
12
+ referrerPolicy: 'Referrer-Policy',
13
+ strictTransportSecurity: 'Strict-Transport-Security',
14
+ xContentTypeOptions: 'X-Content-Type-Options',
15
+ xDNSPrefetchControl: 'X-DNS-Prefetch-Control',
16
+ xDownloadOptions: 'X-Download-Options',
17
+ xFrameOptions: 'X-Frame-Options',
18
+ xPermittedCrossDomainPolicies: 'X-Permitted-Cross-Domain-Policies',
19
+ xXSSProtection: 'X-XSS-Protection',
20
+ permissionsPolicy: 'Permissions-Policy',
21
+ };
22
+
23
+ const resolveCsp = (
24
+ value: Exclude<SecurityHeaders['contentSecurityPolicy'], false | undefined>,
25
+ nonce?: string,
26
+ ) =>
27
+ Object.fromEntries(
28
+ Object.entries(value).map(([directive, sources]) => {
29
+ const sourceValue = sources as boolean | string | string[];
30
+
31
+ if (typeof sourceValue === 'boolean') {
32
+ return [directive, sourceValue];
33
+ }
34
+
35
+ const tokens = (
36
+ typeof sourceValue === 'string'
37
+ ? sourceValue.split(' ')
38
+ : sourceValue
39
+ )
40
+ .map((source) => source.trim())
41
+ .filter(Boolean)
42
+ .map((source) =>
43
+ source === "'nonce-{{nonce}}'" && nonce
44
+ ? `'nonce-${nonce}'`
45
+ : source,
46
+ )
47
+ .filter((source) => source !== "'nonce-{{nonce}}'");
48
+
49
+ return [directive, tokens];
50
+ }),
51
+ );
52
+
53
+ const stringifyHeader = (key: SecurityHeaderKey, value: unknown): string => {
54
+ if (key === 'contentSecurityPolicy') {
55
+ return Object.entries(value as Record<string, boolean | string[]>)
56
+ .filter(([, sources]) => sources !== false)
57
+ .map(([directive, sources]) =>
58
+ directive === 'upgrade-insecure-requests'
59
+ ? 'upgrade-insecure-requests;'
60
+ : `${directive} ${Array.isArray(sources) ? sources.join(' ') : sources};`,
61
+ )
62
+ .join(' ');
63
+ }
64
+
65
+ if (key === 'strictTransportSecurity') {
66
+ const policies = value as {
67
+ maxAge: number;
68
+ includeSubdomains?: boolean;
69
+ preload?: boolean;
70
+ };
71
+
72
+ return [
73
+ `max-age=${policies.maxAge}`,
74
+ policies.includeSubdomains && 'includeSubDomains',
75
+ policies.preload && 'preload',
76
+ ]
77
+ .filter(Boolean)
78
+ .join('; ');
79
+ }
80
+
81
+ if (key === 'permissionsPolicy') {
82
+ return Object.entries(
83
+ value as Record<string, string | string[] | false>,
84
+ )
85
+ .filter(([, sources]) => sources !== false)
86
+ .map(([directive, sources]) =>
87
+ typeof sources === 'string'
88
+ ? `${directive}=${sources}`
89
+ : `${directive}=(${(sources as string[]).join(' ')})`,
90
+ )
91
+ .join(', ');
92
+ }
93
+
94
+ return String(value);
95
+ };
96
+
97
+ export default defineEventHandler((event) => {
98
+ const rules = event.context.security?.rules as
99
+ NuxtSecurityRouteRules | undefined;
100
+
101
+ if (!rules?.enabled) {
102
+ return;
103
+ }
104
+
105
+ if (rules.hidePoweredBy) {
106
+ removeResponseHeader(event, 'x-powered-by');
107
+ }
108
+
109
+ if (!rules.headers) {
110
+ return;
111
+ }
112
+
113
+ for (const [rawKey, rawValue] of Object.entries(rules.headers)) {
114
+ const key = rawKey as SecurityHeaderKey;
115
+ const headerName =
116
+ key === 'contentSecurityPolicy' &&
117
+ rules.contentSecurityPolicyReportOnly
118
+ ? 'Content-Security-Policy-Report-Only'
119
+ : headerNames[key];
120
+
121
+ if (!headerName) {
122
+ continue;
123
+ }
124
+
125
+ if (rawValue === false) {
126
+ const routeHeader = getRouteRules(event).headers?.[headerName];
127
+
128
+ if (getResponseHeader(event, headerName) === routeHeader) {
129
+ removeResponseHeader(event, headerName);
130
+ }
131
+ continue;
132
+ }
133
+
134
+ const value =
135
+ key === 'contentSecurityPolicy'
136
+ ? resolveCsp(
137
+ rawValue as Exclude<
138
+ SecurityHeaders['contentSecurityPolicy'],
139
+ false | undefined
140
+ >,
141
+ event.context.security?.nonce,
142
+ )
143
+ : rawValue;
144
+
145
+ setResponseHeader(event, headerName, stringifyHeader(key, value));
146
+ }
147
+ });
@@ -0,0 +1,43 @@
1
+ export default defineEventHandler((event) => {
2
+ if (event.method === 'OPTIONS') {
3
+ event.node.res.statusCode = 204;
4
+ return '';
5
+ }
6
+
7
+ const url = new URL(
8
+ event.node.req.url || '/',
9
+ `http://${event.node.req.headers.host}`,
10
+ );
11
+
12
+ const sanitize = (path: string) => {
13
+ let p = path
14
+ .replace(/[<>{}|\\^`]/g, '')
15
+ .replace(/\/+/g, '/')
16
+ .replace(/\.\.\//g, '')
17
+ .replace(/\.\./g, '');
18
+ if (p !== '/' && p.endsWith('/')) p = p.slice(0, -1);
19
+ return encodeURI(decodeURI(p)).toLowerCase();
20
+ };
21
+
22
+ const isLocalhost = (host: string) => {
23
+ return (
24
+ host.startsWith('localhost') ||
25
+ host.startsWith('127.') ||
26
+ host.startsWith('192.168.') ||
27
+ host.startsWith('::1') ||
28
+ /^[\d.]+$/.test(host)
29
+ );
30
+ };
31
+
32
+ const sanitizedPath = sanitize(url.pathname);
33
+ const isWww = url.hostname.startsWith('www.');
34
+ const finalHost = isWww ? url.hostname.replace(/^www\./, '') : url.hostname;
35
+ const needsHttps = !isLocalhost(finalHost) && url.protocol !== 'https:';
36
+ const port = url.port ? `:${url.port}` : '';
37
+
38
+ if (sanitizedPath !== url.pathname || isWww) {
39
+ const protocol = !needsHttps ? url.protocol : 'https:';
40
+ const newUrl = `${protocol}//${finalHost}${port}${sanitizedPath}${url.search}${url.hash}`;
41
+ return sendRedirect(event, newUrl, 301);
42
+ }
43
+ });
@@ -0,0 +1,15 @@
1
+ export default defineNitroPlugin((nitroApp) => {
2
+ nitroApp.hooks.hook('render:response', (response) => {
3
+ if (
4
+ typeof response.body === 'string' &&
5
+ response.body.startsWith('<!DOCTYPE html>')
6
+ ) {
7
+ response.body = response.body.replace(
8
+ /<html\b[^>]*>/i,
9
+ (htmlTag) => {
10
+ return htmlTag.replace(/\s{2,}/g, ' ');
11
+ },
12
+ );
13
+ }
14
+ });
15
+ });
@@ -0,0 +1,94 @@
1
+ export default defineNitroPlugin((nitroApp) => {
2
+ const emptyStyleRegex = /<style\b[^>]*>\s*<\/style>/gi;
3
+ const deferredBody = new WeakMap<
4
+ object,
5
+ { body: string[]; bodyAppend: string[] }
6
+ >();
7
+ const decoder = new TextDecoder();
8
+ const encoder = new TextEncoder();
9
+ const normalizeScriptNonces = (source: string) =>
10
+ source.replace(/<script\b[^>]*>/gi, (tag) => {
11
+ let hasNonce = false;
12
+ return tag.replace(/\snonce="[^"]*"/gi, (attribute) => {
13
+ if (hasNonce) {
14
+ return '';
15
+ }
16
+ hasNonce = true;
17
+ return attribute;
18
+ });
19
+ });
20
+
21
+ nitroApp.hooks.hook('render:html', (html) => {
22
+ html.head = html.head
23
+ .map((tag) => tag.replace(emptyStyleRegex, ''))
24
+ .filter(Boolean);
25
+ });
26
+
27
+ nitroApp.hooks.afterEach(({ name, args }) => {
28
+ if (name !== 'render:html') {
29
+ return;
30
+ }
31
+
32
+ const html = args[0] as {
33
+ head: string[];
34
+ bodyPrepend: string[];
35
+ body: string[];
36
+ bodyAppend: string[];
37
+ };
38
+ const context = args[1] as { event: object; streaming?: boolean };
39
+
40
+ for (const field of [
41
+ 'head',
42
+ 'bodyPrepend',
43
+ 'body',
44
+ 'bodyAppend',
45
+ ] as const) {
46
+ html[field] = html[field].map(normalizeScriptNonces);
47
+ }
48
+
49
+ if (context.streaming && (html.body.length || html.bodyAppend.length)) {
50
+ deferredBody.set(context.event, {
51
+ body: [...html.body],
52
+ bodyAppend: [...html.bodyAppend],
53
+ });
54
+ html.body.length = 0;
55
+ html.bodyAppend.length = 0;
56
+ }
57
+ });
58
+
59
+ nitroApp.hooks.hook('render:html:close', (html, { event }) => {
60
+ const deferred = deferredBody.get(event);
61
+ html.bodyAppend = [
62
+ ...(deferred?.body ?? []),
63
+ ...(deferred?.bodyAppend ?? []),
64
+ ...html.bodyAppend,
65
+ ]
66
+ .map((tag) => tag.replace(emptyStyleRegex, ''))
67
+ .filter(Boolean);
68
+ deferredBody.delete(event);
69
+ });
70
+
71
+ nitroApp.hooks.hook('render:html:chunk', (chunk, { event }) => {
72
+ const source = decoder.decode(chunk.chunk);
73
+ let normalized = normalizeScriptNonces(source);
74
+
75
+ if (chunk.index === 0) {
76
+ const headEnd = normalized.indexOf('</head>');
77
+ if (headEnd !== -1) {
78
+ const head = normalized
79
+ .slice(0, headEnd + '</head>'.length)
80
+ .replace(/>\s+</g, '><');
81
+ normalized =
82
+ head + normalized.slice(headEnd + '</head>'.length);
83
+ }
84
+ }
85
+
86
+ if (normalized !== source) {
87
+ chunk.chunk = encoder.encode(normalized);
88
+ }
89
+
90
+ if (chunk.index === 0 && event.context.security?.rules?.hidePoweredBy) {
91
+ removeResponseHeader(event, 'x-powered-by');
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,122 @@
1
+ import { brotliCompress, gzip } from 'node:zlib';
2
+ import { promisify } from 'node:util';
3
+
4
+ const compressBrotli = promisify(brotliCompress);
5
+ const compressGzip = promisify(gzip);
6
+
7
+ interface RequestLike {
8
+ headers?: Headers | Record<string, string | string[] | undefined>;
9
+ }
10
+
11
+ interface ResponseLike {
12
+ body?: unknown;
13
+ headers?: Headers | Record<string, string | number | undefined>;
14
+ }
15
+
16
+ export default defineNitroPlugin((nitroApp) => {
17
+ nitroApp.hooks.hook('render:response', async (response, { event }) => {
18
+ if (import.meta.dev || event.req.method !== 'GET') {
19
+ return;
20
+ }
21
+
22
+ const target = response as ResponseLike;
23
+ const contentType = getResponseHeader(target, 'content-type');
24
+ const acceptedEncoding = getRequestHeader(
25
+ (event.node?.req || event.req) as RequestLike,
26
+ 'accept-encoding',
27
+ );
28
+ const body = toBuffer(target.body);
29
+
30
+ if (
31
+ !body ||
32
+ !contentType?.includes('text/html') ||
33
+ getResponseHeader(target, 'content-encoding')
34
+ ) {
35
+ return;
36
+ }
37
+
38
+ if (acceptedEncoding.includes('br')) {
39
+ target.body = await compressBrotli(body);
40
+ setResponseHeader(target, 'content-encoding', 'br');
41
+ } else if (acceptedEncoding.includes('gzip')) {
42
+ target.body = await compressGzip(body);
43
+ setResponseHeader(target, 'content-encoding', 'gzip');
44
+ } else {
45
+ return;
46
+ }
47
+
48
+ const vary = getResponseHeader(target, 'vary');
49
+ if (
50
+ !vary
51
+ .toLowerCase()
52
+ .split(/\s*,\s*/)
53
+ .includes('accept-encoding')
54
+ ) {
55
+ setResponseHeader(
56
+ target,
57
+ 'vary',
58
+ vary ? `${vary}, Accept-Encoding` : 'Accept-Encoding',
59
+ );
60
+ }
61
+ deleteResponseHeader(target, 'content-length');
62
+ });
63
+ });
64
+
65
+ function getRequestHeader(request: RequestLike, name: string): string {
66
+ if (request.headers instanceof Headers) {
67
+ return request.headers.get(name) || '';
68
+ }
69
+
70
+ const value = request.headers?.[name];
71
+ return Array.isArray(value) ? value.join(',') : value || '';
72
+ }
73
+
74
+ function getResponseHeader(response: ResponseLike, name: string): string {
75
+ if (response.headers instanceof Headers) {
76
+ return response.headers.get(name) || '';
77
+ }
78
+
79
+ const value = response.headers?.[name];
80
+ return value === undefined ? '' : String(value);
81
+ }
82
+
83
+ function setResponseHeader(
84
+ response: ResponseLike,
85
+ name: string,
86
+ value: string,
87
+ ) {
88
+ if (response.headers instanceof Headers) {
89
+ response.headers.set(name, value);
90
+ return;
91
+ }
92
+
93
+ response.headers ||= {};
94
+ response.headers[name] = value;
95
+ }
96
+
97
+ function deleteResponseHeader(response: ResponseLike, name: string) {
98
+ if (response.headers instanceof Headers) {
99
+ response.headers.delete(name);
100
+ return;
101
+ }
102
+
103
+ if (response.headers) {
104
+ delete response.headers[name];
105
+ }
106
+ }
107
+
108
+ function toBuffer(body: unknown): Buffer | null {
109
+ if (typeof body === 'string') {
110
+ return Buffer.from(body);
111
+ }
112
+
113
+ if (body instanceof Uint8Array) {
114
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
115
+ }
116
+
117
+ if (body instanceof ArrayBuffer) {
118
+ return Buffer.from(body);
119
+ }
120
+
121
+ return null;
122
+ }
@@ -0,0 +1,26 @@
1
+ export default defineNitroPlugin((nitroApp) => {
2
+ nitroApp.hooks.hook('render:html', (html) => {
3
+ const head = html.head.join('');
4
+
5
+ const cssLinks =
6
+ head.match(/<link\b[^>]*rel=["']stylesheet["'][^>]*>/gi) ?? [];
7
+
8
+ const mainCssLink = cssLinks[0];
9
+
10
+ if (!mainCssLink) {
11
+ return;
12
+ }
13
+
14
+ const href = mainCssLink.match(
15
+ /\bhref=["']([^"']+\.css(?:\?[^"']*)?)["']/i,
16
+ )?.[1];
17
+
18
+ if (!href) {
19
+ return;
20
+ }
21
+
22
+ html.head.unshift(
23
+ `<link rel="preload" href="${href}" as="style" fetchPriority="high" crossorigin>`,
24
+ );
25
+ });
26
+ });
@@ -0,0 +1,31 @@
1
+ import { writeEarlyHints } from 'h3';
2
+
3
+ export default defineNitroPlugin((nitroApp) => {
4
+ nitroApp.hooks.hook('render:html', (htmlContext, { event }) => {
5
+ const links: string[] = [];
6
+
7
+ for (const tag of htmlContext.head) {
8
+ if (typeof tag !== 'string') continue;
9
+
10
+ if (tag.includes('rel="stylesheet"')) {
11
+ const match = tag.match(/href="([^"]+\.css[^"]*)"/);
12
+ if (match && match[1]) {
13
+ links.push(`<${match[1]}>; rel=preload; as=style`);
14
+ }
15
+ }
16
+
17
+ if (tag.includes('rel="modulepreload"')) {
18
+ const match = tag.match(/href="([^"]+\.js[^"]*)"/);
19
+ if (match && match[1]) {
20
+ links.push(
21
+ `<${match[1]}>; rel=modulepreload; as=script; crossorigin`,
22
+ );
23
+ }
24
+ }
25
+ }
26
+
27
+ if (links.length > 0) {
28
+ writeEarlyHints(event, links);
29
+ }
30
+ });
31
+ });
@@ -0,0 +1,43 @@
1
+ export default defineNitroPlugin((nitroApp) => {
2
+ nitroApp.hooks.hook('render:html', (html, { event }) => {
3
+ const config = useRuntimeConfig(event).myelophone;
4
+
5
+ if (!config.noindex) {
6
+ return;
7
+ }
8
+
9
+ const robotsMetaRegex = /<meta\b(?=[^>]*\bname=["']robots["'])[^>]*>/gi;
10
+
11
+ let robotsFound = false;
12
+
13
+ html.head = html.head.map((chunk) => {
14
+ if (!robotsMetaRegex.test(chunk)) {
15
+ robotsMetaRegex.lastIndex = 0;
16
+ return chunk;
17
+ }
18
+
19
+ robotsMetaRegex.lastIndex = 0;
20
+ robotsFound = true;
21
+
22
+ return chunk.replace(
23
+ robotsMetaRegex,
24
+ '<meta name="robots" content="noindex, nofollow">',
25
+ );
26
+ });
27
+
28
+ if (!robotsFound) {
29
+ html.head.push('<meta name="robots" content="noindex, nofollow">');
30
+ }
31
+ });
32
+
33
+ nitroApp.hooks.hook('render:response', (response, { event }) => {
34
+ const config = useRuntimeConfig(event).myelophone;
35
+
36
+ if (!config.noindex) {
37
+ return;
38
+ }
39
+
40
+ response.headers ??= {};
41
+ response.headers['X-Robots-Tag'] = 'noindex, nofollow';
42
+ });
43
+ });
@@ -0,0 +1,17 @@
1
+ export default defineNitroPlugin((nitroApp) => {
2
+ nitroApp.hooks.hook('render:html', (html) => {
3
+ const compact = (fragment: string) => fragment.replace(/[\r\n]+/g, ' ');
4
+ const sections = [
5
+ html.head,
6
+ html.bodyPrepend,
7
+ html.body,
8
+ html.bodyAppend,
9
+ ];
10
+
11
+ for (const section of sections) {
12
+ for (let index = 0; index < section.length; index++) {
13
+ section[index] = compact(section[index]!);
14
+ }
15
+ }
16
+ });
17
+ });
@@ -0,0 +1,7 @@
1
+ export default defineNitroPlugin((nitro) => {
2
+ nitro.hooks.hook('render:html', (html) => {
3
+ html.head.unshift(
4
+ '<style>body,img{overflow-x:hidden;max-width:100%}html,body,#__nuxt{margin:0;padding:0;width:100%;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,"Helvetica Neue",Arial,sans-serif,color-emoji}body,#__nuxt{min-height:100dvh;}html{height:auto;font-size:15px;-webkit-text-size-adjust:100%;text-size-adjust:100%}body{min-height:100dvh;font-size:1rem;position:relative}@media screen and (min-width:768px){html{font-size:16px}}@media screen and (min-width:1500px){html{font-size:17px}}@media screen and (min-width:1920px){html{font-size:18px}}@media screen and (min-width:2560px){html{font-size:20px}}@media screen and (max-height:650px){html{font-size:15px}}@media screen and (max-height:480px){html{font-size:14px}}.nojs .page-fullscreen-preloader{display:none}</style>',
5
+ );
6
+ });
7
+ });
@@ -0,0 +1 @@
1
+ export default defineEventHandler(() => ({ status: 'ok' }));
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../.nuxt/tsconfig.server.json",
3
+ }