Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 53s
ci / 모바일 린트·타입·Jest (push) Successful in 45s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 21s
ci / .NET API 서버 테스트 (push) Successful in 13s
deploy-site / deploy (push) Failing after 27s
ci / 워크스페이스 빌드 검증 (push) Successful in 36s
Crawlers received an empty client-rendered shell (107 characters of text); most AI crawlers do not run JavaScript, so the product was invisible to them. - Build renders each locale to static HTML (/ for Korean, /en/ … /vi/) with src/entry-server.tsx + scripts/prerender.mjs; the browser hydrates the same locale. The language menu links to those pages instead of switching in place. - Per-locale head: title, description, canonical, hreflang (+ x-default /en/), Open Graph and X cards with a per-locale 1200x630 image. - JSON-LD graph: Organization, WebSite, WebPage, SoftwareApplication (KRW offers, version, release date, download URL from the canonical sources), HowTo and FAQPage. No invented ratings. - robots.txt (search, ai-input and ai-train allowed; /app, /api, invites excluded), sitemap.xml with language alternates, llms.txt and llms-full.txt generated from the same translations and canonical values. - IndexNow key and a post-deploy ping (Bing, Naver, Yandex). - A factual "at a glance" section and two FAQ answers (recognised languages, where data is stored) in all 10 languages. - Icons, apple-touch-icon and web manifest; asset base is now absolute so sub-path pages load the same bundle. Verified: 3.6k-7.7k characters of text per page, no hydration warnings, no overflow at 320/1440 in six locales, axe 0, Lighthouse mobile SEO, accessibility and best practices 100.
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import { cpSync, mkdirSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const siteRoot = dirname(fileURLToPath(import.meta.url))
|
|
const safePublicFiles = [
|
|
'404.html',
|
|
'.well-known/assetlinks.json',
|
|
'accept-invite.css',
|
|
'accept-invite.js',
|
|
'accept-invite/index.html',
|
|
'download.html',
|
|
'favicon.svg',
|
|
'legal.css',
|
|
'privacy/index.html',
|
|
'terms/index.html',
|
|
'delete-account/index.html',
|
|
// 검색·공유용 자산 (scripts/render-brand-assets.mjs 가 만든다)
|
|
'site.webmanifest',
|
|
// IndexNow 소유 확인 키(공개 값). scripts/ci/ping-indexnow.mjs 가 같은 파일을 쓴다.
|
|
'5d886cca4138946b8488203724b2f53d.txt',
|
|
'icons',
|
|
'og',
|
|
]
|
|
|
|
export default defineConfig({
|
|
// Installers ship only through the Forgejo feed, never from this site. Only this
|
|
// non-binary allowlist can reach dist, so a stray local `public/releases` copy
|
|
// (git-ignored) can never be deployed.
|
|
publicDir: false,
|
|
plugins: [
|
|
react(),
|
|
(() => {
|
|
// 서버용 빌드(`vite build --ssr`, dist-ssr)에서는 복사하지 않는다 — 브라우저 빌드가 이미 복사했다.
|
|
let isSsrBuild = false
|
|
return {
|
|
name: 'copy-safe-public-files',
|
|
configResolved(config: { build: { ssr: unknown } }) {
|
|
isSsrBuild = Boolean(config.build.ssr)
|
|
},
|
|
closeBundle() {
|
|
if (isSsrBuild) return
|
|
for (const relativePath of safePublicFiles) {
|
|
const destination = join(siteRoot, 'dist', relativePath)
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
cpSync(join(siteRoot, 'public', relativePath), destination, { errorOnExist: true, recursive: true })
|
|
}
|
|
},
|
|
}
|
|
})(),
|
|
],
|
|
// 언어별 페이지(/en/ 등)에서도 자산을 찾도록 절대 경로를 쓴다.
|
|
base: '/',
|
|
})
|