Personal portfolio site of Nikita Boyarkin — Product / Data Analyst. Static Astro build, bilingual (RU / EN), 17 case studies, client JS only where it earns its place.
Live: https://nikitaboyarkin.github.io/ · EN mirror: https://nikitaboyarkin.github.io/en/
- What's on the site
- Projects
- Stack
- Requirements
- Quick start
- Verification
- Project structure
- Content
- Analytics
- OG image generation
- CV
- GitHub ↔ portfolio sync
- Monitoring
- CI/CD
- Local hooks
- Documentation
- License
Russian is the default locale; every route below has an /en/… mirror unless noted.
| Route | What it holds |
|---|---|
/ |
Home — hero, headline cases, career snapshot, bento grid, featured writing |
/projects/ |
Catalogue — all 17 cases as a board, grouped by track |
/projects/<slug>/ |
Case study page (RU source + EN mirror) |
/projects/volta/<part>/ |
The 23 linked sub-parts of the Volta case |
/about/ |
About + jump nav to anchors #who, #work, #now, #start, #value |
/notes/ |
Notes hub with category filters (absorbs /guides/) |
/writing/ |
Paginated article archive |
/topics/, /topics/<tag>/ |
Topic taxonomy and level grouping |
/graph/ |
Knowledge graph of the notes — communities, force layout, zoom |
/contact/ |
Contact |
/404 |
RU only — no EN mirror, no hreflang |
Generated endpoints: /rss.xml, /sitemap-index.xml, /robots.txt, /llms.txt, /.well-known/llms.txt, /search-index.json, /graph.json, /graph-en.json. Search is client-side over search-index.json — there is no /search/ page.
17 case studies, each with a page at /projects/<slug>/, a board column set by track, and a public repo and/or live demo where one exists. Three are the headline cases featured on the home page — volta, sql, cohort — chosen because each is backed by a reachable public artifact.
Links: page = the case study on the live site · repo = source on GitHub · demo = runnable artifact.
| Project | What it shows | Links |
|---|---|---|
| Volta Neobank | Found the onboarding bottleneck and closed it with an A/B test: +5.72 pp KYC conversion, €656K/yr. 23 linked sub-projects across funnel → A/B → retention → segmentation | page · repo · demo |
| A/B Testing Toolkit | 15 methodology modules, each calibrated by simulation — A/A holds Type I error at α, power curves plot achievable effect sizes | page · repo |
| Browser Mini-Games | 10 playable mini-games in self-contained SVG — 7 teaching analytics (A/B to p<0.05, funnel drop-off, cohort catch, retention day), 3 arcade | page · repo · demo |
| Causal / Uplift | CUPED cut the standard error by 26%: same power on 5.6k users per arm instead of 10k. Uplift models recover heterogeneous treatment effects | page · repo |
| Project | What it shows | Links |
|---|---|---|
| SQL Analytics Case Study | 26 SQL cases — 25 on a synthetic dataset (~183k events) plus 1 real-data on UCI Online Retail II: funnel, retention, LTV, attribution | page · repo · demo |
| RFM Segmentation | Split a bank's client base into 4 RFM segments and showed a small high-value share drives most of the revenue; marketing moved off mass campaigns | page · repo · demo |
| Cohort Analysis | Triangular retention and LTV cohort matrix on synthetic data: ARPU and LTV per cohort corrected for observation age, with export | page · repo · demo |
| Churn Prediction | Leakage-free model: recall@top-10% = 0.53, lift 3.07× at ROC-AUC 0.904 on a chronological split — no future-activity leakage | page · repo |
| Python Analytics Playground | Modular Python toolkit — load, clean, EDA, visualise — assembled into one pipeline with pytest coverage ≥ 80% | page · repo |
| Sales Calls Analytics | Streamlit dashboard over 16,891 synthetic AI-call records: 4-step funnel (greeting → offer → meeting → qualification) and loss analysis | page · repo |
| Project | What it shows | Links |
|---|---|---|
| Supabase Product Analytics | Full-stack analytics on Supabase: A/B gave +5.1 pp (p = 0.0034, chi-square); a Streamlit dashboard reads live data through Row-Level Security | page · repo |
| TaskFlow × PostHog | A SaaS product instrumented end-to-end with PostHog: typed event catalogue, generated traffic and 7 analyses — funnel, retention, paths | page · repo |
| Streamlit Dashboard | Product dashboard on a synthetic SaaS dataset (8,000 users): AARRR funnel, cohort retention, revenue — MRR, ARPU, churn | page · repo |
| Project | What it shows | Links |
|---|---|---|
| Reporting Automation Bot | Telegram bot replaced manual weekly reporting with cron: 1–2 hours of manual work became a scheduled report with KPI tables | page · demo |
| Scrolly English Speaking | Scrollytelling guide to spoken English for work conversations (A2–B1): MDX narrative with D3 visualisations | page · repo |
| Digital Garden | Personal Zettelkasten published as a Quartz v4 site: linked notes, backlinks and a graph instead of a chronological feed | page · repo |
| This Portfolio Site | The site you are reading: Astro 7, TypeScript, Markdown collections, static build, dark/light theme, RSS, sitemap, JSON-LD, Pages deploy | page · repo |
Board order is set by PROJECT_ORDER in src/lib/projects.ts; featured cases by HEADLINE_PROJECTS in the same file. Every metric above is authored in the project's Markdown file and cross-checked against src/lib/metrics.ts.
| Layer | Choice |
|---|---|
| Framework | Astro 7 (output: "static", no base — user Pages site served from the domain root) |
| Language | TypeScript 5.9 (strict), Astro components |
| Content | Markdown collections with Zod schemas (src/content.config.ts, Astro Content Layer) |
| Styling | Hand-written CSS custom properties (src/styles/global.css), 3 themes: dark (default), light, cyberpunk |
| Toolchain | Bun — install, scripts, tests (bun:test), lockfile |
| Analytics | PostHog (posthog-js), inert when the build-time key is unset; optional Plausible |
| Hosting | GitHub Pages via GitHub Actions |
| Charting | Native SVG chart components + src/lib/chart-svg.ts (no chart library) |
No client framework ships: interactive bits (search, graph, theme, filters, analytics) are small vanilla modules, and posthog-js loads lazily after the first interaction.
- Bun — the only package manager used here (
bun.lockis the lockfile) - Python 3 for
make check(scripts/check_site.py) rsvg-convert(librsvg) — only for regenerating OG images- Optional:
GITHUB_TOKEN/GH_TOKENfor the GitHub sync scripts (higher rate limit, sees private repos)
bun install
bun run dev # http://localhost:4321
bun run build # static output → dist/
bun run preview # serve the production buildRun this triad after every content or component change. All green = safe to commit.
bun run build # Astro build
bun run check # astro check + tsc --noEmit on tests
make check # astro check + tests + test:monitoring + test:built + check_site.py — validates the built dist/
bun run coverage # same suite with coveragemake check runs bun run check (Astro/TS type-check), the test suite (test, then test:monitoring and test:built), and then inspects the built output, failing on:
- missing required pages
- internal links that do not resolve to a file in
dist/ - missing images referenced from HTML
- the profile photo exceeding 500 KB
- required assets absent from
index.html - metrics drift — headline numbers in
src/lib/metrics.tsvs. the site (check_metrics_drift)
Lighthouse CI runs separately — see CI/CD.
├── astro.config.mjs # site, i18n, redirects, sitemap
├── src/
│ ├── content.config.ts # Zod schemas for all 6 collections
│ ├── content/ # Markdown: projects(-en), posts(-en), volta-parts(-en)
│ ├── layouts/ # Base.astro (nav, theme, meta, PostHog), Post.astro
│ ├── components/ # 27 components + 7 chart primitives — cards, filters, graph, hero
│ ├── lib/ # pure logic: metrics, topics, graph, fuzzy, charts, brand, path
│ ├── pages/ # 24 routes + endpoints (rss, sitemap, robots, llms.txt, search-index.json)
│ ├── data/ # chart data + generated github-activity.json
│ └── styles/ # global.css (tokens + themes), blog.css
├── scripts/ # OG/CV generators, GitHub sync, check_site.py, content-drift-audit, mobile audits
├── tests/lib/ # 25 bun:test suites for src/lib/*
├── docs/ # PRDs, ADRs, SPEC, analytics review ritual
├── monitoring/ # local RED monitoring stack (Prometheus + Grafana)
└── public/ # images, hero SVGs, OG images, fonts, CV PDF, demos, games
src/lib/ holds all non-trivial logic and is the only part covered by unit tests. Components stay presentational.
| Module | Responsibility |
|---|---|
metrics.ts |
Single source of truth for headline numbers (hero, career snapshot, value page, CV cross-check) |
projects.ts |
PROJECT_ORDER, project loading, sorting |
topics.ts |
TOPICS taxonomy + level grouping — drives /topics/ |
posts.ts |
Post loading, PER_PAGE pagination |
graph.ts / graph-layout.ts / graph-data.ts / graph-tooltip.ts / graph-zoom.ts / graph-url.ts |
Build-time knowledge graph: greedy-modularity communities, force layout, interaction |
charts.ts / chart-svg.ts |
Chart config and the SVG primitives the chart components render |
fuzzy.ts |
Dependency-free Levenshtein ≤ 2 for search (Cyrillic-safe) |
analytics.ts / beacon.ts / scroll-depth.ts |
Client analytics: typed track(), RED beacon, read-depth |
brand.ts |
Palette tokens + WCAG contrast assertion (shared with the OG renderer) |
path.ts |
withBase() — use it for every internal link and asset path |
llms-txt.ts |
Build-time llms.txt generator, shared by /llms.txt and /.well-known/llms.txt so the two cannot drift |
qa-corpus.ts |
Curated bilingual Q&A corpus behind the "Ask me" widget (AskMe.astro) |
Six collections, schemas in src/content.config.ts:
| Collection | Files | Language |
|---|---|---|
projects |
17 | RU |
projects-en |
17 | EN |
volta-parts |
23 | RU |
volta-parts-en |
23 | EN |
posts |
24 | RU |
posts-en |
2 | EN |
- Create
src/content/projects/<id>.mdandsrc/content/projects-en/<id>.md— always both. - Add a hero SVG to
public/images/(theherofield is required). - Set
track(experiments|analytics|product|engineering) for the kanban board. - Add the slug to
PROJECT_ORDERinsrc/lib/projects.ts. bun run build && make check.bun run sync:gh:applyto setupdated:from the repo's last push.
Create src/content/posts/<slug>.md. category must be one of decision-log, framework, guide, note — a local hook (portfolio-category-guard.js) blocks anything else. Add an EN mirror in posts-en/ when it should be bilingual.
- Project page skeleton — STAR (H2 order):
Ситуация → Задача → Действия → Результат → Ограничения → Документация. EN:Situation → Task → Actions → Result → Limitations → Documentation. Action should be the longest section (50–60% of the body);Задачаis 1–2 sentences of first-person goal and carries no digits. Volta parts addРекомендации/Recommendations; thevoltahub keeps its own D10 narrative. Bound bytests/lib/content-skeleton.test.ts. - Descriptions: 1–2 sentences, result + number first, 120–200 chars; the first 72 chars must stand alone (
MaterialStriptruncates there). Author RU and EN independently — meaning parity, not literal translation. - Numbers are frozen. A readability rewrite never changes a metric.
bun run audit:contentdiffs every numeric token insrc/content/**againstdocs/content-baseline.jsonand exits 1 on any change. Accept an intentional change withbun run audit:content:snapshot. related:is locale-neutral — write/projects/<slug>/and/posts/<slug>/in both languages; the EN resolver prefixesen/itself. Never write/en/projects/....- Volta hub map is generated. The grouped map in
projects/volta.mdbetween<!-- volta-map:start -->/<!-- volta-map:end -->comes frombun run volta:map. Re-run after adding or renaming a part. - Fold one-off sections (
Architecture,Run,Testing, …) underДействия/Actions.
- RU is the default locale; EN mirrors live in
src/pages/en/and render the-encollections. - Every
Basepage passeslangandcounterpartHref;LangSwitch.astrotoggles and emits<link rel="alternate" hreflang>. - When adding a page, ship both languages and wire
counterpartHrefon both sides. - RU-only on purpose — no EN mirror, no hreflang:
notes,404.
Old routes collapse into the /about anchor cluster (astro.config.mjs): /whois/, /work-with-me/, /now/, /start/ → /about/#… (with /en/ twins for the first three); /guides/ → /notes/guides/; /cv/ → the CV PDF.
src/components/Analytics.astro renders the PostHog snippet. It is inert unless the build-time env vars are set — all of them are PUBLIC_*, so they bake into the static build:
| Variable | Purpose |
|---|---|
PUBLIC_POSTHOG_KEY |
Project token — no snippet rendered when unset |
PUBLIC_POSTHOG_HOST |
Defaults to https://us.i.posthog.com |
PUBLIC_PLAUSIBLE_DOMAIN |
Optional — Plausible script, for a privacy-friendly counter |
PUBLIC_PLAUSIBLE_SRC |
Optional — custom or self-hosted Plausible script URL |
PUBLIC_BEACON_ENDPOINT |
Optional — pushes scroll-depth / RED beacon events to the monitoring stack (BeaconMetrics.astro) |
Copy .env.example to .env for local development; for CI the same names go into GitHub Actions secrets.
src/lib/analytics.ts is the only client module that talks to PostHog besides Analytics.astro (which owns init). Components call track(); they never touch the global. posthog-js is lazy-loaded after the first user interaction, so consumer chunks stay off the critical path, and track() buffers (bounded, flushed on attach) rather than dropping early events.
Typed events (AnalyticsEventMap): project_viewed, post_read, lang_switched, theme_change, ask_me_used, random_post_click, section_viewed, outbound_click, search_used, search_no_results, read_depth, filter_applied, projects_track_filter. Super properties: locale, theme, prefers_reduced_motion, initial_referrer_class, landing_path — the first-touch class is computed once per visitor and persisted.
Convention: any element carrying data-analytics="<name>" fires <name> on click through a delegated listener in Analytics.astro (featured_project, cv_download_pdf, bento_*, headline_all_projects, …). New CTAs should reuse that attribute instead of calling track() for the same click — mix the two and a click is counted twice.
bun run og # per-post OG cards (24 PNG + webp)
bun run og:home # homepage identity banner
bun run og:graph # knowledge-graph preview
bun run og:cv # CV OG / LinkedIn / cover variants (og:cv:li, og:cv:cover)
bun run og:watch # regenerate on changeRendering goes through rsvg-convert with vendored fonts in scripts/og-fonts/. scripts/lib/og-render.mjs sets both FONTCONFIG_FILE and PANGOCAIRO_BACKEND=fc — macOS defaults to the CoreText backend, which ignores fontconfig and silently renders the wrong font. Every generated banner passes a WCAG AA contrast assertion before it is written. Palette lives in src/lib/brand.ts, shared with global.css. scripts/audit-palette-coverage.mjs reports where palette tokens are used (it has no npm script — run it directly with bun).
The CV is not authored here. It lives in a separate rendercv project at ../cv/ (Boyarkin_Nikita_Product_Analyst_CV.yaml). The portfolio ships one downloadable PDF, public/CV-Nikita-Boyarkin.pdf, behind a single "CV" button on every page.
# in ../cv/
rendercv render Boyarkin_Nikita_Product_Analyst_CV.yaml
# here
bun run cv:pdf # copies into public/ — override the source dir with CV_SOURCE_DIRHeadline numbers must agree with src/lib/metrics.ts. Commit the PDF; do not reintroduce a hand-authored /cv/ page.
scripts/sync-github-projects.mjs keeps updated: and private: frontmatter in sync with the GitHub API for every repo referenced by github: (RU + EN twins). It never touches date:, which is the authored publication date.
bun run sync:gh # check mode — reports drift, exit 1 on hard drift (runs in CI)
bun run sync:gh:apply # write updated:/private: back into frontmatter
bun run sync:gh --dry-run # preview
bun run sync:gh:candidates # public, non-fork repos not yet featuredscripts/sync-github-activity.mjs writes src/data/github-activity.json (contributions, streaks, 5 chart cards) for the /about charts. Auth via GITHUB_TOKEN / GH_TOKEN / STREAK_PAT. Both run on schedules — see CI/CD.
monitoring/ holds a self-contained RED stack (Rate / Errors / Duration): a Bun exporter with Prometheus exposition + embedded SQLite, Prometheus, Grafana and a Cloudflare tunnel. Runs locally via Docker. See monitoring/README.md — it needs a PostHog personal API key, not the public project token.
Three workflows in .github/workflows/:
| Workflow | Trigger | Does |
|---|---|---|
deploy.yml |
push / PR to master | main |
install → GitHub drift gate (sync:gh, exits 1) → bun run check → bun run test → build → make check (tests + test:monitoring + test:built + check_site.py) → Lighthouse CI → deploy dist/ to Pages |
sync-github.yml |
weekly | runs sync:gh:apply and opens a PR with the changes |
github-activity.yml |
daily | refreshes github-activity.json; commits only when the payload changed |
Pull requests run every gate except deploy. Lighthouse runs in a separate job with no analytics env vars, so the scores stay clean.
Lighthouse assertions (.lighthouserc.json, 7 URLs): accessibility, best-practices and SEO are errors at ≥ 0.95; performance is a warning at ≥ 0.85.
Do not push to master before confirming Pages is set to Settings → Pages → Build and deployment → GitHub Actions.
.claude/hooks/ — contrast-gate.js (colour-contrast gate on generated output) and portfolio-category-guard.js (blocks post frontmatter with an out-of-taxonomy category).
bun run verify:writing-filter is a Playwright regression check for the /writing/ filter. It runs against a served build (bun run serve-dist first, or point BASE_URL anywhere) and guards the CSS-cascade bug fixed in src/styles/blog.css, where an author display beat the UA [hidden] { display: none } rule and the list stayed visible after a filter click.
| Path | Contents |
|---|---|
CLAUDE.md |
Full repository guide — schemas, conventions, command reference |
CONTEXT.md |
Domain glossary for the conversion surface (RU) |
docs/prd-v8.md |
Current PRD (RU) — replaces prd-v7.md |
docs/cta-inventory.md |
Every CTA, its event and destination |
docs/analytics-events.md |
Event catalogue with payloads |
docs/analytics-review.md |
Weekly 15-minute analytics ritual + funnel benchmark |
docs/contact-log.md |
Manual hiring funnel log |
docs/backlog.md |
Open work |
docs/adr/ |
Architecture decision records |
DESIGN.md |
Visual system notes |
Older PRDs (
prd-v2…prd-v7) are kept for history. Preferdocs/prd-v8.mdandCLAUDE.mdwhen they disagree.
See LICENSE.txt. Site content and copy are personal — please don't reuse the text or profile assets.
