diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1bf4ab948b5a..30cdaea4f069 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,3 +1,6 @@ +import * as cheerio from 'cheerio' +import fs from 'node:fs' +import path from 'node:path' import { defineConfig, resolveSiteDataByRoute, @@ -12,6 +15,11 @@ import llmstxt from 'vitepress-plugin-llms' const prod = !!process.env.NETLIFY +const headers: [string, string][] = [ + ['/assets/*', 'Cache-Control: max-age=31536000, immutable'], + ['/_translations/*', 'X-Robots-Tag: noindex'] +] + export default defineConfig({ title: 'VitePress', @@ -117,9 +125,9 @@ export default defineConfig({ apiKey: '52f578a92b88ad6abde815aae2b0ad7c', indexName: 'vitepress' } - }, + } - carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' } + // carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' } // TODO: temporarily disabled }, locales: { @@ -166,5 +174,95 @@ export default defineConfig({ ['meta', { property: 'og:title', content: title }] ) } - : undefined + : undefined, + + // TODO: add only on prod + transformHtml: (code, id, ctx) => { + if (id.endsWith('/404.html')) return + + // TODO: provide this as manifest + + const $ = cheerio.load(code) + const m = $.extract({ + links: [ + { + selector: 'link:is([rel*=preload],[rel*=preconnect])', + value: (el) => el.attribs + } + ], + scripts: [ + { + selector: 'script[type=module]', + value: (el) => { + const src = el.attribs.src + if (src && !src.startsWith('http')) { + return { href: src, rel: 'modulepreload' } + } + return null + } + } + ] + }) + const toPreload: HeadConfig[] = [ + ...m.links, + ...m.scripts, + { rel: 'preload', as: 'image', href: '/vitepress-logo-mini.svg' }, + ctx.pageData.frontmatter.layout === 'home' + ? { rel: 'preload', as: 'image', href: '/vitepress-logo-large.svg' } + : undefined + ] + .filter((x) => x !== undefined) + .map((link) => ['link', link]) + + id = id + .slice(ctx.siteConfig.outDir.length) + .replace(/(^|\/)index(?:\.html)?$/, '$1') + if (ctx.siteConfig.cleanUrls) { + id = id.replace(/\.html$/, '') + } + + headers.push([ + id, + 'Link: ' + toPreload.map((link) => toLinkHeader(link)).join(', ') + ]) + + return code.replace(/(<\w+)/g, '$1\n') // FIXME: hacky line splitting + }, + + buildEnd: (siteConfig) => { + headers.sort( + (a, b) => + b[0].length - a[0].length || + a[0].localeCompare(b[0]) || + a[1].localeCompare(b[1]) + ) + + fs.mkdirSync(path.join(siteConfig.outDir, '.vite'), { recursive: true }) + + fs.writeFileSync( + path.join(siteConfig.outDir, '.vite/headers.json'), + JSON.stringify(headers, null, 2), + 'utf-8' + ) + + fs.writeFileSync( + path.join(siteConfig.outDir, '_headers'), + headers.map(([id, header]) => `${id}\n\t${header}`).join('\n\n') + '\n', + 'utf-8' + ) + } }) + +function toLinkHeader([_, { href, ...attributes }]: HeadConfig): string { + const attributeParts = [`<${encodeURI(href)}>`] + + for (const [key, value] of Object.entries(attributes)) { + if (value === '') { + attributeParts.push(key) + } else { + attributeParts.push(`${key}="${value}"`) + } + } + + return attributeParts.join('; ') +} diff --git a/docs/package.json b/docs/package.json index 98f666051aab..db038a4b49c3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@lunariajs/core": "^0.1.1", + "cheerio": "^1.1.0", "markdown-it-mathjax3": "^4.3.2", "open-cli": "^8.0.0", "postcss-rtlcss": "^5.7.1", diff --git a/netlify.toml b/netlify.toml index b74ba994c16c..9f2e3c7515a7 100644 --- a/netlify.toml +++ b/netlify.toml @@ -6,18 +6,6 @@ publish = "docs/.vitepress/dist" command = "pnpm docs:build && pnpm docs:lunaria:build" -[[headers]] - for = "/assets/*" - [headers.values] - cache-control = ''' - max-age=31536000, - immutable''' - -[[headers]] - for = "/_translations/*" - [headers.values] - x-robots-tag = "noindex" - [[redirects]] from = "https://vitepress.vuejs.org/*" to = "https://vitepress.dev/:splat" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e59cee9db0a5..d0004a6ff8e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,6 +318,9 @@ importers: '@lunariajs/core': specifier: ^0.1.1 version: 0.1.1 + cheerio: + specifier: ^1.1.0 + version: 1.1.0 markdown-it-mathjax3: specifier: ^4.3.2 version: 4.3.2 @@ -1488,10 +1491,17 @@ packages: cheerio-select@1.6.0: resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + cheerio@1.0.0-rc.10: resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==} engines: {node: '>= 6'} + cheerio@1.1.0: + resolution: {integrity: sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==} + engines: {node: '>=18.17'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1635,6 +1645,9 @@ packages: css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -1692,6 +1705,9 @@ packages: dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} @@ -1703,9 +1719,16 @@ packages: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -1726,6 +1749,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -1733,6 +1759,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1985,6 +2015,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@5.0.1: resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} @@ -1995,6 +2028,10 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2544,9 +2581,18 @@ packages: parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2831,6 +2877,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -3097,6 +3146,10 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici@7.10.0: + resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -3221,6 +3274,14 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4349,6 +4410,15 @@ snapshots: domhandler: 4.3.1 domutils: 2.8.0 + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + cheerio@1.0.0-rc.10: dependencies: cheerio-select: 1.6.0 @@ -4359,6 +4429,20 @@ snapshots: parse5-htmlparser2-tree-adapter: 6.0.1 tslib: 2.8.1 + cheerio@1.1.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.0.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.10.0 + whatwg-mimetype: 4.0.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -4512,6 +4596,14 @@ snapshots: domutils: 2.8.0 nth-check: 2.1.1 + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + css-what@6.1.0: {} csstype@3.1.3: {} @@ -4553,6 +4645,12 @@ snapshots: domhandler: 4.3.1 entities: 2.2.0 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + domelementtype@2.3.0: {} domhandler@3.3.0: @@ -4563,12 +4661,22 @@ snapshots: dependencies: domelementtype: 2.3.0 + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -4587,10 +4695,17 @@ snapshots: emoji-regex@9.2.2: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + entities@2.2.0: {} entities@4.5.0: {} + entities@6.0.1: {} + environment@1.1.0: {} es-define-property@1.0.1: {} @@ -4877,6 +4992,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.1 + htmlparser2@5.0.1: dependencies: domelementtype: 2.3.0 @@ -4893,6 +5015,10 @@ snapshots: human-signals@8.0.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} index-to-position@1.1.0: {} @@ -5529,8 +5655,21 @@ snapshots: dependencies: parse5: 6.0.1 + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + parse5@6.0.1: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-browserify@1.0.1: {} path-key@3.1.1: {} @@ -5826,6 +5965,8 @@ snapshots: safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + sax@1.4.1: {} section-matter@1.0.0: @@ -6063,6 +6204,8 @@ snapshots: undici-types@7.8.0: {} + undici@7.10.0: {} + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -6277,6 +6420,12 @@ snapshots: webidl-conversions@3.0.1: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 8e4af6442c28..b7d431b98c42 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -1,7 +1,7 @@ import type { Component, InjectionKey } from 'vue' import { inject, markRaw, nextTick, reactive, readonly } from 'vue' import type { Awaitable, PageData, PageDataPayload } from '../shared' -import { notFoundPageData, treatAsHtml } from '../shared' +import { notFoundPageData, treatAsHtml, normalize } from '../shared' import { siteDataRef } from './data' import { getScrollOffset, inBrowser, withBase } from './utils' @@ -303,16 +303,16 @@ function handleHMR(route: Route): void { } function shouldHotReload(payload: PageDataPayload): boolean { - const payloadPath = payload.path.replace(/(?:(^|\/)index)?\.md$/, '$1') - const locationPath = location.pathname - .replace(/(?:(^|\/)index)?\.html$/, '') - .slice(siteDataRef.value.base.length - 1) + const payloadPath = normalize(payload.path) + const locationPath = normalize(location.pathname).slice( + siteDataRef.value.base.length - 1 + ) return payloadPath === locationPath } function normalizeHref(href: string): string { const url = new URL(href, fakeHost) - url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, '$1') + url.pathname = url.pathname.replace(/(^|\/)index(?:\.html)?$/, '$1') // ensure correct deep link so page refresh lands on correct files. if (siteDataRef.value.cleanUrls) { url.pathname = url.pathname.replace(/\.html$/, '')