Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 20 additions & 31 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,30 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" type="image/png" />
<link rel="preconnect" href="https://www.googletagmanager.com" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Mitya Kurs is a senior full-stack developer and team lead with 14 years of experience shipping products, leading teams, and building scalable systems."
/>
<meta name="robots" content="index,follow" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Mitya Kurs Portfolio" />
<meta
property="og:title"
content="Mitya Kurs | Senior Full-Stack Developer & Team Lead"
/>
<link rel="icon" href="/favicon.png" sizes="48x48" type="image/png" />
<link rel="icon" href="/favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="/icon-192.png" sizes="192x192" type="image/png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta
property="og:description"
content="Mitya Kurs is a senior full-stack developer and team lead with 14 years of experience shipping products, leading teams, and building scalable systems."
name="theme-color"
content="#f3f1e9"
media="(prefers-color-scheme: light)"
/>
<meta property="og:url" content="https://profile.mitya.dev/" />
<meta property="og:image" content="https://profile.mitya.dev/favicon.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Mitya Kurs | Senior Full-Stack Developer & Team Lead"
name="theme-color"
content="#27272a"
media="(prefers-color-scheme: dark)"
/>
<meta
name="twitter:description"
content="Mitya Kurs is a senior full-stack developer and team lead with 14 years of experience shipping products, leading teams, and building scalable systems."
/>
<meta
name="twitter:image"
content="https://profile.mitya.dev/favicon.png"
/>
<title>Mitya Kurs | Senior Full-Stack Developer & Team Lead</title>
<link rel="preconnect" href="https://www.googletagmanager.com" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--
SEO meta (title, description, robots, canonical, Open Graph, Twitter) is owned
solely by SeoHead (react-helmet-async) and baked into each prerendered route.
Do NOT duplicate it here — static copies collide with the per-route tags and
produce conflicting og:image, og:title and title tags in the prerendered HTML.
-->
<script>
(function () {
var t;
Expand Down
22 changes: 21 additions & 1 deletion nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,32 @@ server {
add_header Cache-Control "public, immutable";
}

# Serve the CV inline (open in browser) rather than forcing a download.
location = /Mitya_Kurs.pdf {
default_type application/pdf;
add_header Content-Disposition "inline; filename=\"Mitya_Kurs.pdf\"";
expires 1d;
add_header Cache-Control "public, must-revalidate";
}

# Social share cards + icons: long-lived but revalidated so refreshed art propagates.
location ~* ^/(og-image|cv-og|cv-thumb|profile|apple-touch-icon|icon-192|icon-512|favicon-32|favicon)\.png$ {
expires 7d;
add_header Cache-Control "public, must-revalidate";
}

location = /site.webmanifest {
default_type application/manifest+json;
expires 7d;
add_header Cache-Control "public, must-revalidate";
}

location = /index.html {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}

location / {
try_files $uri $uri/ /index.html;
try_files $uri $uri/index.html /index.html;
}
}
Binary file added public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/cv-og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/cv-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Mitya Kurs Portfolio",
"short_name": "Mitya Kurs",
"description": "Mitya Kurs — Senior Engineering Leader & Hands-On Builder. 14 years shipping production systems.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#f3f1e9",
"theme_color": "#f3f1e9",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
12 changes: 8 additions & 4 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://profile.mitya.dev/</loc>
<lastmod>2026-04-19</lastmod>
<lastmod>2026-06-11</lastmod>
</url>
<url>
<loc>https://profile.mitya.dev/cv</loc>
<lastmod>2026-06-11</lastmod>
</url>
<url>
<loc>https://profile.mitya.dev/theBuzz/about</loc>
<lastmod>2026-04-19</lastmod>
<lastmod>2026-06-11</lastmod>
</url>
<url>
<loc>https://profile.mitya.dev/theBuzz/toolbox</loc>
<lastmod>2026-04-19</lastmod>
<lastmod>2026-06-11</lastmod>
</url>
<url>
<loc>https://profile.mitya.dev/theBuzz/experience</loc>
<lastmod>2026-04-19</lastmod>
<lastmod>2026-06-11</lastmod>
</url>
</urlset>
164 changes: 164 additions & 0 deletions scripts/og-image.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* One-off, author-time generator for the social share cards.
*
* Renders 1200x630 branded cards (og-image.png + cv-og.png) to /public using the
* puppeteer/Chrome already pulled in by react-snap. NOT part of the deploy build, so
* CI/Docker stay Chrome-free. Re-run manually after editing the design or the avatar:
*
* node scripts/og-image.cjs
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const puppeteer = require("puppeteer");

function findChrome() {
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
return process.env.PUPPETEER_EXECUTABLE_PATH;
}
const candidates = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/google-chrome",
"/usr/bin/chromium-browser",
"/usr/bin/chromium",
];
for (const p of candidates) {
try {
execSync(`test -x "${p}"`, { stdio: "ignore" });
return p;
} catch (_) {}
}
return undefined;
}

const root = path.resolve(__dirname, "..");
const publicDir = path.join(root, "public");
const avatarPath = path.join(root, "src/assets/profile/avatar.webp");
const avatarDataUri =
"data:image/webp;base64," + fs.readFileSync(avatarPath).toString("base64");

// Embed Poppins (from @fontsource) so rendering is deterministic and offline.
const fontDir = path.join(root, "node_modules/@fontsource/poppins/files");
const fontFace = (weight) => {
const data = fs
.readFileSync(path.join(fontDir, `poppins-latin-${weight}-normal.woff2`))
.toString("base64");
return `@font-face{font-family:'Poppins';font-style:normal;font-weight:${weight};font-display:block;src:url(data:font/woff2;base64,${data}) format('woff2');}`;
};
const fontFaces = [400, 500, 600, 700].map(fontFace).join("");

// Brand tokens mirrored from src/index.css (light theme).
const SURFACE = "#f3f1e9";
const TEXT = "#333332";
const MUTED = "#7c7a73";
const ACCENT = "#646cff";
const SURROUND = "#4c4b4c";

function card({ eyebrow, title, subtitle, tags }) {
return `<!doctype html><html><head><meta charset="utf-8" />
<style>
${fontFaces}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1200px; height: 630px; }
body {
font-family: 'Poppins', Helvetica, Arial, sans-serif;
background: ${SURFACE};
color: ${TEXT};
display: flex;
align-items: center;
gap: 72px;
padding: 90px 96px;
position: relative;
overflow: hidden;
}
body::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 14px;
background: ${ACCENT};
}
.photo {
width: 360px; height: 360px;
flex: 0 0 360px;
border-radius: 32px;
object-fit: cover;
box-shadow: 0 24px 60px -18px rgba(0,0,0,0.45);
}
.col { display: flex; flex-direction: column; min-width: 0; }
.eyebrow {
font-size: 30px; font-weight: 600; letter-spacing: 6px;
text-transform: uppercase; color: ${ACCENT}; margin-bottom: 18px;
}
.title { font-size: 92px; font-weight: 700; line-height: 1.02; }
.subtitle {
font-size: 44px; font-weight: 500; color: ${SURROUND};
margin-top: 14px;
}
.tags {
font-size: 30px; font-weight: 500; color: ${MUTED};
margin-top: 34px; letter-spacing: 1px;
}
.url {
font-size: 30px; font-weight: 600; color: ${ACCENT};
margin-top: 40px;
}
</style></head>
<body>
<img class="photo" src="${avatarDataUri}" alt="" />
<div class="col">
${eyebrow ? `<div class="eyebrow">${eyebrow}</div>` : ""}
<div class="title">${title}</div>
<div class="subtitle">${subtitle}</div>
${tags ? `<div class="tags">${tags}</div>` : ""}
<div class="url">profile.mitya.dev</div>
</div>
</body></html>`;
}

const cards = [
{
file: "og-image.png",
html: card({
title: "Mitya Kurs",
subtitle: "Senior Engineering Leader & Hands-On Builder",
tags: "React · Node.js · AWS · AI",
}),
},
{
file: "cv-og.png",
html: card({
eyebrow: "Curriculum Vitae",
title: "Mitya Kurs",
subtitle: "Senior Engineering Leader",
tags: "14 years shipping production systems",
}),
},
];

(async () => {
const executablePath = findChrome();
const browser = await puppeteer.launch({
headless: "new",
executablePath,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 });

for (const { file, html } of cards) {
await page.setContent(html, { waitUntil: "load" });
await page.evaluateHandle("document.fonts.ready");
// Give embedded fonts/image a beat to paint before snapshotting.
await new Promise((r) => setTimeout(r, 300));
const out = path.join(publicDir, file);
await page.screenshot({ path: out, type: "png" });
console.log("wrote", out);
}

await browser.close();
})().catch((err) => {
console.error(err);
process.exit(1);
});
8 changes: 7 additions & 1 deletion scripts/prerender.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ const puppeteerExecutablePath = findChrome();

run({
source: "dist",
include: ["/", "/theBuzz/about", "/theBuzz/experience", "/theBuzz/toolbox"],
include: [
"/",
"/cv",
"/theBuzz/about",
"/theBuzz/experience",
"/theBuzz/toolbox",
],
waitFor: 2000,
puppeteerArgs: ["--no-sandbox", "--disable-setuid-sandbox"],
...(puppeteerExecutablePath ? { puppeteerExecutablePath } : {}),
Expand Down
8 changes: 8 additions & 0 deletions src/components/seo/seoHead.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Helmet } from "react-helmet-async";
import {
DEFAULT_OG_IMAGE,
DEFAULT_OG_IMAGE_ALT,
DEFAULT_TWITTER_CARD,
SITE_NAME,
} from "../../seo/config";
Expand All @@ -15,6 +16,7 @@ export const SeoHead: React.FC<SeoHeadProps> = ({ meta, jsonLd }) => {
const twitterCard = meta.twitterCard ?? DEFAULT_TWITTER_CARD;
const robots = meta.indexable ? "index,follow" : "noindex,nofollow";
const image = meta.ogImage ?? DEFAULT_OG_IMAGE;
const imageAlt = meta.ogImageAlt ?? DEFAULT_OG_IMAGE_ALT;
const jsonLdBlocks = Array.isArray(jsonLd) ? jsonLd : jsonLd ? [jsonLd] : [];

return (
Expand All @@ -26,15 +28,21 @@ export const SeoHead: React.FC<SeoHeadProps> = ({ meta, jsonLd }) => {

<meta property="og:type" content="website" />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content={meta.title} />
<meta property="og:description" content={meta.description} />
<meta property="og:url" content={meta.canonical} />
<meta property="og:image" content={image} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:alt" content={imageAlt} />

<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={meta.title} />
<meta name="twitter:description" content={meta.description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={imageAlt} />

{jsonLdBlocks.map((schema, index) => (
<script key={`${meta.path}-schema-${index}`} type="application/ld+json">
Expand Down
Loading
Loading