d3ro-voice/.forgejo/workflows/deploy-site.yml
Yun Chan a0abda7a5c
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
feat(site): prerender every locale and publish structured data, llms.txt and sitemaps
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.
2026-09-27 10:45:05 +09:00

95 lines
4.8 KiB
YAML

name: deploy-site
# 사이트(site/) 배포의 유일한 경로: Cloudflare Pages `d3ro` (d3ro.chanpaca.net).
# 예전 deploy-site-windows.yml(수동 전용·배포 검증 없음)과 .github/workflows/deploy-site.yml
# (GitHub Pages, 실행된 적 없음)을 이 파일로 합쳤다.
#
# site/는 저장소 루트의 packages/core/src/*.ts 를 상대 import 하므로 전체 트리를 체크아웃한다
# (sparse checkout 금지).
#
# 필요한 시크릿: CF_API_TOKEN(또는 CLOUDFLARE_API_TOKEN), CF_ACCOUNT_ID(또는 CLOUDFLARE_ACCOUNT_ID)
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: linux-builder
steps:
- name: checkout
env: { CI_TOKEN: "${{ github.token }}" }
run: |
proto="${GITHUB_SERVER_URL%%://*}"; host="${GITHUB_SERVER_URL#*://}"
url="$proto://actions:${CI_TOKEN}@${host%/}/${GITHUB_REPOSITORY}.git"
[ -d .git ] || git init -q .
git remote remove origin 2>/dev/null || true
git remote add origin "$url"
git fetch -q --depth 1 origin "$GITHUB_REF"
git checkout -q -f FETCH_HEAD
git clean -qfdx
- name: Node (.nvmrc) 설치
shell: bash
run: |
set -euo pipefail
export CI_PROJECT_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/d3ro-ci"
. scripts/ci/bootstrap-linux-toolchain.sh node
dirname "$(command -v node)" >> "$GITHUB_PATH"
- name: 모바일 릴리스 공개 경계 검사
run: node scripts/ci/verify-mobile-release-boundary.mjs --self-test
- name: 의존성 설치 및 사이트 빌드
run: |
npm ci --prefix site
npm run build --prefix site
node -e "const fs=require('node:fs'); const v=require('./release/product-version.json').version; fs.writeFileSync('site/dist/release-identity.json', JSON.stringify({commit:process.env.GITHUB_SHA,version:v},null,2)+'\n')"
- name: Cloudflare Pages 배포 (d3ro.chanpaca.net)
env:
CLOUDFLARE_API_TOKEN: "${{ secrets.CF_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}"
CLOUDFLARE_ACCOUNT_ID: "${{ secrets.CF_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}"
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ]; then
echo "Cloudflare Pages 배포 시작 (d3ro)..."
npx --yes wrangler@latest pages deploy site/dist \
--project-name d3ro \
--branch main \
--commit-dirty=true || \
npx --yes wrangler@latest pages deploy site/dist \
--project-name d3ro-voice \
--branch main \
--commit-dirty=true
else
echo "CLOUDFLARE_API_TOKEN 없음" >&2
exit 1
fi
- name: 공개 배포 식별자 검증
run: |
# Pages 배포가 엣지에 퍼지기까지 수십 초 걸린다. 새 커밋이 보일 때까지 최대 3분 기다린다.
for attempt in $(seq 1 18); do
curl --fail --silent --show-error --retry 3 --retry-delay 5 \
-H 'Cache-Control: no-cache' \
"https://d3ro.chanpaca.net/release-identity.json?t=$(date +%s)" \
--output release-identity.live.json
if node -e "const live=JSON.parse(require('node:fs').readFileSync('release-identity.live.json','utf8')); process.exit(live.commit===process.env.GITHUB_SHA?0:1)"; then break; fi
echo "live commit not yet $GITHUB_SHA (attempt $attempt)"; sleep 10
done
node -e "const fs=require('node:fs'); const live=JSON.parse(fs.readFileSync('release-identity.live.json','utf8')); if(live.commit!==process.env.GITHUB_SHA) throw new Error('public commit mismatch'); const expected=require('./release/product-version.json').version; if(live.version!==expected) throw new Error('public version mismatch')"
node scripts/ci/verify-android-app-links.mjs
curl --fail --silent --show-error https://d3ro.chanpaca.net/ --output /dev/null
curl --fail --silent --show-error https://d3ro.chanpaca.net/privacy/ --output /dev/null
curl --fail --silent --show-error https://d3ro.chanpaca.net/terms/ --output /dev/null
curl --fail --silent --show-error https://d3ro.chanpaca.net/delete-account/ --output /dev/null
# 검색·AI 검색용 산출물(언어별 미리 그린 페이지, sitemap, robots, llms.txt)
for path in en/ ja/ sitemap.xml robots.txt llms.txt llms-full.txt; do
curl --fail --silent --show-error "https://d3ro.chanpaca.net/$path" --output /dev/null
done
curl --fail --silent --show-error https://d3ro.chanpaca.net/en/ | grep -q 'application/ld+json'
- name: IndexNow 알림 (Bing·네이버·Yandex)
continue-on-error: true
run: node scripts/ci/ping-indexnow.mjs