dphil.ru
The website you are currently looking at
Role
Creator, Full-Stack Engineer, DevOps
Year
2026
Stack
Next.js 16React 19TypeScriptTailwind CSS 4TanStack QueryGSAP 3View TransitionsReact Hook FormStrapi 5.5PostgreSQL 17S3 StorageDockerDokploy

The task
To create a portfolio website that serves not merely as a quick business card, but as a live demonstration of engineering maturity. The goal was to implement production-grade solutions: fault-tolerant infrastructure, deep localization, advanced animations without compromising UX, and strict data architecture.
Stages
Architectural Design and Data Modeling
- Data Schema: Developed a relational model in Strapi 5.5 and Postgres 17. Implemented hybrid localization from the start: technical fields (slug, links, stack) were made universal, while content was made translatable, eliminating data synchronization issues.
- Frontend Architecture: Built the application skeleton using the Feature-Sliced Design (FSD) methodology. Defined strict boundaries for each slice:
index.tsfor safe client exports andserver.tsfor server code, successfully isolating the Strapi transport layer from the client bundle.
DevOps Foundation and Overcoming Container Ephemerality
- Infrastructure: Deployed a Docker environment managed by the self-hosted orchestrator, Dokploy.
- Media Layer: Since Docker containers are ephemeral and wipe local data upon each deploy, I integrated an S3-compatible object storage and CDN on day one. For local development convenience, I set up a graceful fallback to the hard drive if cloud variables are missing.
Core Development, Server Rendering, and Clean i18n
Rendering: Initialized Next.js 16 and React 19. Moved CMS fetching to the server (RSC + Route Handlers), ensuring the instant delivery of ready-to-use HTML without flashing loading states.
- Localization Optimization: Configured
i18nextto run strictly on the server with a per-locale cached instance. The client receives pre-translated strings as props—saving significant JS bandwidth in the browser and eliminating unnecessary client-side providers.
Animation Engineering and API Synchronization
- Integration: Embedded GSAP 3 for micro-animations and Next.js's experimental View Transitions API for seamless page navigation.
- Engineering Challenge: Encountered a "content flash" (FOUC) bug where React commits during lazy chunk hydration overlapped with the running GSAP intro. Resolved the issue structurally (via a
key-remount of the component withupdate="none"), making the animations fully resilient to React StrictMode.
Cache Automation, UI Polish, and Release
- On-Demand ISR: Connected Strapi and Next.js via webhooks. Upon publishing content, the backend hits a frontend route to selectively invalidate cache tags for a specific project—data updates instantly on the fly without triggering a full site rebuild.
- UI Finalization: Configured design tokens in Tailwind 4 (
@theme inline), deploying an inline script for theme switching without visual flicker. Established CI/CD through automated image builds on Docker Hub.
The process
Technology Stack
| Layer | Technologies |
|---|---|
| Frontend | Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4 |
| Client-side Data | TanStack Query |
| Animations | GSAP 3 + @gsap/react, View Transitions API (experimental Next 16 feature) |
| Forms | React Hook Form + Zod |
| Content | react-markdown + remark-gfm, yet-another-react-lightbox, react-photo-album |
| i18n | i18next / react-i18next, 3 languages: RU / EN / DE |
| Backend | Strapi 5.5 (headless CMS) on Node, PostgreSQL 17 |
| Media Storage | S3-compatible object storage + CDN |
| Infrastructure | Docker, VPS, Dokploy, separate git repositories for frontend/backend |
Frontend: Under the Hood
Architecture — Feature-Sliced Design
The codebase is structured in layers app → views → widgets → features → entities → shared, rather than being dumped into a components/ folder. Each slice has its own barrel (index.ts) for client-safe exports and a separate server.ts for server code—ensuring the client bundle physically cannot pull in the Strapi transport layer. Business logic (infinite scroll, contact form, page data fetching) is extracted into hooks and view-api services, not smeared across components. This is an architecture that can be easily scaled within a team—not a "portfolio workaround."
// entities/project/index.ts — client-safe barrel
export { projectTags, type ProjectTag } from './config/tags';
export { fetchProjectsPage, PROJECTS_PAGE_SIZE } from './api/client';
export { useInfiniteProjects } from './api/use-infinite-projects';
export { ProjectCard, type ProjectCardProps } from './ui/ProjectCard';
// entities/project/server.ts — server-only barrel, imported
// only by RSC/route handlers, never reaches the client bundle
export {
getProject,
getProjects,
getProjectsCount,
getProjectSlugs,
} from './api/server';
Server Components + Server Route Handlers
Data from Strapi is fetched on the server (RSC + route handlers); the client receives ready-made HTML and minimal JS—resulting in a fast first paint, out-of-the-box SEO, and absolutely no "flashing" loading states on the homepage.
Theming and Tokens without FOUC
All styling (colors, spacings, fonts) is mapped to CSS tokens (Tailwind 4 @theme inline). Theme switching works without the Flash of Unstyled Content (FOUC): an inline script applies .dark before the first paint (via useServerInsertedHTML), combined with a <noscript> fallback and full respect for prefers-reduced-motion.
Server-Side Multilingualism
i18next runs exclusively on the server—the instance is cached per-locale, and client components receive already translated strings as props, without redundant providers and excess JS in the browser. The locale is determined by priority: NEXT_LOCALE cookie → Accept-Language header (via negotiator + intl-localematcher) → default en. Everything is localized, including the PDF resume: depending on the selected locale, the Russian or English version of the CV is served (the English variant is used for the German locale).
GSAP Animations and Mitigating Content "Flash"
Custom animated cursor, text intro animations (SplitText), smooth transitions between pages via Next.js's experimental View Transitions API.
A notable challenge was the content "flash" bug on initial load (heading appears → disappears → animates again). The root cause was non-trivial: React, in experimental View Transition mode, calls document.startViewTransition not only during navigation but on any commit with a transition (specifically, when lazy-loading a client chunk right after hydration), causing the page snapshot animation to overlay the still-running GSAP intro. The final solution was a strict separation of concerns: the initial page load belongs to GSAP, routing transitions to the View Transitions API, and nothing else is animated. Implemented structurally (via a key-remount of <ViewTransition> with update="none" for internal commits), rather than a hack using classes/timers—making it resilient to React StrictMode. Handling such edge cases is a good indicator of an engineer's ability to not just use experimental APIs, but to reverse-engineer their internal mechanics when documentation is scarce.
// shared/lib/vt/PageTransition.tsx
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
// key={pathname} → route change unmounts/remounts this boundary,
// activating the "vtpage" transition pair (vt-page-out/in in CSS).
// update="none" → any other commit (lazy chunks, data refresh)
// is skipped by React entirely — no snapshot fights the running GSAP intro.
<ViewTransition key="{pathname}" name="vtpage" update="none">
<div>{children}</div>
</ViewTransition>
);
}
Gallery and Media
yet-another-react-lightbox (React 19 compatible) + react-photo-album for a masonry layout of project screenshots—portrait and landscape shots do not break the row height. Markdown content from the CMS is rendered via react-markdown + remark-gfm with a custom <img> renderer that opens these images in the shared lightbox.
Mockups Without a Single Extra File
Project screenshots that lack real assets are CSS-drawn browser/phone mockups (deterministically chosen based on the project slug), rather than placeholder images. This saves page weight and solves the issue of missing assets for client projects under NDA.
Forms
React Hook Form + Zod validation on the contact form—a type-safe validation schema that is shared between client-side and potential server-side checks.
Backend: Strapi + Postgres
Strict Data Model
Collections: projects, tags, "about me" (singleton), contact form submissions—with relations between them, not just flat tables. Field localization is selectively thought out: textual content (description, task, process, result) is per-locale, while system fields (slug, year, stack, links, cover image, tag order) are shared across all locales. This completely prevents data desync between different language versions of a single project.
Media — in S3, Not on VPS Storage
File uploading is configured via @strapi/provider-upload-aws-s3 to an S3-compatible storage (Ceph RGW) distributed via CDN. The reason isn't just because it's "trendy," but highly practical: the backend runs in a Docker container that gets rebuilt on every deploy—the container's local disk is ephemeral, meaning without external storage, all uploaded media would vanish with every release. Set up with a graceful fallback to the local disk if S3 environment variables are missing, allowing seamless local development without a cloud bucket.
Publishing Without Rebuilding — On-Demand ISR
When content is published or updated, Strapi hits a webhook at /api/revalidate on the frontend, which precisely invalidates Next.js cache tags (projects, project:<slug>, about)—updating the specific page instantly without a full site redeploy or dropping the entire site cache. Updating one case study doesn't reset the cache for the others.
// app/api/revalidate/route.ts
switch (body.model) {
case 'project':
tags.add('projects');
if (body.entry?.slug) tags.add(`project:${body.entry.slug}`);
break;
// ...
}
// 'max' = stale-while-revalidate: the visitor instantly gets the cached
// page, while a fresh version is built in the background; if the CMS
// goes down during publishing, the old page remains, rather than throwing a 500 error.
for (const tag of tags) revalidateTag(tag, 'max');
The conscious choice of "one reload later, but never a 500 error" over blocking instant invalidation is a typical trade-off that differentiates a demo project from a high-uptime production application.
SEO Metadata
Title/description are managed from the CMS but support dynamic tokens like {years} and {projects}—the years of experience and total project count are calculated on the fly (career start year + live CMS counter), ensuring the copy never goes stale and doesn't require manual updates every January.
// shared/lib/seo.ts
export function applyMetaTokens(text: string, projectsTotal: number): string {
const years = new Date().getFullYear() - site.careerStartYear;
const projects = projectsTotal;
return text.replaceAll('{years}', String(years)).replaceAll('{projects}', String(projects));
}
In the CMS, the text is written as "{years}+ years of commercial development" once—and remains accurate forever.
Notifications and Feedback
Submitting the contact form triggers an email to the admin (SMTP via env variables, using the same graceful fallback pattern—without configured credentials, the form simply logs the request instead of crashing).
Plans
- Blog — utilizing the same content model as the projects (Strapi collection + on-demand ISR), localized in RU/EN/DE. Aims to establish technical expertise and drive organic SEO traffic to the site, rather than relying solely on the "about me" card.
- Tests — currently, correctness is ensured by TypeScript + Zod schemas at data boundaries, but there is no automated behavioral coverage: E2E (Playwright) for key scenarios—theme/language switching, contact form, View Transitions navigation—and unit tests for business logic in
entities/features. This is a natural progression of the established FSD architecture, where logic is decoupled from components and is therefore testable in isolation.
What This Demonstrates Overall
- Full-stack mastery: From infrastructure (Docker, S3, CDN, webhooks) to pixel-perfect animation polishing in the browser—with no gaps in between.
- Production mindset, not demo code: Cache invalidation, container ephemerality, graceful fallbacks for local development, React StrictMode resilience—these are things that only surface in real-world projects.
- Readiness for bleeding-edge tech (Next.js 16, React 19, View Transitions API) and the ability to dive into their internal mechanics when official documentation is still lacking.
- Design discipline: Tokenized styling, thoughtful typography (Unbounded + Exo 2, with Cyrillic support), minimalism without losing detail.
- Meaningful localization: 3 languages implemented at both the content and metadata levels, including separate PDF resumes tailored to the audience.
The results
languages with browser auto-detection
pages server-rendered
JS overhead for client translations
CLS (Cumulative Layout Shift)