189 lines
5.7 KiB
TypeScript
189 lines
5.7 KiB
TypeScript
import { defineConfig, type HtmlTagDescriptor, type Plugin } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import path from 'node:path'
|
|
|
|
import siteConfiguration from './site.json'
|
|
|
|
// https://vitejs.dev/config/
|
|
export default defineConfig({
|
|
plugins: [
|
|
react(),
|
|
tailwindcss(),
|
|
siteConfigurationPlugin(siteConfiguration),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, './src'),
|
|
},
|
|
},
|
|
server: {
|
|
proxy: {
|
|
"/api": {
|
|
target: "http://127.0.0.1:5173",
|
|
changeOrigin: false,
|
|
xfwd: true,
|
|
},
|
|
},
|
|
},
|
|
build: {
|
|
sourcemap: true,
|
|
},
|
|
})
|
|
|
|
type SiteConfiguration = {
|
|
title?: string
|
|
description?: string
|
|
language?: string
|
|
robots?: { index?: boolean }
|
|
icons?: { icon?: string }
|
|
openGraph?: { image?: string }
|
|
analytics?: { googleAnalyticsId?: string }
|
|
customScripts?: {
|
|
headStart?: string
|
|
headEnd?: string
|
|
bodyStart?: string
|
|
bodyEnd?: string
|
|
}
|
|
accessibility?: { addBypassLinks?: boolean }
|
|
}
|
|
|
|
/** Applies /site.json to the generated document shell. */
|
|
function siteConfigurationPlugin(config: SiteConfiguration): Plugin {
|
|
function sanitizeHtmlValue(value: string | undefined): string {
|
|
return value?.replace(/[^a-zA-Z0-9_-]/g, '') || ''
|
|
}
|
|
function escapeHtmlText(value: string): string {
|
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
}
|
|
function replaceHtmlCommentSlot(html: string, slotName: string, content: string): string {
|
|
return html.replace(`<!-- ${slotName} -->`, content)
|
|
}
|
|
|
|
const title = config.title ?? 'App'
|
|
const description = config.description ?? ''
|
|
const favicon = config.icons?.icon ?? ''
|
|
const socialImage = config.openGraph?.image ?? ''
|
|
const language = sanitizeHtmlValue(config.language) || 'en'
|
|
const googleAnalyticsId = sanitizeHtmlValue(config.analytics?.googleAnalyticsId)
|
|
const headStart = config.customScripts?.headStart ?? ''
|
|
const headEnd = config.customScripts?.headEnd ?? ''
|
|
const bodyStart = config.customScripts?.bodyStart ?? ''
|
|
const bodyEnd = config.customScripts?.bodyEnd ?? ''
|
|
const robotsTxt = config.robots?.index === false ? 'User-agent: *\nDisallow: /\n' : ''
|
|
|
|
return {
|
|
name: 'site-configuration',
|
|
configureServer(server) {
|
|
server.middlewares.use((req, res, next) => {
|
|
if (!robotsTxt || req.url?.split('?')[0] !== '/robots.txt') return next()
|
|
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
res.end(robotsTxt)
|
|
})
|
|
},
|
|
generateBundle() {
|
|
if (!robotsTxt) return
|
|
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName: 'robots.txt',
|
|
source: robotsTxt,
|
|
})
|
|
},
|
|
transformIndexHtml: {
|
|
order: 'pre',
|
|
handler(html) {
|
|
let result = html
|
|
result = replaceHtmlCommentSlot(result, 'site:lang', language)
|
|
result = replaceHtmlCommentSlot(result, 'site:title', escapeHtmlText(title))
|
|
result = replaceHtmlCommentSlot(result, 'site:head-start', headStart)
|
|
result = replaceHtmlCommentSlot(result, 'site:head-end', headEnd)
|
|
result = replaceHtmlCommentSlot(result, 'site:body-start', bodyStart)
|
|
result = replaceHtmlCommentSlot(result, 'site:body-end', bodyEnd)
|
|
|
|
const tags: HtmlTagDescriptor[] = []
|
|
if (description) {
|
|
tags.push({ tag: 'meta', attrs: { name: 'description', content: description }, injectTo: 'head' })
|
|
}
|
|
if (config.robots?.index === false) {
|
|
tags.push({ tag: 'meta', attrs: { name: 'robots', content: 'noindex, nofollow' }, injectTo: 'head' })
|
|
}
|
|
if (favicon) {
|
|
tags.push({ tag: 'link', attrs: { rel: 'icon', href: favicon }, injectTo: 'head' })
|
|
}
|
|
if (title) {
|
|
tags.push({ tag: 'meta', attrs: { property: 'og:title', content: title }, injectTo: 'head' })
|
|
}
|
|
if (description) {
|
|
tags.push({ tag: 'meta', attrs: { property: 'og:description', content: description }, injectTo: 'head' })
|
|
}
|
|
if (socialImage) {
|
|
tags.push(
|
|
{ tag: 'meta', attrs: { property: 'og:image', content: socialImage }, injectTo: 'head' },
|
|
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' }, injectTo: 'head' },
|
|
{ tag: 'meta', attrs: { name: 'twitter:image', content: socialImage }, injectTo: 'head' },
|
|
)
|
|
}
|
|
|
|
if (googleAnalyticsId) {
|
|
tags.push(
|
|
{
|
|
tag: 'script',
|
|
attrs: {
|
|
async: true,
|
|
src: `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`,
|
|
},
|
|
injectTo: 'head',
|
|
},
|
|
{
|
|
tag: 'script',
|
|
children: `
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', ${JSON.stringify(googleAnalyticsId)});
|
|
`,
|
|
injectTo: 'head',
|
|
},
|
|
)
|
|
}
|
|
|
|
if (config.accessibility?.addBypassLinks) {
|
|
tags.push(
|
|
{
|
|
tag: 'style',
|
|
children: `
|
|
.bypass-link {
|
|
position: fixed;
|
|
top: 8px;
|
|
left: 8px;
|
|
z-index: 2147483647;
|
|
transform: translateY(-150%);
|
|
border-radius: 6px;
|
|
background: #111827;
|
|
color: #fff;
|
|
padding: 8px 12px;
|
|
font: 600 14px/1.2 system-ui, sans-serif;
|
|
text-decoration: none;
|
|
}
|
|
.bypass-link:focus {
|
|
transform: translateY(0);
|
|
}
|
|
`,
|
|
injectTo: 'head',
|
|
},
|
|
{
|
|
tag: 'a',
|
|
attrs: { class: 'bypass-link', href: '#root' },
|
|
children: 'Skip to content',
|
|
injectTo: 'body-prepend',
|
|
},
|
|
)
|
|
}
|
|
|
|
return { html: result, tags }
|
|
},
|
|
},
|
|
}
|
|
}
|