Resume site with live content from Gitea
Replaces the static portfolio with a resume site that reads /data/resume.json: the text comes from data/resume.json in this repo, and the projects are the public Gitea repos tagged `resume`. Merges in the content of the hh.ru resume (backend/DevOps skills, experience, languages) without personal details. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,31 @@
|
||||
# Emil Shanayev — Portfolio
|
||||
# Emil Shanayev — resume site
|
||||
|
||||
Updated static portfolio based on the original `Emil_Shanayev_Resume` repository.
|
||||
Live at **https://shanaty.ru/resume/**. A static Next.js site whose content is loaded live from Gitea:
|
||||
|
||||
## Local development
|
||||
- **Text** (name, bio, skills, experience, education, contacts, …) comes from [`data/resume.json`](data/resume.json) in this repo.
|
||||
- **Projects** are the public repos on Gitea (`git.shanaty.ru/Emil_Shanaty`) that carry the topic **`resume`**. Tag a repo and it appears within about a minute; remove the topic and it disappears.
|
||||
- Optional `projectNotes` in `data/resume.json` (keyed by repo name) enrich a project with a title, kind, longer description, highlights, tech tags, extra links and a `rank` (lower ranks first; the rest are newest first). Without a note a project shows its Gitea description, language and topics.
|
||||
|
||||
## How it works
|
||||
|
||||
A systemd timer on the server (`resume_sync.py`, in the `auto` repo) rebuilds `https://shanaty.ru/data/resume.json` every minute from the `main` branch of this repo plus the topic search. The page fetches that file on load and falls back to the copy bundled at build time if it is unreachable. A broken `data/resume.json` (invalid JSON, missing fields, non-http links) is rejected and the previous published data stays.
|
||||
|
||||
To update content: edit `data/resume.json`, commit, and **push to Gitea** (`git push gitea main`). No rebuild or deploy is needed.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
npm run dev # http://localhost:3000, data from the public https://shanaty.ru/data/resume.json
|
||||
npm run build # static export in out/
|
||||
```
|
||||
|
||||
Open `http://localhost:3000`.
|
||||
`BASE_PATH` sets the URL prefix (default `/Emil_Shanayev_Resume`, for GitHub Pages) and `NEXT_PUBLIC_DATA_URL` the data URL.
|
||||
|
||||
## Production build
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
`./deploy-shanaty.sh` builds and publishes the site to shanaty.ru (needs the ssh key `~/.ssh/MyGitea`). Only needed after changing code or styles.
|
||||
|
||||
The static export is written to `out/`.
|
||||
## Not published on purpose
|
||||
|
||||
## GitHub Pages
|
||||
|
||||
The project is preconfigured for:
|
||||
|
||||
`https://emil28092005.github.io/Emil_Shanayev_Resume/`
|
||||
|
||||
Push the project to the `main` branch, then select **Settings → Pages → Source → GitHub Actions**. The included workflow builds and deploys the static export automatically.
|
||||
The official resume on hh.ru holds more personal details (phone, date of birth, salary expectations). This site only carries the professional content.
|
||||
|
||||
@@ -102,3 +102,43 @@ a:focus-visible { outline:2px solid var(--acid); outline-offset:5px; }
|
||||
.tool-grid { grid-template-columns:1fr; }.tool-grid a:nth-child(n) { border-right:0; padding:24px 0; }
|
||||
.education { grid-template-columns:1fr; gap:20px; padding-bottom:70px; }.contact-grid a { grid-template-columns:80px 1fr 20px; }.contact-grid strong { font-size:12px; overflow-wrap:anywhere; }.footer-bottom { gap:20px; }.footer-bottom span:last-child { text-align:right; }
|
||||
}
|
||||
|
||||
/* Resume site: skills, experience, live project list, side info. */
|
||||
.skill-grid { display:grid; grid-template-columns:repeat(3,1fr); }
|
||||
.skill-card { padding:34px 28px 34px 0; border-right:1px solid var(--line); border-bottom:1px solid var(--line); transition:background .25s; }
|
||||
.skill-card:nth-child(3n+2), .skill-card:nth-child(3n) { padding-left:28px; }
|
||||
.skill-card:nth-child(3n) { border-right:0; }
|
||||
.skill-card:hover { background:linear-gradient(135deg,rgba(199,255,70,.09),transparent 68%); }
|
||||
.skill-card h3 { font-size:22px; margin:30px 0 12px; letter-spacing:-.035em; }
|
||||
.skill-card p { margin:0; color:#b0bab2; font-size:14px; line-height:1.65; }
|
||||
.exp-item { display:grid; grid-template-columns:200px 1fr; gap:34px; padding:34px 0; border-bottom:1px solid var(--line); }
|
||||
.exp-item > span { font:300 11px/1.6 'DM Mono',monospace; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
|
||||
.exp-item h3 { margin:0 0 16px; font-size:22px; letter-spacing:-.03em; }
|
||||
.exp-item ul { margin:0; padding:0; list-style:none; display:grid; gap:9px; color:#b0bab2; line-height:1.6; }
|
||||
.exp-item li::before { content:'—'; color:var(--muted); margin-right:10px; }
|
||||
.status-note { margin:0; padding:44px 0; border-bottom:1px solid var(--line); color:var(--muted); font:400 12px/1.8 'DM Mono',monospace; }
|
||||
.status-note a { color:var(--acid); border-bottom:1px solid rgba(199,255,70,.4); }
|
||||
.status-note b { color:var(--ink); font-weight:500; }
|
||||
.project-updated { margin:20px 0 0; color:var(--muted); font:300 10px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.05em; }
|
||||
.about-grid { display:grid; grid-template-columns:1.25fr 1fr; gap:64px; padding-bottom:70px; }
|
||||
.about-grid .education-list { padding-bottom:0; }
|
||||
.side-info { padding-top:18px; }
|
||||
.mini-heading { margin:0 0 14px; padding-bottom:10px; border-bottom:1px solid rgba(243,246,240,.35); font:500 11px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.11em; }
|
||||
.side-info .mini-heading:not(:first-child) { margin-top:40px; }
|
||||
.lang-list { list-style:none; margin:0; padding:0; }
|
||||
.lang-list li { display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--line); }
|
||||
.lang-list b { font:400 11px 'DM Mono',monospace; color:var(--acid); font-weight:400; }
|
||||
.side-info .tag-list { margin-top:0; }
|
||||
.contact-note { color:#92958a; font-style:normal; font-size:12px; }
|
||||
@media (max-width:980px) {
|
||||
.skill-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.skill-card:nth-child(n) { padding:34px 24px 34px 0; border-right:1px solid var(--line); }
|
||||
.skill-card:nth-child(2n) { padding-left:24px; border-right:0; }
|
||||
.about-grid { grid-template-columns:1fr; gap:40px; }
|
||||
}
|
||||
@media (max-width:760px) {
|
||||
.skill-grid { grid-template-columns:1fr; }
|
||||
.skill-card:nth-child(n) { padding:30px 0; border-right:0; }
|
||||
.exp-item { grid-template-columns:1fr; gap:14px; padding:28px 0; }
|
||||
.about-grid { padding-bottom:50px; }
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Emil Shanayev — AI Systems & Scientific Software',
|
||||
description: 'Portfolio of Emil Shanayev: AI-native systems, scientific software, graphics and infrastructure.',
|
||||
title: 'Emil Shanayev — Resume',
|
||||
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
|
||||
authors: [{ name: 'Emil Shanayev' }],
|
||||
}
|
||||
|
||||
|
||||
+4
-126
@@ -1,129 +1,7 @@
|
||||
import { resume } from '@/data/resume'
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3 13 13 3M6 3h7v7" />
|
||||
</svg>
|
||||
)
|
||||
import resume from '@/data/resume.json'
|
||||
import type { Resume } from '@/lib/types'
|
||||
import Site from './site'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<div className="ambient ambient-one" aria-hidden="true" />
|
||||
<div className="ambient ambient-two" aria-hidden="true" />
|
||||
<header className="nav shell">
|
||||
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="#work">Work</a>
|
||||
<a href="#profile">Profile</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero shell" id="top">
|
||||
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
|
||||
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
|
||||
<p className="eyebrow">{resume.eyebrow}</p>
|
||||
<h1>{resume.name}</h1>
|
||||
<h2>{resume.role}</h2>
|
||||
<p className="hero-copy">{resume.bio}</p>
|
||||
<div className="hero-actions">
|
||||
<a className="button primary" href="#work">Selected work</a>
|
||||
<a className="button" href={resume.contacts[0].href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>
|
||||
</div>
|
||||
<div className="hero-facts">
|
||||
<span><b>05</b> featured systems</span>
|
||||
<span><b>∞</b> curiosity for hard problems</span>
|
||||
</div>
|
||||
<div className="hero-index" aria-hidden="true">01—05</div>
|
||||
</section>
|
||||
|
||||
<section className="capabilities shell" id="profile">
|
||||
<div className="section-heading">
|
||||
<p>Profile / capabilities</p>
|
||||
<span>Three intersecting domains</span>
|
||||
</div>
|
||||
<div className="cap-grid">
|
||||
{resume.capabilities.map((item) => (
|
||||
<article className="cap-card" key={item.index}>
|
||||
<span className="cap-number">{item.index}</span>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.text}</p>
|
||||
<div className="tag-list">{item.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="work shell" id="work">
|
||||
<div className="section-heading">
|
||||
<p>Selected work</p>
|
||||
<span>Built systems, not placeholder concepts</span>
|
||||
</div>
|
||||
<div className="project-list">
|
||||
{resume.projects.map((project) => (
|
||||
<article className="project" key={project.number}>
|
||||
<div className="project-meta">
|
||||
<span>{project.number}</span>
|
||||
<p>{project.kind}</p>
|
||||
</div>
|
||||
<div className="project-body">
|
||||
<h3>{project.name}</h3>
|
||||
<p>{project.description}</p>
|
||||
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
|
||||
<div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</div>
|
||||
<div className="project-links">
|
||||
<a href={project.href} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
|
||||
{'secondaryHref' in project && project.secondaryHref && (
|
||||
<a href={project.secondaryHref} target="_blank" rel="noreferrer">{'secondaryLabel' in project ? project.secondaryLabel : 'Related project'} <ArrowIcon /></a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="tools shell">
|
||||
<div className="section-heading">
|
||||
<p>Tools & experiments</p>
|
||||
<span>Smaller utilities from active exploration</span>
|
||||
</div>
|
||||
<div className="tool-grid">
|
||||
{resume.tools.map((tool) => (
|
||||
<a href={tool.href} target="_blank" rel="noreferrer" key={tool.name}>
|
||||
<div><strong>{tool.name}</strong><p>{tool.note}</p></div><ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about shell">
|
||||
<div className="section-heading">
|
||||
<p>Education</p><span>Learning by building</span>
|
||||
</div>
|
||||
<div className="education-list">
|
||||
{resume.education.map((education) => (
|
||||
<div className="education" key={education.institution}>
|
||||
<span>{education.period}</span>
|
||||
<div><h3>{education.institution}</h3><p>{education.program}</p></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="footer shell" id="contact">
|
||||
<p className="eyebrow">Have a difficult technical problem?</p>
|
||||
<h2>Let’s build something<br /><em>that actually works.</em></h2>
|
||||
<div className="contact-grid">
|
||||
{resume.contacts.map((contact) => (
|
||||
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.label}>
|
||||
<span>{contact.label}</span><strong>{contact.value}</strong><ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Designed as a static Next.js portfolio</span></div>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
return <Site initial={resume as unknown as Resume} />
|
||||
}
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Live, Project, Resume } from '@/lib/types'
|
||||
|
||||
// Deployed on shanaty.ru the build points at the same-origin file; elsewhere (dev, GitHub Pages) it uses the public URL.
|
||||
const DATA_URL = process.env.NEXT_PUBLIC_DATA_URL ?? 'https://shanaty.ru/data/resume.json'
|
||||
const GITEA_PROFILE = 'https://git.shanaty.ru/Emil_Shanaty'
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const monthYear = (iso?: string) => (iso ? new Date(iso).toLocaleDateString('en', { month: 'short', year: 'numeric' }) : '')
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3 13 13 3M6 3h7v7" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
type Status = 'loading' | 'live' | 'offline'
|
||||
|
||||
export default function Site({ initial }: Readonly<{ initial: Resume }>) {
|
||||
const [resume, setResume] = useState<Resume>(initial)
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [generatedAt, setGeneratedAt] = useState<string>()
|
||||
const [status, setStatus] = useState<Status>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
fetch(DATA_URL, { cache: 'no-store', signal: controller.signal })
|
||||
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
||||
.then((data: Live) => {
|
||||
if (!data?.resume || !Array.isArray(data.projects)) throw new Error('unexpected data')
|
||||
setResume(data.resume)
|
||||
setProjects(data.projects)
|
||||
setGeneratedAt(data.generated_at)
|
||||
setStatus('live')
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (error.name !== 'AbortError') setStatus('offline')
|
||||
})
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
// Once the list has loaded and is empty (no repo carries the `resume` topic yet), the section is hidden entirely.
|
||||
const showProjects = status !== 'live' || projects.length > 0
|
||||
const primary = resume.contacts[0]
|
||||
const github = resume.contacts.find((contact) => contact.label === 'GitHub')
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="ambient ambient-one" aria-hidden="true" />
|
||||
<div className="ambient ambient-two" aria-hidden="true" />
|
||||
<header className="nav shell">
|
||||
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="#skills">Skills</a>
|
||||
<a href="#experience">Experience</a>
|
||||
{showProjects && <a href="#projects">Projects</a>}
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero shell" id="top">
|
||||
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
|
||||
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
|
||||
<p className="eyebrow">{resume.eyebrow}</p>
|
||||
<h1>{resume.name}</h1>
|
||||
<h2>{resume.role}</h2>
|
||||
<p className="hero-copy">{resume.bio}</p>
|
||||
<div className="hero-actions">
|
||||
{showProjects && <a className="button primary" href="#projects">Selected work</a>}
|
||||
<a className={showProjects ? 'button' : 'button primary'} href={primary.href}>Get in touch <ArrowIcon /></a>
|
||||
{github && <a className="button" href={github.href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>}
|
||||
</div>
|
||||
<div className="hero-facts">
|
||||
{status === 'live' && projects.length > 0 && <span><b>{pad(projects.length)}</b> projects, live from Gitea</span>}
|
||||
<span><b>{pad(resume.skills.length)}</b> areas of practice</span>
|
||||
</div>
|
||||
{status === 'live' && projects.length > 0 && <div className="hero-index" aria-hidden="true">01—{pad(projects.length)}</div>}
|
||||
</section>
|
||||
|
||||
<section className="capabilities shell" id="skills">
|
||||
<div className="section-heading">
|
||||
<p>Skills / stack</p>
|
||||
<span>Where I work</span>
|
||||
</div>
|
||||
<div className="skill-grid">
|
||||
{resume.skills.map((skill, index) => (
|
||||
<article className="skill-card" key={skill.title}>
|
||||
<span className="cap-number">{pad(index + 1)}</span>
|
||||
<h3>{skill.title}</h3>
|
||||
<p>{skill.text}</p>
|
||||
<div className="tag-list">{skill.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about shell" id="experience">
|
||||
<div className="section-heading">
|
||||
<p>Experience</p>
|
||||
<span>Building, running, leading</span>
|
||||
</div>
|
||||
<div className="exp-list">
|
||||
{resume.experience.map((entry) => (
|
||||
<div className="exp-item" key={entry.title}>
|
||||
<span>{entry.meta}</span>
|
||||
<div>
|
||||
<h3>{entry.title}</h3>
|
||||
<ul>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showProjects && (
|
||||
<section className="work shell" id="projects">
|
||||
<div className="section-heading">
|
||||
<p>Selected work</p>
|
||||
<span>
|
||||
{status === 'live' && `Live from Gitea · updated ${monthYear(generatedAt)}`}
|
||||
{status === 'loading' && 'Loading from Gitea…'}
|
||||
{status === 'offline' && 'Live list unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
{status === 'loading' && (
|
||||
<p className="status-note">
|
||||
Loading projects…
|
||||
<noscript> The project list is loaded live from git.shanaty.ru and needs JavaScript. Repositories: <a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>.</noscript>
|
||||
</p>
|
||||
)}
|
||||
{status === 'offline' && (
|
||||
<p className="status-note">
|
||||
The live project list could not be loaded right now. Browse the repositories directly at{' '}
|
||||
<a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>
|
||||
{github && <> or <a href={github.href}>github.com/{github.value}</a></>}.
|
||||
</p>
|
||||
)}
|
||||
<div className="project-list">
|
||||
{projects.map((project, index) => (
|
||||
<article className="project" key={project.name}>
|
||||
<div className="project-meta">
|
||||
<span>{pad(index + 1)}</span>
|
||||
<p>{project.kind ?? project.language ?? 'Repository'}</p>
|
||||
</div>
|
||||
<div className="project-body">
|
||||
<h3>{project.title ?? project.name}</h3>
|
||||
{project.description && <p>{project.description}</p>}
|
||||
{project.highlights && project.highlights.length > 0 && (
|
||||
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
|
||||
)}
|
||||
{project.tech.length > 0 && <div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>}
|
||||
{project.updated && <p className="project-updated">Updated {monthYear(project.updated)}</p>}
|
||||
</div>
|
||||
<div className="project-links">
|
||||
<a href={project.url} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
|
||||
{project.homepage && <a href={project.homepage} target="_blank" rel="noreferrer">Live site <ArrowIcon /></a>}
|
||||
{project.links?.map((link) => (
|
||||
<a href={link.href} target="_blank" rel="noreferrer" key={link.href}>{link.label} <ArrowIcon /></a>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="about shell" id="education">
|
||||
<div className="section-heading">
|
||||
<p>Education & more</p>
|
||||
<span>Learning by building</span>
|
||||
</div>
|
||||
<div className="about-grid">
|
||||
<div className="education-list">
|
||||
{resume.education.map((entry) => (
|
||||
<div className="education" key={entry.institution}>
|
||||
<span>{entry.period}</span>
|
||||
<div><h3>{entry.institution}</h3><p>{entry.program}</p></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="side-info">
|
||||
<h3 className="mini-heading">Languages</h3>
|
||||
<ul className="lang-list">
|
||||
{resume.languages.map((language) => (
|
||||
<li key={language.name}><span>{language.name}</span><b>{language.level}</b></li>
|
||||
))}
|
||||
</ul>
|
||||
<h3 className="mini-heading">Interests</h3>
|
||||
<div className="tag-list">{resume.interests.map((interest) => <span key={interest}>{interest}</span>)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="footer shell" id="contact">
|
||||
<p className="eyebrow">Have a difficult technical problem?</p>
|
||||
<h2>Let’s build something<br /><em>that actually works.</em></h2>
|
||||
<div className="contact-grid">
|
||||
{resume.contacts.map((contact) => (
|
||||
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.value}>
|
||||
<span>{contact.label}</span>
|
||||
<strong>{contact.value}{contact.note && <em className="contact-note"> · {contact.note}</em>}</strong>
|
||||
<ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Content and project list are read live from Gitea</span></div>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
{
|
||||
"name": "Emil Shanayev",
|
||||
"eyebrow": "backend · devops · AI systems · scientific computing · game tech",
|
||||
"role": "Backend, DevOps & AI Systems Developer",
|
||||
"bio": "I build AI-native products, backend services and the infrastructure to run them: from Python/FastAPI and Go services, WebSocket APIs and containerised CI/CD pipelines to distributed molecular workloads, LLM-controlled worlds and a Vulkan game engine. I run self-hosted services on my own Linux servers.",
|
||||
"location": "Russia",
|
||||
"availability": "Open to internships, full-time, part-time and project work · remote, hybrid or on-site",
|
||||
"contacts": [
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "emil28092005@yandex.ru",
|
||||
"href": "mailto:emil28092005@yandex.ru",
|
||||
"note": "preferred"
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "emil28092005@gmail.com",
|
||||
"href": "mailto:emil28092005@gmail.com"
|
||||
},
|
||||
{
|
||||
"label": "Telegram",
|
||||
"value": "@emilshanaty",
|
||||
"href": "https://t.me/emilshanaty"
|
||||
},
|
||||
{
|
||||
"label": "GitHub",
|
||||
"value": "emil28092005",
|
||||
"href": "https://github.com/emil28092005"
|
||||
},
|
||||
{
|
||||
"label": "Gitea",
|
||||
"value": "git.shanaty.ru/Emil_Shanaty",
|
||||
"href": "https://git.shanaty.ru/Emil_Shanaty"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"title": "Backend & web",
|
||||
"text": "Asynchronous REST and WebSocket services, database design and query optimisation.",
|
||||
"stack": [
|
||||
"Python",
|
||||
"FastAPI",
|
||||
"asyncio",
|
||||
"Go",
|
||||
"Rust",
|
||||
"TypeScript",
|
||||
"SQL",
|
||||
"PostgreSQL",
|
||||
"SQLite",
|
||||
"Redis",
|
||||
"WebSocket",
|
||||
"JSON-RPC",
|
||||
"Java (basic)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "DevOps & infrastructure",
|
||||
"text": "Containerised delivery, CI/CD automation and day-to-day Linux server administration.",
|
||||
"stack": [
|
||||
"Docker",
|
||||
"docker-compose",
|
||||
"Kubernetes",
|
||||
"GitHub Actions",
|
||||
"Jenkins",
|
||||
"Harbor",
|
||||
"Linux",
|
||||
"Bash",
|
||||
"VPS",
|
||||
"Debian",
|
||||
"Ubuntu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "AI-native systems",
|
||||
"text": "Agent protocols, recursive research teams, MCP integrations and human-in-the-loop feedback systems.",
|
||||
"stack": [
|
||||
"MCP",
|
||||
"ACP / JSON-RPC",
|
||||
"LLM APIs",
|
||||
"TypeScript",
|
||||
"Python"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Scientific software",
|
||||
"text": "Molecular data processing, cheminformatics workloads, deterministic pipelines and reproducible command-line tools.",
|
||||
"stack": [
|
||||
"Python",
|
||||
"RDKit",
|
||||
"ChEMBL",
|
||||
"Go",
|
||||
"PostgreSQL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Graphics & game tech",
|
||||
"text": "Performance-oriented engines, native audio tools and 3D content pipelines.",
|
||||
"stack": [
|
||||
"C# / .NET",
|
||||
"Vulkan",
|
||||
"Unity",
|
||||
"Blender",
|
||||
"Rust",
|
||||
"C++"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Team & process",
|
||||
"text": "Leading small teams, Agile/Scrum coordination and clear communication with stakeholders.",
|
||||
"stack": [
|
||||
"Agile",
|
||||
"Scrum",
|
||||
"Team leading",
|
||||
"Git",
|
||||
"GitHub"
|
||||
]
|
||||
}
|
||||
],
|
||||
"experience": [
|
||||
{
|
||||
"title": "Independent projects & self-directed engineering",
|
||||
"meta": "Ongoing",
|
||||
"points": [
|
||||
"Full-featured asynchronous backend applications in Python/FastAPI.",
|
||||
"Containerised applications with Docker, deployed to Kubernetes.",
|
||||
"CI/CD pipelines with GitHub Actions for automated testing and deployment.",
|
||||
"Administration of Linux servers (Debian VPS) and self-hosted services.",
|
||||
"Databases in practice: SQLite, Redis, PostgreSQL."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Team experience",
|
||||
"meta": "University and agency-style projects",
|
||||
"points": [
|
||||
"Led teams in university projects and managed a team in an agency-style setting.",
|
||||
"Worked in Agile environments using Scrum; task distribution and coordination.",
|
||||
"Regular communication with team members and stakeholders."
|
||||
]
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"period": "2026 — 2030",
|
||||
"institution": "HSE University, Moscow",
|
||||
"program": "Bachelor’s programme · Game and Digital Product Development · 1st-year student"
|
||||
},
|
||||
{
|
||||
"period": "2023 — 2025",
|
||||
"institution": "Innopolis University",
|
||||
"program": "Data Structures and Artificial Intelligence (DSAI) · 2 years, incomplete higher education"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"name": "Russian",
|
||||
"level": "Native"
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"level": "B2"
|
||||
}
|
||||
],
|
||||
"interests": [
|
||||
"DevOps & Infrastructure as Code",
|
||||
"High-load backend systems",
|
||||
"Linux & systems administration",
|
||||
"Computer graphics & game development",
|
||||
"Open source",
|
||||
"Learning from documentation and by building"
|
||||
],
|
||||
"projectNotes": {
|
||||
"Cortex_Engine": {
|
||||
"title": "Cortex Engine",
|
||||
"rank": 1,
|
||||
"kind": "AI-native 3D game engine",
|
||||
"description": "A C#/.NET 9 game engine with a pure P/Invoke Vulkan 1.3 renderer, PBR shading, cubemap-array shadows, native Box3D physics and Flecs ECS. Its editor and runtime expose scene operations to AI agents through MCP.",
|
||||
"highlights": [
|
||||
"Pure Vulkan 1.3 backend",
|
||||
"PBR + multi-light shadows",
|
||||
"Playable FPS demo",
|
||||
"7 MCP scene tools"
|
||||
],
|
||||
"tech": [
|
||||
"C#",
|
||||
".NET 9",
|
||||
"Vulkan",
|
||||
"SDL3",
|
||||
"Box3D",
|
||||
"Flecs",
|
||||
"MCP"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/Cortex_Engine"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SciMesh": {
|
||||
"title": "SciMesh",
|
||||
"rank": 2,
|
||||
"kind": "Distributed scientific computing platform",
|
||||
"description": "A local-first platform that turns molecular computations into independent tasks, dispatches them to Go workers and deterministically combines checksum-protected artifacts. The Python SDK provides exact ChEMBL workloads without dense N×N matrices.",
|
||||
"highlights": [
|
||||
"Python workload SDK",
|
||||
"Go coordinator + workers",
|
||||
"PostgreSQL / SQLite modes",
|
||||
"Cross-platform signed releases"
|
||||
],
|
||||
"tech": [
|
||||
"Python",
|
||||
"Go",
|
||||
"RDKit",
|
||||
"PostgreSQL",
|
||||
"Docker",
|
||||
"SHA-256"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/SciMesh"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AICC-Protocol": {
|
||||
"title": "AICC Protocol",
|
||||
"rank": 3,
|
||||
"kind": "LLM-controlled character protocol",
|
||||
"description": "An engine-agnostic protocol and Python SDK for connecting language-model agents to virtual environments. The reference capsule testbed proves the full loop: perception, movement and interaction over WebSocket with a protocol-conformant bridge.",
|
||||
"highlights": [
|
||||
"Async-first Python SDK",
|
||||
"WebSocket + in-process transports",
|
||||
"9/9 conformance scenarios",
|
||||
"Vision and navigation demo"
|
||||
],
|
||||
"tech": [
|
||||
"Python",
|
||||
"JSON Schema",
|
||||
"WebSocket",
|
||||
"Pydantic",
|
||||
"LLM agents"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/AICC-Protocol"
|
||||
},
|
||||
{
|
||||
"label": "aicc-py",
|
||||
"href": "https://github.com/emil28092005/aicc-py"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hypothesis-machine": {
|
||||
"title": "Hypothesis Machine",
|
||||
"rank": 4,
|
||||
"kind": "Recursive research-agent extension",
|
||||
"description": "A TypeScript extension for the Pi Coding Agent that turns one interactive session into a persistent, recursive research team. It combines parallel agents, recoverable trees, full-text research memory and isolated Docker experiments with independent review.",
|
||||
"highlights": [
|
||||
"Recursive / parallel agents",
|
||||
"Persistent tree recovery",
|
||||
"SQLite FTS research memory",
|
||||
"Sandboxed experiment runner"
|
||||
],
|
||||
"tech": [
|
||||
"TypeScript",
|
||||
"Node.js 22",
|
||||
"SQLite",
|
||||
"Docker",
|
||||
"Pi Agent"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/hypothesis-machine"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bibi_bot": {
|
||||
"title": "Bibi Bot",
|
||||
"rank": 5,
|
||||
"kind": "Self-hosted Telegram AI assistant",
|
||||
"description": "A production-oriented Telegram assistant for private groups: one-way HLS game streaming in a Mini App, DeepSeek-backed chat, repository-grounded answers, scoped SQLite memory and PostgreSQL-backed team context.",
|
||||
"highlights": [
|
||||
"Telegram Mini App + HLS",
|
||||
"Repository research with partial clones",
|
||||
"Scoped memory and privacy controls",
|
||||
"Docker Compose deployment"
|
||||
],
|
||||
"tech": [
|
||||
"Python",
|
||||
"FastAPI",
|
||||
"Telegram",
|
||||
"DeepSeek",
|
||||
"PostgreSQL",
|
||||
"Docker"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/bibi_bot"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Soundgen": {
|
||||
"title": "Soundgen",
|
||||
"rank": 11,
|
||||
"kind": "Tool / experiment",
|
||||
"description": "Rust 8-bit synthesis, MCP generation and human-feedback training loop",
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/Soundgen"
|
||||
}
|
||||
]
|
||||
},
|
||||
"KimiFleet": {
|
||||
"title": "KimiFleet",
|
||||
"rank": 12,
|
||||
"kind": "Tool / experiment",
|
||||
"description": "Visible multi-agent orchestrator over ACP with live dashboard and handoffs",
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/KimiFleet"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Imagen": {
|
||||
"title": "Imagen",
|
||||
"rank": 13,
|
||||
"kind": "Tool / experiment",
|
||||
"description": "MCP pixel-art generator with transparent backgrounds and rated references",
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/Imagen"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Fractalier": {
|
||||
"title": "Fractalier",
|
||||
"rank": 14,
|
||||
"kind": "Tool / experiment",
|
||||
"description": "Procedural fractal evolution game with selection, crossover and mutation",
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/Fractalier"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hermes-plugin-mome": {
|
||||
"title": "MoME",
|
||||
"rank": 15,
|
||||
"kind": "Tool / experiment",
|
||||
"description": "Sparse-gated personal memory plugin with an online-learning router",
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/emil28092005/hermes-plugin-mome"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
export const resume = {
|
||||
name: 'Emil Shanayev',
|
||||
eyebrow: 'AI systems · scientific computing · game technology',
|
||||
role: 'AI Systems & Scientific Software Developer',
|
||||
bio: 'I build AI-native products and developer infrastructure across scientific computing, agent protocols and game technology — from distributed molecular workloads and LLM-controlled worlds to a Vulkan engine and production Telegram services.',
|
||||
location: 'Russia',
|
||||
availability: 'Open to internships, research collaborations and engineering projects',
|
||||
contacts: [
|
||||
{ label: 'GitHub', value: 'emil28092005', href: 'https://github.com/emil28092005' },
|
||||
{ label: 'Email', value: 'emil28092005@gmail.com', href: 'mailto:emil28092005@gmail.com' },
|
||||
{ label: 'Email', value: 'emil28092005@yandex.ru', href: 'mailto:emil28092005@yandex.ru' },
|
||||
{ label: 'Telegram', value: '@emilshanaty', href: 'https://t.me/emilshanaty' },
|
||||
],
|
||||
capabilities: [
|
||||
{
|
||||
index: '01',
|
||||
title: 'Scientific software',
|
||||
text: 'Molecular data processing, cheminformatics workloads, deterministic pipelines and reproducible command-line tools.',
|
||||
stack: ['Python', 'RDKit', 'ChEMBL', 'Go', 'PostgreSQL'],
|
||||
},
|
||||
{
|
||||
index: '02',
|
||||
title: 'AI-native systems',
|
||||
text: 'Agent protocols, recursive research teams, MCP integrations and human-in-the-loop feedback systems.',
|
||||
stack: ['MCP', 'ACP / JSON-RPC', 'LLM APIs', 'TypeScript', 'Python'],
|
||||
},
|
||||
{
|
||||
index: '03',
|
||||
title: 'Systems & infrastructure',
|
||||
text: 'Performance-oriented engines, native audio tools, backend services and the infrastructure needed to ship them.',
|
||||
stack: ['C# / .NET', 'Vulkan', 'Rust', 'Docker', 'WebSocket', 'Linux'],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
number: '01',
|
||||
name: 'Cortex Engine',
|
||||
kind: 'AI-native 3D game engine',
|
||||
description: 'A C#/.NET 9 game engine with a pure P/Invoke Vulkan 1.3 renderer, PBR shading, cubemap-array shadows, native Box3D physics and Flecs ECS. Its editor and runtime expose scene operations to AI agents through MCP.',
|
||||
highlights: ['Pure Vulkan 1.3 backend', 'PBR + multi-light shadows', 'Playable FPS demo', '7 MCP scene tools'],
|
||||
tech: ['C#', '.NET 9', 'Vulkan', 'SDL3', 'Box3D', 'Flecs', 'MCP'],
|
||||
href: 'https://github.com/emil28092005/Cortex_Engine',
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
name: 'SciMesh',
|
||||
kind: 'Distributed scientific computing platform',
|
||||
description: 'A local-first platform that turns molecular computations into independent tasks, dispatches them to Go workers and deterministically combines checksum-protected artifacts. The Python SDK provides exact ChEMBL workloads without dense N×N matrices.',
|
||||
highlights: ['Python workload SDK', 'Go coordinator + workers', 'PostgreSQL / SQLite modes', 'Cross-platform signed releases'],
|
||||
tech: ['Python', 'Go', 'RDKit', 'PostgreSQL', 'Docker', 'SHA-256'],
|
||||
href: 'https://github.com/emil28092005/SciMesh',
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
name: 'AICC Protocol',
|
||||
kind: 'LLM-controlled character protocol',
|
||||
description: 'An engine-agnostic protocol and Python SDK for connecting language-model agents to virtual environments. The reference capsule testbed proves the full loop: perception, movement and interaction over WebSocket with a protocol-conformant bridge.',
|
||||
highlights: ['Async-first Python SDK', 'WebSocket + in-process transports', '9/9 conformance scenarios', 'Vision and navigation demo'],
|
||||
tech: ['Python', 'JSON Schema', 'WebSocket', 'Pydantic', 'LLM agents'],
|
||||
href: 'https://github.com/emil28092005/AICC-Protocol',
|
||||
secondaryHref: 'https://github.com/emil28092005/aicc-py',
|
||||
secondaryLabel: 'aicc-py',
|
||||
},
|
||||
{
|
||||
number: '04',
|
||||
name: 'Hypothesis Machine',
|
||||
kind: 'Recursive research-agent extension',
|
||||
description: 'A TypeScript extension for the Pi Coding Agent that turns one interactive session into a persistent, recursive research team. It combines parallel agents, recoverable trees, full-text research memory and isolated Docker experiments with independent review.',
|
||||
highlights: ['Recursive / parallel agents', 'Persistent tree recovery', 'SQLite FTS research memory', 'Sandboxed experiment runner'],
|
||||
tech: ['TypeScript', 'Node.js 22', 'SQLite', 'Docker', 'Pi Agent'],
|
||||
href: 'https://github.com/emil28092005/hypothesis-machine',
|
||||
},
|
||||
{
|
||||
number: '05',
|
||||
name: 'Bibi Bot',
|
||||
kind: 'Self-hosted Telegram AI assistant',
|
||||
description: 'A production-oriented Telegram assistant for private groups: one-way HLS game streaming in a Mini App, DeepSeek-backed chat, repository-grounded answers, scoped SQLite memory and PostgreSQL-backed team context.',
|
||||
highlights: ['Telegram Mini App + HLS', 'Repository research with partial clones', 'Scoped memory and privacy controls', 'Docker Compose deployment'],
|
||||
tech: ['Python', 'FastAPI', 'Telegram', 'DeepSeek', 'PostgreSQL', 'Docker'],
|
||||
href: 'https://github.com/emil28092005/bibi_bot',
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{ name: 'Soundgen', note: 'Rust 8-bit synthesis, MCP generation and human-feedback training loop', href: 'https://github.com/emil28092005/Soundgen' },
|
||||
{ name: 'KimiFleet', note: 'Visible multi-agent orchestrator over ACP with live dashboard and handoffs', href: 'https://github.com/emil28092005/KimiFleet' },
|
||||
{ name: 'Imagen', note: 'MCP pixel-art generator with transparent backgrounds and rated references', href: 'https://github.com/emil28092005/Imagen' },
|
||||
{ name: 'Fractalier', note: 'Procedural fractal evolution game with selection, crossover and mutation', href: 'https://github.com/emil28092005/Fractalier' },
|
||||
{ name: 'MoME', note: 'Sparse-gated personal memory plugin with an online-learning router', href: 'https://github.com/emil28092005/hermes-plugin-mome' },
|
||||
],
|
||||
education: [
|
||||
{
|
||||
period: '2023—2025',
|
||||
institution: 'Innopolis University',
|
||||
program: 'Data Structures and Artificial Intelligence (DSAI)',
|
||||
},
|
||||
{
|
||||
period: '2026—present',
|
||||
institution: 'HSE University',
|
||||
program: '1st-year student · Bachelor’s programme',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the site and publish it at https://shanaty.ru/resume/ (Gitea/Caddy server).
|
||||
#
|
||||
# The text and project list are NOT baked into the build: the page reads /data/resume.json, which the
|
||||
# server rebuilds every minute from data/resume.json in the Gitea repo plus the repos tagged `resume`.
|
||||
# So redeploy only after changing code or styles; content changes just need a push to Gitea.
|
||||
# The site is uploaded to a temp dir and swapped in, so it is never half-updated.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
HOST="${GT2GH_HOST:-root@135.106.198.222}"
|
||||
KEY="${GT2GH_KEY:-$HOME/.ssh/MyGitea}"
|
||||
SITE_DIR=/opt/gitea/site
|
||||
|
||||
[ -r "$KEY" ] || { echo "deploy: ssh key not found: $KEY" >&2; exit 1; }
|
||||
|
||||
rm -rf .next out
|
||||
BASE_PATH=/resume NEXT_PUBLIC_DATA_URL=/data/resume.json npm run build
|
||||
[ -f out/index.html ] || { echo "deploy: build failed (out/index.html missing)" >&2; exit 1; }
|
||||
|
||||
tar -C out -c . | ssh -i "$KEY" -o IgnoreUnknown=WarnWeakCrypto -o WarnWeakCrypto=no "$HOST" "
|
||||
set -e
|
||||
cd $SITE_DIR
|
||||
rm -rf .resume.new .resume.old
|
||||
mkdir .resume.new
|
||||
tar -x -C .resume.new --no-same-owner
|
||||
chmod -R a+rX .resume.new
|
||||
if [ -d resume ]; then mv resume .resume.old; fi
|
||||
mv .resume.new resume
|
||||
rm -rf .resume.old
|
||||
"
|
||||
echo "Deployed: https://shanaty.ru/resume/"
|
||||
@@ -0,0 +1,39 @@
|
||||
export type Contact = { label: string; value: string; href: string; note?: string }
|
||||
export type Skill = { title: string; text: string; stack: string[] }
|
||||
export type Experience = { title: string; meta?: string; points: string[] }
|
||||
export type Education = { period: string; institution: string; program: string }
|
||||
export type Link = { label: string; href: string }
|
||||
|
||||
export type Resume = {
|
||||
name: string
|
||||
eyebrow: string
|
||||
role: string
|
||||
bio: string
|
||||
location: string
|
||||
availability: string
|
||||
contacts: Contact[]
|
||||
skills: Skill[]
|
||||
experience: Experience[]
|
||||
education: Education[]
|
||||
languages: { name: string; level: string }[]
|
||||
interests: string[]
|
||||
}
|
||||
|
||||
// A public Gitea repository tagged `resume`, merged with the optional notes from data/resume.json.
|
||||
export type Project = {
|
||||
name: string
|
||||
title?: string
|
||||
url: string
|
||||
description?: string
|
||||
kind?: string
|
||||
highlights?: string[]
|
||||
tech: string[]
|
||||
links?: Link[]
|
||||
homepage?: string
|
||||
language?: string
|
||||
stars?: number
|
||||
updated?: string
|
||||
}
|
||||
|
||||
// Shape of /data/resume.json, written every minute by the server-side sync.
|
||||
export type Live = { generated_at: string; resume: Resume; projects: Project[] }
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
// BASE_PATH overrides the GitHub Pages default, e.g. BASE_PATH=/resume for https://shanaty.ru/resume/.
|
||||
const basePath = process.env.BASE_PATH ?? '/Emil_Shanayev_Resume'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
basePath: '/Emil_Shanayev_Resume',
|
||||
assetPrefix: '/Emil_Shanayev_Resume',
|
||||
...(basePath && { basePath, assetPrefix: basePath }),
|
||||
images: { unoptimized: true },
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user