v1.0 - initial version
This commit is contained in:
commit
a1e8bef389
15 changed files with 2031 additions and 0 deletions
177
vite.config.ts
Normal file
177
vite.config.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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 }
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue