My personal website: my work, blog and CV, plus a chat that answers questions about my experience using only the site's content and my CV. Built with Next.js and hosted on Vercel.
npm install
npm run devOpen http://localhost:3000.
npm run lintnpm testbuilds the site if needed, runs the e2e tests against it in headless Chrome, then the unit tests
GitHub Actions runs lint and npm test on every pull request and push to main.
The site is hosted on Vercel (Hobby plan).
- Every push to
maindeploys to production at https://tany4.com - Every pull request gets a preview deployment
- Environment variables are set in the Vercel project:
OPENAI_API_KEY(chat) andNEXT_PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN(production only), plus the ones the weekly email needs (see Analytics) - A Vercel Firewall rule rate-limits
/api/chatto 10 requests per minute per IP
- Edit CV content
- Update
src/cv/cv.ts(single source of truth)
- Update
- View CV on web
http://localhost:3000/cv
- Regenerate PDF
npm run cv:pdf- Output:
public/TaniaPapazafeiropoulou-CV.pdf
The PDF is generated using @react-pdf/renderer from src/cv/pdf/CvPdfDocument.tsx. No browser or Playwright required.
All cookieless, so no consent banner is needed:
- Vercel Web Analytics (
@vercel/analytics): visitors, pages, referrers, countries, devices. Vercel project → Analytics. Free up to 50,000 events a month, 30 days of history.- Clicks on external links, email links and file downloads are recorded as page views under
/out/<site>(e.g./out/linkedin.com,/out/email) and/download/<file>(e.g./download/TaniaPapazafeiropoulou-CV.pdf), because custom events need a paid plan. They add to the page-view total.
- Clicks on external links, email links and file downloads are recorded as page views under
- Vercel Speed Insights (
@vercel/speed-insights): real-visitor performance score. Vercel project → Speed Insights. - Cloudflare Web Analytics: loaded only when
NEXT_PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKENis set (production only). Cloudflare dashboard → Web Analytics. 6 months of history.
Every Monday around 05:00 UTC, Vercel Cron calls /api/cron/weekly-analytics, which emails last week's numbers (Monday to Sunday, UTC) to hello@tany4.com. It sends over SMTP from an existing mailbox: Gmail with an app password by default. It includes visitors and page views against the week before, top pages, referrers, countries and link clicks from Vercel, plus Cloudflare's visits, page views and median page load time. It flags days with unusual spikes and runs of days with no traffic.
Vercel project environment variables (Production):
CRON_SECRET: random string. Vercel sends it to the endpoint, and requests without it are rejected.VERCEL_ANALYTICS_TOKEN: Vercel access token scoped to the personal account, used to read Web AnalyticsSMTP_USER: the mailbox that sends the email, e.g. the Gmail addressSMTP_PASSWORD: that mailbox's app password (Google Account → Security → 2-Step Verification → App passwords)SMTP_HOST(optional): defaults tosmtp.gmail.com; e.g.smtp.zoho.comfor a Zoho plan with SMTP accessCLOUDFLARE_API_TOKEN: Cloudflare API token with Account Analytics ReadCLOUDFLARE_ACCOUNT_ID: Cloudflare account ID
If a data source fails, the email still goes out and says what couldn't be loaded. To send a test email for the last 7 days, trigger the cron job from the Vercel dashboard, or call the endpoint with the CRON_SECRET header and ?range=last-7-days.
To check the real numbers without sending an email, run the preview job. It builds the email for last week and for the last 7 days and writes both to the function logs. It is registered as a cron job (once a year, on 1 January) only so it can be triggered on demand:
vercel crons run /api/cron/weekly-analytics-preview
vercel logs --environment production --since 5m