Personal portfolio website built with Vue 3 + Vite, focused on modern UI/UX, responsive layout, and clean component architecture.
- Vue 3 (Composition API)
- Vite 5
- Vue Router
- Pinia
- SCSS
- Swiper
- Firebase Hosting (frontend deploy)
- Cloudflare Worker (recommended contact proxy)
- Multi-page portfolio: Home, About, Resume, Portfolio, Blog, Contact, Game
- Responsive desktop/mobile layout
- Portfolio modal gallery
- Blog filters/sorting/search
- Weather + clock widgets in header
- JSON-driven content (
public/data/site-content.json)
- Node.js 18+
- pnpm
pnpm installpnpm devpnpm buildpnpm previewpnpm test
pnpm test:watchCreate .env in project root:
VITE_OPENWEATHER_API_KEY=
VITE_CONTACT_API_URL=/contact
VITE_SITE_URL=https://mykolamud.pp.ua
VITE_DEFAULT_OG_IMAGE_PATH=/images/avatar-logo.webp
VITE_DEFAULT_OG_IMAGE_ALT=Mykola Mud front-end engineer portfolio social preview image
VITE_DEFAULT_OG_IMAGE_WIDTH=1200
VITE_DEFAULT_OG_IMAGE_HEIGHT=630
# Optional remote endpoint overrides (defaults are built in)
VITE_GITHUB_API_BASE_URL=
VITE_BLOG_POSTS_URL=
VITE_PORTFOLIO_PROJECTS_URL=
VITE_CLICK_SOUND_GITHUB_CONTENT_URL=
VITE_OPENWEATHER_API_BASE_URL=
VITE_OPENWEATHER_ICON_BASE_URL=Important:
VITE_*values are embedded at build time.- Do not store real secrets in frontend env for production.
- Prefer server-side secret handling via Cloudflare Worker.
VITE_CONTACT_API_URLdefaults to/contact(same domain Worker API).VITE_SITE_URLis used for absolute canonical/OG URLs in runtime SEO metadata and for generatedsitemap.xml/robots.txt.- Production value for this project should be
https://mykolamud.pp.ua(not theworkers.devsubdomain). VITE_DEFAULT_OG_IMAGE_PATHsets a default social preview image path for SEO metadata.VITE_DEFAULT_OG_IMAGE_ALT,VITE_DEFAULT_OG_IMAGE_WIDTH,VITE_DEFAULT_OG_IMAGE_HEIGHTcontrol default social image metadata.- External content/API hosts can be overridden via optional
VITE_*endpoint vars; sane defaults are defined insrc/config/remoteEndpoints.js.
- Default social preview image is configured globally via
VITE_DEFAULT_OG_IMAGE_PATH(used for both Open Graph and Twitter). - Route-specific override is supported in router SEO config via
meta.seo.ogImage. - Optional route-specific social image metadata overrides:
meta.seo.ogImageAltmeta.seo.ogImageWidthmeta.seo.ogImageHeight
Recommended image spec (Open Graph / Twitter large card):
- Size:
1200x630 - Format:
webporjpg - Keep key text/logo inside a centered safe area (avoid edges for platform crops)
- Use clear contrast and minimal text for better share previews
Release runbook:
RELEASE_CHECKLIST.mdnpm run buildalso generates SEO crawl assets indist/:- route HTML snapshots for core SEO pages (
/about,/resume,/portfolio,/blog,/contact) with route-specific<head>metadata dist/sitemap.xmldist/robots.txt
- route HTML snapshots for core SEO pages (
pnpm build
firebase deploy --only hostingCurrent Firebase config:
- Hosting public dir:
dist - SPA rewrite:
** -> /index.html - Project alias from
.firebaserc
Worker serves built frontend (dist) and handles contact submissions via POST /contact.
SEO prerender strategy (compatible with current SPA + Worker flow):
- Build step generates route-specific HTML snapshots for core marketing routes.
- Each snapshot keeps the same app bundle but includes route-specific
title,description, canonical, OG/Twitter tags, and JSON-LD in<head>. - This improves crawler entry-page metadata without migrating the project to SSR.
- Set
VITE_SITE_URLin CI for correct absolute canonical/OG URLs in prerendered snapshots and sitemap.
High-level flow:
- Worker serves SPA on
https://<worker>.workers.dev/ - Frontend sends form data to Worker endpoint (
/contact) - Worker validates payload and applies CORS policy
- Worker reads secrets from environment
- Worker sends message to Telegram API
- Worker returns normalized JSON response
Suggested secrets in Worker:
TELEGRAM_BOT_TOKENTELEGRAM_CHAT_ID
Frontend now calls /contact by default.
Content lives locally under public/data/ and is served same-origin:
public/data/site-content.json— text, cards, testimonials, resume titles/overview/skillspublic/data/resume.json— experience & education timeline (experienceItems[],educationItems[])public/data/portfolio.json— portfolio projects (portfolioCard[]); images inpublic/images/public/data/blog-posts.json— blog posts (posts[]); images inpublic/images/post/
Experience groups positions by company: give several entries the same institution
value to render them as one company with a nested career-growth timeline.
Update these files to refresh content without changing component logic. Changes require a redeploy of the static files (no full rebuild needed — they are not bundled).
Portfolio/blog can still be pointed back to a remote source via optional VITE_PORTFOLIO_PROJECTS_URL / VITE_BLOG_POSTS_URL.
src/
components/
about/
blog/
contact/
home-content/
navigation/
portfolio/
resume/
composables/
layout/
services/
views/
public/
data/site-content.json
firebase.json
Use:
npx firebase-tools deploy --only hostingor install globally:
npm i -g firebase-toolsVITE_* vars are compiled into the build. Rebuild before deploy:
pnpm build
firebase deploy --only hostingIf contact form uses Telegram token in frontend, token is exposed publicly. Move to Worker proxy for production.
- Track tasks in Linear and GitHub Project backlog.
- Start task: set
In Progress. - After implementation + confirmation: set
Done. - New idea from discussion: create issue and put in
Backlog.
Added workflow:
.github/workflows/deploy-worker.yml
It deploys Worker automatically on every push/merge to main.
In repo settings add:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_ID
worker/wrangler.tomlworker/src/index.js
Set once with Wrangler (local):
cd worker
npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put TELEGRAM_CHAT_IDOptional CORS origin is in worker/wrangler.toml:
ALLOWED_ORIGIN(set your real domain instead of*for production)
cd worker
npx wrangler deploy