feat: prerender every route + Gitea Actions deploy
Make the site fully prerendered so a plain curl returns complete content
for every route (crawlers / AI screening tools see real text, not an empty
#root), while humans keep full client interactivity.
Prerender:
- Build-time per-route render: prefetch data, renderToString, inline the
dehydrated react-query cache as window.__RQ_STATE__; client hydrateRoots
and refetches live (activity stays fresh; crawlers get the baked snapshot).
- New entry-server.tsx + prerender/{prefetch,routes,meta}.ts + run-prerender.mjs;
shared lib/ranges.ts keeps SSR and client query keys identical.
- pnpm build now: tsc -b -> vite client build -> ssr build -> prerender.
- API base absolute at build (VITE_API_BASE), relative /api/v1 in the browser.
- CSS imports moved to the client entry so the tree imports under Node.
- schema.org Person + Occupation JSON-LD and per-route title/description/og.
- UTC + explicit field widths on shared date formatting so SSR and client
hydration match byte-for-byte (fixes hydration mismatch on /activity).
- Strip non-text gist content from the CV fetch (1MB -> 25KB gzipped page).
Deploy (Gitea Actions, replaces script/deploy.sh):
- deploy.yml: on push to main, lint/test gate, build api+worker as static
musl binaries (pure-rustls, no glibc skew) + prerendered web, deploy each
over SSH as gitea_ci with scoped sudo.
- refresh.yml: daily cron re-bakes only the web snapshot so gist/activity
edits propagate without a push or bouncing the api/worker.
- script/infra-setup.sh + asset/sudoers.d/{api,worker,web}-host.conf for
one-time per-host provisioning. Secrets: RSYNC_SSH_KEY, QUERY_GITHUB_TOKEN,
QUERY_GITEA_TOKEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7zF7Kf4JqDwa6M8Qgge9M
This commit is contained in:
98
.gitea/workflows/refresh.yml
Normal file
98
.gitea/workflows/refresh.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
name: refresh
|
||||
|
||||
# Daily re-bake of the prerendered site. The crawler-visible HTML is a static
|
||||
# snapshot taken at build time; this job rebuilds it from the current gist (CV)
|
||||
# and activity API and redeploys *only* the web tier — so edits propagate to
|
||||
# what crawlers and AI screeners see without a code push and without bouncing
|
||||
# the api/worker. Humans already get live data via post-hydration refetch.
|
||||
#
|
||||
# Uses the same gitea_ci + scoped-sudo path as deploy.yml's deploy-web job;
|
||||
# see asset/sudoers.d/web-host.conf and script/infra-setup.sh.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 04:17 UTC daily — off-peak, arbitrary minute to avoid the top-of-hour herd.
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy # share the deploy lock so a refresh never races a push deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
WEB_HOST: oolon.kosherinata.internal
|
||||
SERVER_NAME: rob.tn
|
||||
WEB_ROOT: /var/www/rob.tn
|
||||
API_PORT: "42424"
|
||||
API_UPSTREAM_SCHEME: http
|
||||
API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424
|
||||
VITE_API_BASE: https://rob.tn/api/v1
|
||||
DEPLOY_KEY: |
|
||||
${{ secrets.RSYNC_SSH_KEY }}
|
||||
|
||||
jobs:
|
||||
build-web:
|
||||
name: Rebuild prerendered web
|
||||
runs-on: rust # image has node; we only need the web toolchain here
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build web (vite client + prerender)
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare pnpm@10 --activate
|
||||
pnpm --dir ui install --frozen-lockfile
|
||||
pnpm --dir ui run build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with: { name: web-dist, path: ui/dist, retention-days: 1 }
|
||||
|
||||
deploy-web:
|
||||
name: Deploy refreshed web to oolon
|
||||
needs: build-web
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with: { name: web-dist, path: dist }
|
||||
|
||||
- name: SSH init
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
||||
gitea_ci@"${WEB_HOST}" 'hostname -f'
|
||||
|
||||
- name: Render nginx vhost
|
||||
run: |
|
||||
mkdir -p rendered
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
t = open("asset/nginx/site.conf.tmpl").read()
|
||||
for k in ("SERVER_NAME", "API_UPSTREAM_SCHEME", "API_UPSTREAM_ADDR"):
|
||||
t = t.replace("{{%s}}" % k, os.environ[k])
|
||||
t = t.replace("{{DOCROOT}}", os.environ["WEB_ROOT"])
|
||||
open("rendered/site.conf", "w").write(t)
|
||||
PY
|
||||
|
||||
- name: Sync static site (prerendered)
|
||||
run: |
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/bin/install -d -m 0755 '"${WEB_ROOT}"
|
||||
rsync -az --delete --mkpath --rsync-path='sudo rsync' \
|
||||
--chown=root:root --chmod=D755,F644 \
|
||||
dist/ gitea_ci@"${WEB_HOST}":"${WEB_ROOT}/"
|
||||
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/sbin/restorecon -R '"${WEB_ROOT}"
|
||||
|
||||
- name: Sync nginx vhost + reload
|
||||
run: |
|
||||
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
|
||||
rendered/site.conf \
|
||||
gitea_ci@"${WEB_HOST}":/etc/nginx/conf.d/"${SERVER_NAME}".conf
|
||||
ssh gitea_ci@"${WEB_HOST}" '
|
||||
set -euo pipefail
|
||||
sudo /usr/sbin/setsebool -P httpd_can_network_connect on
|
||||
if ! sudo /usr/sbin/semanage port -l | grep -E "^http_port_t" | grep -qw '"${API_PORT}"'; then
|
||||
sudo /usr/sbin/semanage port -a -t http_port_t -p tcp '"${API_PORT}"'
|
||||
fi
|
||||
sudo /usr/sbin/restorecon -R /etc/nginx/conf.d/'"${SERVER_NAME}"'.conf
|
||||
sudo /usr/sbin/nginx -t
|
||||
sudo /usr/bin/systemctl reload nginx'
|
||||
Reference in New Issue
Block a user