My website hasn't worked for five years. I'm rebuilding it into a portal — showing everything
-
1
00:00
Five copies of one entry — live
1 What to do
I open the old website and search the page: the same entry is found five times.
-
2
00:48
The duplicates are not in the search, but in the markup
1 What to do
I look at the code: the carousel renders each block as duplicates, and the search engine sees a page stuffed with repeats.
-
3
01:08
An empty first screen
1 What to do
The first screen has no answer to the question of how I'm useful and what to come to me with.
-
4
01:22
Breakdown of the "before": nine roles, Skills, English 55 %
1 What to do
I go through the old website block by block: a subtitle of nine job titles, skill percentages, an experience that breaks off.
-
5
02:58
Two storefronts: the website and the channel
1 What to do
I put the website's skills block next to the channel description: the two storefronts say different things.
-
6
03:29
Who I am and why this isn't about a pretty website
1 What to do
Briefly about myself and about the fact that the website exists to prove something, not for looks.
-
7
04:30
Architecture on the board: four tasks
1 What to do
I draw the website's four tasks on the board, and for each one a section and where it leads.
-
8
06:54
The stack out loud: why not a website generator
1 What to do
FastAPI, server-side pages, Tailwind; why not a static generator and not a single-page application.
-
9
07:55
Three things before the first prompt
1 What to do
Content separate from layout, project rules in CLAUDE.md, the task taken from the architecture, not from my head.
-
10
08:59
First prompt: the skeleton
1 What to do
I give Claude Code the portal skeleton: routes, markdown reading, a base template. First the plan, then the files.
2 Prompt
EN · PromptWe're building a personal portal on FastAPI. No static generator, no SPA: pages are rendered on the server via Jinja2. Project structure: app/main.py — the FastAPI application app/content.py — reading markdown from content/ and parsing the front matter templates/ — Jinja2: base.html and one template per section static/css/site.css — built Tailwind (we commit it to the repository already built) content/ru|uk|en/ — section texts in markdown with YAML front matter content/works/ — one file per work What to do now: 1. app/main.py: a FastAPI application, StaticFiles mounted at /static, Jinja2Templates pointing at templates/. 2. Routes. The language is the first path segment, Russian without a prefix: / /uk/ /en/ /how /uk/how /en/how /works /uk/works /en/works /works/{slug} and the same with prefixes /materials /contact /archive — likewise. One shared handler, not twenty identical functions. 3. app/content.py: reads content/{lang}/{name}.md, parses the YAML front matter (title, description, updated), returns HTML and metadata. If the file doesn't exist — return a 404, not an empty page. 4. templates/base.html: the lang attribute is filled in, title and meta description come from the front matter, one link to /static/css/site.css. 5. No texts in templates. Any string I might want to edit lives in content/. The template has only markup and substitutions. We're not touching the styling now — only the structure and the correct texts in the correct places. First show the plan and the list of files you're going to create. Don't write the files until I confirm.EN · Prompt from the screenСобираем персональный портал на FastAPI. Ни генератора статики, ни SPA: страницы рендерятся на сервере через Jinja2. Структура проекта: app/main.py — приложение FastAPI app/content.py — чтение markdown из content/ и разбор фронтматтера templates/ — Jinja2: base.html и по шаблону на раздел static/css/site.css — собранный Tailwind (кладём в репозиторий собранным) content/ru|uk|en/ — тексты разделов в markdown с YAML-фронтматтером content/works/ — по одному файлу на работу Что сделать сейчас: 1. app/main.py: приложение FastAPI, StaticFiles смонтирован на /static, Jinja2Templates на templates/. 2. Маршруты. Язык — первый сегмент пути, русский без префикса: / /uk/ /en/ /how /uk/how /en/how /works /uk/works /en/works /works/{slug} и то же с префиксами /materials /contact /archive — так же. Один общий обработчик, а не двадцать одинаковых функций. 3. app/content.py: читает content/{lang}/{name}.md, разбирает YAML-фронтматтер (title, description, updated), отдаёт HTML и метаданные. Файла нет — отдаём 404, а не пустую страницу. 4. templates/base.html: атрибут lang подставляется, title и meta description берутся из фронтматтера, один link на /static/css/site.css. 5. Никаких текстов в шаблонах. Любая строка, которую я захочу поправить, живёт в content/. В шаблоне только разметка и подстановки. Оформление сейчас не трогаем — только структура и правильные тексты в правильных местах. Сначала покажи план и список файлов, которые собираешься создать. Файлы не пиши, пока я не подтвержу. -
11
10:49
Sections: works in a single scheme
1 What to do
Each work is described in a single scheme of five parts, "what didn't work out" is mandatory.
2 Prompt
EN · PromptThe "Works" section. Each work lives in its own markdown file in content/works/ and is described in the same scheme — five parts, always in this order: 1. Task — what they came with 2. What was before — how it worked earlier 3. How it's built — what the solution is assembled from 4. What came out — measurable, if there's something to measure with 5. What didn't work out — a mandatory part, it can't be skipped Do: - the work's front matter: slug, title, client_type (without the company name), period, stack (a list), summary — one line; - the /works list template: cards in one column, the card has only title, summary and period. Nothing more; - the single work template /works/{slug}: five parts in a fixed order with subheadings, at the bottom — a link to /contact; - if the work's file has no "what didn't work out" part — the page build must fail with a clear error, not silently skip the section. This is a rule, not a wish. Company names, domains and client names are not in the texts and never appear. We describe the client by type: "fintech product", "manufacturing company".EN · Prompt from the screenРаздел «Работы». Каждая работа лежит отдельным markdown-файлом в content/works/ и описана по одной и той же схеме — пять частей, всегда в этом порядке: 1. Задача — с чем пришли 2. Что было до — как это работало раньше 3. Как устроено — из чего собрано решение 4. Что получилось — измеримо, если есть чем мерить 5. Что не получилось — обязательная часть, пропускать нельзя Сделай: - фронтматтер работы: slug, title, client_type (без названия компании), period, stack (список), summary — одна строка; - шаблон списка /works: карточки в одну колонку, в карточке только title, summary и period. Ничего больше; - шаблон одной работы /works/{slug}: пять частей в фиксированном порядке с подзаголовками, внизу — ссылка на /contact; - если в файле работы нет части «что не получилось» — сборка страницы должна упасть с понятной ошибкой, а не молча пропустить раздел. Это правило, а не пожелание. Названий компаний, доменов и имён клиентов в текстах нет и не появляется. Клиента описываем типом: «финтех-продукт», «производственная компания». -
12
13:09
Where AI did the wrong thing: invented facts
1 What to do
During translation the model added numbers that aren't in the sources. I roll it back and add a rule about facts to the project file.
2 Prompt
EN · PromptMake the Ukrainian version of the "Works" section: content/uk/works.md and the work files in content/works/ with Ukrainian variants of the text. Make it sound natural for a Ukrainian-speaking reader.
EN · Prompt from the screenСделай украинскую версию раздела «Работы»: content/uk/works.md и файлы работ в content/works/ с украинскими вариантами текста. Пусть звучит естественно для украиноязычного читателя.
$## Факты - Цифры, даты, названия, сроки и любые количественные утверждения берутся только из файлов в content/. Ничего не добавлять, не округлять, не обобщать и не «усиливать». - Нет данных для строки — оставить пропуск вида TODO(факт: ...) и спросить меня. Пустое место лучше правдоподобной выдумки. - При переводе переводится ровно то, что написано. Добавлять регалии, количество проектов, годы опыта и отраслевые формулировки — запрещено.
-
13
15:09
A silent error: hreflang "ua" instead of "uk"
1 What to do
I check the language links by hand: they aren't in the head of the page, and neither is x-default.
-
14
15:38
Languages and URL structure
1 What to do
Three languages as folders in the URL, hreflang plus x-default, and what we deliberately don't translate.
2 Prompt
EN · PromptLanguage versions. Three languages: Russian, Ukrainian, English. URLs are folders: Russian without a prefix, /uk/, /en/. No subdomains, no parameters in the URL. Do: 1. In base.html — a block of links to the language versions of the current page (rel="alternate") plus a default version for the case when the visitor's language matches none of them. The default is the Russian version. Fill in the language codes yourself; I'll check them separately. 2. A language switcher in the header: ordinary links, no JavaScript. It leads to the same page in another language, not to the home page. 3. We don't translate everything. Language versions exist for: the first screen, "How I work", "Works", "Contact". The "Archive" section with videos has none — they're in Russian, and a Ukrainian card for a Russian video adds nothing. If a page has no version in the requested language — don't invent an empty page: lead to the Russian version and don't put a language link on it. 4. Check: go through all the routes and output a table "URL — language — is there a translation — where the language link leads".
EN · Prompt from the screenЯзыковые версии. Три языка: русский, украинский, английский. Адреса — папками: русский без префикса, /uk/, /en/. Ни поддоменов, ни параметров в адресе. Сделай: 1. В base.html — блок ссылок на языковые версии текущей страницы (rel="alternate") плюс версия по умолчанию для случая, когда язык посетителя не совпал ни с одним. По умолчанию — русская версия. Коды языков подставь сам; я проверю их отдельно. 2. Переключатель языка в шапке: обычные ссылки, без JavaScript. Ведёт на ту же страницу в другом языке, а не на главную. 3. Переводим не всё. Есть языковые версии у: первого экрана, «Как я работаю», «Работы», «Контакт». Их нет у раздела «Архив» с роликами — они на русском, и украинская карточка русского ролика ничего не добавляет. Если для страницы нет версии на запрошенном языке — не выдумывай пустую страницу: веди на русскую версию и не ставь на неё языковую ссылку. 4. Проверка: пройди по всем маршрутам и выведи таблицу «адрес — язык — есть ли перевод — куда ведёт языковая ссылка».
-
15
16:42
The funnel: form, email, channel
1 What to do
A form of three fields, the request goes to the database and to Telegram, email on my own domain, a link to the channel.
2 Prompt
EN · PromptA form at /contact. Exactly three fields: task (multi-line), deadline (single line), contact (single line). Don't add more fields — every extra field costs replies. Do: 1. POST /contact — an ordinary form submission, no JavaScript. After submitting — a separate "thank you" page, not a pop-up message: you can set an analytics goal on it. 2. The request is saved to SQLite (file data/site.db, table leads: id, task, deadline, contact, time, page language, referrer) and at the same time goes as a message to my Telegram via a bot. The token and chat_id come from environment variables, they're not in the code. Read them via os.environ, .env doesn't go into the repository. 3. If Telegram is unavailable — the request is still saved in the database, and the error is written to the log. A request must not be lost because the bot went down. 4. Simple bot protection without a captcha: a hidden honeypot field and a check that the form was submitted no sooner than three seconds after the page loaded. 5. Email on my own domain, a mailto link next to the form. The Telegram link leads to the channel, not to direct messages.
EN · Prompt from the screenФорма на /contact. Ровно три поля: задача (многострочное), срок (одна строка), контакт (одна строка). Больше полей не добавлять — каждое лишнее поле стоит ответов. Сделай: 1. POST /contact — обычная отправка формы, без JavaScript. После отправки — отдельная страница «спасибо», а не всплывающее сообщение: на неё можно поставить цель в аналитике. 2. Заявка сохраняется в SQLite (файл data/site.db, таблица leads: id, задача, срок, контакт, время, язык страницы, источник перехода) и одновременно уходит сообщением в мой телеграм через бота. Токен и chat_id — из переменных окружения, в коде их нет. Читай их через os.environ, .env в репозиторий не попадает. 3. Если телеграм недоступен — заявка всё равно сохранена в базе, а ошибка записана в лог. Потерять заявку из-за упавшего бота нельзя. 4. Простая защита от роботов без капчи: скрытое поле-приманка и проверка, что форма отправлена не раньше чем через три секунды после загрузки страницы. 5. Почта — на своём домене, ссылка mailto рядом с формой. Ссылка на телеграм ведёт в канал, не в личку.
-
16
17:21
Four events in analytics
1 What to do
Instead of a visit counter, four events: came from YouTube, read a work to the end, submitted the form, went to Telegram.
-
17
17:51
Deploy to my own VPS
1 What to do
The portal goes up as a new website on the server: the application under a service, a certificate with one command.
-
18
18:35
Before and after
1 What to do
The old and new website side by side: the same page searches and the sections of the new structure.
-
19
19:49
Where it breaks
-
20
20:58
Homework and what's next
1 What to do
Go through your own website using the audit checklist, assemble the "after" structure and the first-screen line.