Personal portfolio, blog, and interactive tools site for Rujikorn Ngoensaard (XH / bossruji) — Data Architect specializing in Azure, Databricks, Microsoft Fabric, and data governance.
- Framework: Astro 6 (static output)
- Styling: Tailwind CSS v4 via Vite plugin
- Runtime: Bun + Node 22.16.0
- Testing: Vitest (100% coverage) & Playwright (E2E)
- Database: Supabase (blog CMS/source of truth, client-side benchmarks, CV gate)
- Blog Analytics: Cloudflare Pages Functions + D1 for anonymous aggregate blog views
- Edge Functions: Supabase Edge Functions (Deno) for CV email delivery
- Deployment: Cloudflare Pages (Git integration)
- Admin: Protected by Cloudflare Access (Zero Trust, email OTP)
graph LR
subgraph Developer
A[Local Dev] -->|git push| B[GitHub]
end
subgraph GitHub
B -->|PR / push| C[CI: Tests + Build]
end
subgraph Cloudflare
B -->|Git integration| D[Cloudflare Pages]
D -->|serves| E["Static Site<br/>xhverse.co"]
F["Cloudflare Access"] -->|protects /admin/*| E
E -->|POST /api/blog/views| K["Pages Function<br/>blog view counter"]
E -->|GET /admin/api/blog/views| L["Pages Function<br/>editorial analytics"]
E -->|POST /admin/api/blog/rebuild| N["Pages Function<br/>rebuild trigger"]
F -->|protects /admin/* and /admin/api/*| L
end
subgraph Supabase
G["PostgreSQL<br/>(posts CMS, benchmarks,<br/>cv_requests, leads)"]
H["Storage<br/>(CV PDF)"]
I["Edge Functions<br/>(send-cv only)"]
end
subgraph CloudflareD1["Cloudflare D1"]
M["blog view counters<br/>daily + referrer rollups"]
end
C -->|build-time fetch| G
N -->|deploy hook or Pages API| D
E -->|client INSERT/SELECT| G
E -->|direct download| H
G -->|webhook on UPDATE| I
I -->|sends email via Resend| J[User Inbox]
K -->|aggregate by slug| M
L -->|read admin rollups| M
%% v3 deferred: no unique visitor tracking or fingerprinting in this phase
See docs/architecture.md for detailed data flow, security model, and page inventory. For the blog view-count runtime, see docs/blog-analytics.md.
- Portfolio & Blog — Static portfolio content plus Supabase-authored blog posts published as static HTML with anonymous aggregate view counts
- Interactive Tools — Data Platform Maturity Checker, Governance Readiness Scorecard
- Advisory Services — Engagement types, process flow, conversion path
- CV Gate — Email capture modal → admin approval → automated PDF delivery via Edge Function
- Admin Panel — Dashboard, CV request management, lead tracking, tool submissions, blog analytics
- Dark/Light Theme — CSS variable system with anti-FOUC, zero-JS theme swap
- 100% Test Coverage — Unit tests on all data/logic modules, E2E on all pages
git clone https://github.com/devruji/xhverse-site.git
cd xhverse-site
bun installbun run dev # http://localhost:4321bun run typecheck # Astro type/content checks
bun run coverage # Unit tests + 100% coverage enforcement
bun run test:e2e # Playwright (chromium desktop + mobile)
bun run check # Full pipeline: typecheck + build + coverage + e2ebun run build # Output → ./dist/bunx supabase functions deploy send-cv # Deploy CV email function
bunx supabase secrets set RESEND_API_KEY=re_xxxxx- Cloudflare Pages Functions:
/api/blog/viewsand/admin/api/blog/views - Cloudflare D1 binding:
BLOG_ANALYTICS_DBfor anonymous aggregate blog views - Cloudflare rebuild trigger: deploy hook or Pages API env vars for admin-triggered static blog republishing
- Supabase Edge Functions:
send-cvfor approved CV email delivery
| Phase | Scope | Status |
|---|---|---|
| v1 | Public anonymous aggregate page views by blog slug | Code path added; Cloudflare D1 binding configured |
| v2 | Admin/editorial rollups by post, date, and referrer bucket | Code path added under /admin/blog/analytics/; Cloudflare D1 binding configured |
| v3 | Unique visitor counting or de-duplication | Deferred; no fingerprinting or identity tracking |
Cloudflare Pages Git integration handles all deployments:
| Branch | Environment | URL |
|---|---|---|
main |
Production | https://xhverse.co |
development |
Preview | Auto-generated preview URL |
feat/* |
Preview | Auto-generated preview URL |
Cloudflare Pages settings:
- Build command:
bun run build - Output directory:
dist - Node version: Read from
.node-version
| Variable | Environment | Purpose |
|---|---|---|
SUPABASE_URL |
Build | Database URL |
PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Build + client | Public read key for published blog rows and browser Supabase clients |
SUPABASE_SECRET_KEY |
Build (secret, optional legacy) | Server read key fallback for published blog rows |
PUBLIC_SUPABASE_URL |
Build + client | For CSP connect-src |
PUBLIC_SITE_URL |
Production | Canonical URL override |
PUBLIC_ALLOW_INDEXING |
Preview (optional) | Force indexing on preview |
BLOG_VIEW_TRACKING |
Pages Functions (optional) | Set disabled to read counts without incrementing |
BLOG_REBUILD_HOOK_URL |
Pages Functions (secret, optional) | Cloudflare deploy hook called after admin blog mutations |
CLOUDFLARE_API_TOKEN |
Pages Functions (secret) | Cloudflare API token with Pages deployment edit/create access for admin rebuilds when no deploy hook is configured |
CLOUDFLARE_ACCOUNT_ID |
Pages Functions | Cloudflare account ID used by the admin rebuild API trigger |
CLOUDFLARE_PAGES_PROJECT_NAME |
Pages Functions | Pages project name, currently xhverse-site-git |
CLOUDFLARE_PAGES_REBUILD_BRANCH |
Pages Functions (optional) | Branch to rebuild after admin mutations; defaults to main |
Cloudflare D1 is configured in both preview and production Pages environments as a binding named BLOG_ANALYTICS_DB; it is not a client-exposed environment variable.
Use wrangler.example.toml for the expected binding shape. Do not commit a real
wrangler.toml until the current Cloudflare Pages dashboard configuration has
been downloaded and reconciled.
Blog authoring uses Supabase as the source of truth. The admin Blog Writer saves
drafts/published posts to Supabase; public blog pages stay static and update
after Cloudflare Pages rebuilds from the configured deploy hook or Pages API
trigger. src/data/blog.ts is retained only as local fallback/seed content when
Supabase build credentials are absent.
To migrate current fallback posts into Supabase, run the seed script with service role build credentials after the schema migration is applied:
bun run scripts/seed-blog-posts-to-supabase.ts --dry-run
bun run scripts/seed-blog-posts-to-supabase.tsfeat/* ──PR──→ development ──Release PR──→ main ──tag──→ GitHub Release
Both development and main are protected branches requiring PRs + passing CI.
This project's code is licensed under the MIT License.
Note: Unless otherwise stated, all written content, images, and personal media published on the site are all rights reserved to the author.