@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.
- package/.devcontainer/Dockerfile +7 -0
- package/.devcontainer/devcontainer.json +21 -0
- package/.editorconfig +43 -0
- package/.github/workflows/package.yml +73 -0
- package/.nuxtignore +6 -0
- package/.releaserc.js +132 -0
- package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
- package/.yarnrc.yml +8 -0
- package/CHANGELOG.md +141 -0
- package/LICENSE +77 -0
- package/README.md +3004 -0
- package/app/app.vue +455 -0
- package/app/assets/css/animation.css +81 -0
- package/app/assets/css/backgrounds.css +57 -0
- package/app/assets/css/base.css +182 -0
- package/app/assets/css/index.css +9 -0
- package/app/assets/css/links.css +12 -0
- package/app/assets/css/multimedia.css +102 -0
- package/app/assets/css/preloader.css +68 -0
- package/app/assets/css/print.css +40 -0
- package/app/assets/css/tailwind.css +20 -0
- package/app/assets/css/theme.css +29 -0
- package/app/components/consent/GoogleMap.vue +59 -0
- package/app/components/consent/Youtube.vue +27 -0
- package/app/components/cookie/Banner.vue +70 -0
- package/app/components/cookie/ConsentWrapper.vue +71 -0
- package/app/components/cookie/PrivacyPolicy.vue +179 -0
- package/app/components/cookie/SettingsModal.vue +387 -0
- package/app/components/grid/Col.vue +262 -0
- package/app/components/grid/Container.vue +236 -0
- package/app/components/grid/Row.vue +248 -0
- package/app/components/grid/Stack.vue +233 -0
- package/app/components/myelophone/Copyright.vue +5 -0
- package/app/components/myelophone/Welcome.vue +107 -0
- package/app/components/page/FullscreenPreloader.vue +177 -0
- package/app/components/page/Preloader.vue +18 -0
- package/app/components/page/ReadyLocalized.vue +19 -0
- package/app/components/ready/.gitkeep +0 -0
- package/app/components/safe/Email.vue +52 -0
- package/app/components/safe/ImgWithLoader.vue +180 -0
- package/app/components/safe/NuxtImg.vue +25 -0
- package/app/components/safe/NuxtPicture.vue +29 -0
- package/app/components/section/.gitkeep +0 -0
- package/app/components/seo/BrandInfo.vue +76 -0
- package/app/components/seo/ContentNoIndex.vue +35 -0
- package/app/components/seo/NoIndex.vue +22 -0
- package/app/components/seo/OrgInfo.vue +130 -0
- package/app/components/seo/ProfileInfo.vue +137 -0
- package/app/components/ui/Accordion.vue +163 -0
- package/app/components/ui/Alert.vue +179 -0
- package/app/components/ui/Avatar.vue +88 -0
- package/app/components/ui/Badge.vue +150 -0
- package/app/components/ui/Banner.vue +133 -0
- package/app/components/ui/Button.vue +175 -0
- package/app/components/ui/Card.vue +66 -0
- package/app/components/ui/Checkbox.vue +134 -0
- package/app/components/ui/Chip.vue +136 -0
- package/app/components/ui/CommandPalette.vue +434 -0
- package/app/components/ui/Console.vue +583 -0
- package/app/components/ui/CursorCreative.vue +148 -0
- package/app/components/ui/FeatureAccordion.vue +165 -0
- package/app/components/ui/FileUpload.vue +705 -0
- package/app/components/ui/GeoDependent.vue +72 -0
- package/app/components/ui/Heading.vue +51 -0
- package/app/components/ui/Icon.vue +42 -0
- package/app/components/ui/Icon8.vue +39 -0
- package/app/components/ui/Input.vue +88 -0
- package/app/components/ui/LangLink.vue +34 -0
- package/app/components/ui/LangVisible.vue +33 -0
- package/app/components/ui/LanguageSelect.vue +202 -0
- package/app/components/ui/LightBox.vue +155 -0
- package/app/components/ui/LocalTime.vue +170 -0
- package/app/components/ui/Modal.vue +161 -0
- package/app/components/ui/Popup.vue +200 -0
- package/app/components/ui/PostContent.server.vue +23 -0
- package/app/components/ui/ProtectedEmail.vue +52 -0
- package/app/components/ui/RelativeTime.vue +141 -0
- package/app/components/ui/ScheduledContent.vue +150 -0
- package/app/components/ui/ScrollToTop.client.vue +48 -0
- package/app/components/ui/SegmentedControl.vue +148 -0
- package/app/components/ui/SiteSearch.vue +1191 -0
- package/app/components/ui/Skeleton.vue +91 -0
- package/app/components/ui/SmartContrast.vue +74 -0
- package/app/components/ui/SnapContainer.vue +222 -0
- package/app/components/ui/SnapSection.vue +294 -0
- package/app/components/ui/SonnerStyle.vue +7 -0
- package/app/components/ui/SplitSection.vue +99 -0
- package/app/components/ui/Steps.vue +858 -0
- package/app/components/ui/StickyWrapper.vue +46 -0
- package/app/components/ui/Table.vue +251 -0
- package/app/components/ui/Tabs.vue +451 -0
- package/app/components/ui/TextColumn.vue +36 -0
- package/app/components/ui/Textarea.vue +251 -0
- package/app/components/ui/Toggler.vue +40 -0
- package/app/components/ui/Tooltip.vue +524 -0
- package/app/components/ui/TruncateText.vue +105 -0
- package/app/components/ui/ViewportSpacer.vue +72 -0
- package/app/components/view/AnimatedCounter.vue +121 -0
- package/app/components/view/AnnouncementBar.vue +104 -0
- package/app/components/view/AvatarGroup.vue +104 -0
- package/app/components/view/Breadcrumbs.vue +97 -0
- package/app/components/view/ContentWithToc.vue +152 -0
- package/app/components/view/Copyright.vue +68 -0
- package/app/components/view/CountdownTimer.vue +184 -0
- package/app/components/view/CreativeCTA.vue +143 -0
- package/app/components/view/CurrencySelect.vue +254 -0
- package/app/components/view/FeatureBoxGrid.vue +142 -0
- package/app/components/view/HorizontalMenu.vue +97 -0
- package/app/components/view/ImageCompare.vue +141 -0
- package/app/components/view/ImgSlider.vue +333 -0
- package/app/components/view/InfiniteMarquee.vue +147 -0
- package/app/components/view/InfoBar.vue +86 -0
- package/app/components/view/LogoGrid.vue +106 -0
- package/app/components/view/LogoSlider.vue +83 -0
- package/app/components/view/NotFound.vue +81 -0
- package/app/components/view/PartialBackground.vue +313 -0
- package/app/components/view/QuoteBig.vue +111 -0
- package/app/components/view/ResponsiveMenu.vue +219 -0
- package/app/components/view/ReviewsSlider.vue +230 -0
- package/app/components/view/SocialBar.vue +78 -0
- package/app/components/view/StickyHeadingContent.vue +158 -0
- package/app/components/view/TallyForm.vue +295 -0
- package/app/components/view/WarningOutdated.vue +113 -0
- package/app/composables/useApi.ts +88 -0
- package/app/composables/useAppSeo.ts +78 -0
- package/app/composables/useBreadcrumbs.ts +106 -0
- package/app/composables/useChangeUrl.ts +73 -0
- package/app/composables/useCommand.ts +28 -0
- package/app/composables/useCookieControl.ts +111 -0
- package/app/composables/useCookieModal.ts +9 -0
- package/app/composables/useDevice.ts +109 -0
- package/app/composables/useGeoFetch.ts +139 -0
- package/app/composables/useLoadScript.ts +36 -0
- package/app/composables/useModalState.ts +37 -0
- package/app/composables/useMultiLang.ts +278 -0
- package/app/composables/useNetwork.ts +49 -0
- package/app/composables/useNuxtVersion.ts +5 -0
- package/app/composables/usePiniaStore.ts +5 -0
- package/app/composables/usePopupControl.ts +57 -0
- package/app/composables/useQuery.ts +307 -0
- package/app/composables/useReadyPage.ts +182 -0
- package/app/composables/useSafeList.ts +90 -0
- package/app/composables/useScriptLoader.ts +221 -0
- package/app/composables/useSiteSearch.ts +32 -0
- package/app/composables/useSnapScroll.ts +24 -0
- package/app/composables/useStatic.ts +5 -0
- package/app/composables/useStorage.ts +252 -0
- package/app/composables/useStoreBroadcast.ts +146 -0
- package/app/composables/useThemeSync.ts +81 -0
- package/app/constants/layout.ts +109 -0
- package/app/constants/predefinedCookieScripts.ts +1222 -0
- package/app/constants/ui-events.ts +1 -0
- package/app/error.vue +169 -0
- package/app/layouts/default-blog.vue +5 -0
- package/app/layouts/default.vue +3 -0
- package/app/locales/en/commands.json +5 -0
- package/app/locales/en/cookies.json +182 -0
- package/app/locales/en/error.json +4 -0
- package/app/locales/en/interface.json +33 -0
- package/app/locales/en/myelophone.json +8 -0
- package/app/locales/en/outdated.json +5 -0
- package/app/locales/en/times.json +18 -0
- package/app/locales/pl/commands.json +5 -0
- package/app/locales/pl/cookies.json +182 -0
- package/app/locales/pl/error.json +4 -0
- package/app/locales/pl/interface.json +33 -0
- package/app/locales/pl/myelophone.json +8 -0
- package/app/locales/pl/outdated.json +5 -0
- package/app/locales/pl/times.json +18 -0
- package/app/locales/ru/commands.json +5 -0
- package/app/locales/ru/cookies.json +182 -0
- package/app/locales/ru/error.json +4 -0
- package/app/locales/ru/interface.json +33 -0
- package/app/locales/ru/myelophone.json +8 -0
- package/app/locales/ru/outdated.json +5 -0
- package/app/locales/ru/times.json +18 -0
- package/app/middleware/blog-layout.ts +28 -0
- package/app/middleware/i18n.global.ts +17 -0
- package/app/middleware/not-found-status.ts +11 -0
- package/app/modules/default-blog/index.ts +39 -0
- package/app/modules/default-blog/posts.ts +80 -0
- package/app/modules/default-sitemap/index.ts +116 -0
- package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
- package/app/modules/fullscreen-preloader/module.ts +43 -0
- package/app/modules/htaccess/.htaccess +38 -0
- package/app/modules/htaccess/index.ts +23 -0
- package/app/modules/i18n-shaker.mjs +221 -0
- package/app/modules/multi18n/module.ts +71 -0
- package/app/modules/myelophone-config/module.ts +241 -0
- package/app/pages/[...all].vue +9 -0
- package/app/pages/index.vue +3 -0
- package/app/pages/post/[...slug].vue +30 -0
- package/app/plugins/bfcache-refresh.client.ts +9 -0
- package/app/plugins/canonical.ts +30 -0
- package/app/plugins/csp-nonce.server.ts +41 -0
- package/app/plugins/multi18n.client.ts +21 -0
- package/app/plugins/multi18n.server.ts +15 -0
- package/app/plugins/nuxt-version.client.ts +4 -0
- package/app/plugins/restore.client.ts +21 -0
- package/app/plugins/reveal.ts +89 -0
- package/app/plugins/url-helper.client.ts +66 -0
- package/app/server/api/exchange-rates.get.ts +216 -0
- package/app/server/api/healthz.ts +1 -0
- package/app/server/api/logout.post.ts +6 -0
- package/app/server/middleware/normalize-url.ts +14 -0
- package/app/server/middleware/security-headers.ts +147 -0
- package/app/server/middleware/url-helper.ts +43 -0
- package/app/server/plugins/clean-html-spaces.ts +15 -0
- package/app/server/plugins/clean-styles.ts +94 -0
- package/app/server/plugins/compression.ts +122 -0
- package/app/server/plugins/css-preload.ts +26 -0
- package/app/server/plugins/early-hints.ts +31 -0
- package/app/server/plugins/force-noindex.ts +43 -0
- package/app/server/plugins/html-one-line.ts +17 -0
- package/app/server/plugins/inline-style.ts +7 -0
- package/app/server/routes/healthz.get.ts +1 -0
- package/app/server/tsconfig.json +3 -0
- package/app/stores/cart.ts +945 -0
- package/app/stores/commands.ts +43 -0
- package/app/stores/device.ts +156 -0
- package/app/stores/querySync.ts +5 -0
- package/app/stores/settings.ts +145 -0
- package/app/stores/user.ts +607 -0
- package/app/types/cookie.ts +89 -0
- package/app/types/myelophone.d.ts +87 -0
- package/app/types/site-search.ts +110 -0
- package/app/utils/asyncRefresh.ts +4 -0
- package/app/utils/blog.server.ts +142 -0
- package/app/utils/jsonLd.ts +56 -0
- package/app/utils/myelophoneConfig.ts +4 -0
- package/app/utils/sortBy.ts +18 -0
- package/app/utils/transliterate.ts +75 -0
- package/app/utils/ytVideo.ts +37 -0
- package/app/workers/site-search.worker.ts +661 -0
- package/biome.json +94 -0
- package/commitlint.config.js +3 -0
- package/nuxt.config.ts +539 -0
- package/package.json +101 -0
- package/playground/content/.gitkeep +0 -0
- package/playground/locales/.gitkeep +0 -0
- package/playground/myelophone.dev.ts +45 -0
- package/playground/nuxt.config.ts +105 -0
- package/playground/package.json +5 -0
- package/playground/pages/.gitkeep +0 -0
- package/playground/tsconfig.json +3 -0
- package/playwright.config.ts +23 -0
- package/public/browsers/chrome.svg +77 -0
- package/public/browsers/edge.svg +98 -0
- package/public/browsers/firefox.svg +300 -0
- package/public/favicon.ico +0 -0
- package/public/myelophone_eng.png +0 -0
- package/public/myelophone_eng_white.png +0 -0
- package/spa-loading-template.html +72 -0
- package/tests/home.spec.ts +9 -0
- package/tests/utils.ts +23 -0
- package/tsconfig.json +16 -0
- package/vite/plugins/clean-css.ts +190 -0
- package/vite/plugins/postcss-add-viewportunits.ts +33 -0
- 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' }));
|