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:
35
asset/sudoers.d/api-host.conf
Normal file
35
asset/sudoers.d/api-host.conf
Normal file
@@ -0,0 +1,35 @@
|
||||
# Scoped sudo for the gitea_ci deploy user on the moments-api host (nikola).
|
||||
# Installed by script/infra-setup.sh as /etc/sudoers.d/moments_api_gitea_ci and
|
||||
# verified with `visudo -cf`. Every rule is pinned to one literal destination;
|
||||
# the `*` in an rsync rule matches rsync's --server argument vector, the trailing
|
||||
# path is what actually bounds it. `:` and `=` are escaped (sudoers reserves them).
|
||||
|
||||
# --- file pushes (rsync --rsync-path='sudo rsync') ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/moments.conf
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/moments-api
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/moments/api.env
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-api.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-api-cert.path
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-api-cert-reload.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/moments-api.xml
|
||||
|
||||
# --- service account + directories ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g moments -m 0750 /etc/moments
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o moments -g moments -m 0750 /var/lib/moments
|
||||
|
||||
# --- cert ACL, SELinux, firewalld ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/setfacl -m u\:moments\:r /etc/pki/tls/private/*.pem
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/moments-api /etc/moments /var/lib/moments
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -l
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -a -t http_port_t -p tcp 42424
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --reload
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --query-service\=moments-api
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --add-service\=moments-api --permanent
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --add-service\=moments-api
|
||||
|
||||
# --- service lifecycle ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable --now moments-api-cert.path
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable moments-api.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart moments-api.service
|
||||
20
asset/sudoers.d/web-host.conf
Normal file
20
asset/sudoers.d/web-host.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
# Scoped sudo for the gitea_ci deploy user on the web host (oolon). Installed by
|
||||
# script/infra-setup.sh as /etc/sudoers.d/moments_web_gitea_ci and verified with
|
||||
# `visudo -cf`. Used by both deploy.yml's deploy-web and refresh.yml.
|
||||
# See api-host.conf for the rsync-rule convention.
|
||||
|
||||
# --- docroot + static site ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -m 0755 /var/www/rob.tn
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /var/www/rob.tn/
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /var/www/rob.tn
|
||||
|
||||
# --- nginx vhost ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/nginx/conf.d/rob.tn.conf
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /etc/nginx/conf.d/rob.tn.conf
|
||||
|
||||
# --- SELinux booleans/ports for the /api proxy + reload ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/setsebool -P httpd_can_network_connect on
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -l
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -a -t http_port_t -p tcp 42424
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/nginx -t
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
|
||||
27
asset/sudoers.d/worker-host.conf
Normal file
27
asset/sudoers.d/worker-host.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
# Scoped sudo for the gitea_ci deploy user on the moments-worker host (frootmig).
|
||||
# Installed by script/infra-setup.sh as /etc/sudoers.d/moments_worker_gitea_ci and
|
||||
# verified with `visudo -cf`. See api-host.conf for the rsync-rule convention.
|
||||
|
||||
# --- file pushes (rsync --rsync-path='sudo rsync') ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/moments.conf
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/moments-worker
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/moments/worker.env
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-worker.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-worker-cert.path
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/moments-worker-cert-reload.service
|
||||
|
||||
# --- service account + directories ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g moments -m 0750 /etc/moments
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o moments -g moments -m 0750 /var/lib/moments
|
||||
|
||||
# --- cert ACL, SELinux ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/setfacl -m u\:moments\:r /etc/pki/tls/private/*.pem
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/moments-worker /etc/moments /var/lib/moments
|
||||
|
||||
# --- service lifecycle ---
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable --now moments-worker-cert.path
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable moments-worker.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart moments-worker.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl is-active --quiet moments-worker.service
|
||||
Reference in New Issue
Block a user