Bu saytni Claude Code bilan qanday qurganman
by ada · 2026-05-01 · edited 2026-05-01
Bu saytni Claude Code bilan qanday qurganman
O'qib turgan ushbu blog — loyihaning o'zi. Bu kichik portfolio uchun mo'ljallangan namuna ish bo'lib, men uni Anthropic'ning Claude Code (Claude uchun CLI) yordamida bir o'tirishda yig'ib chiqdim. Yozishimning maqsadi — ichkaridagi narsalarni ko'rsatish, shunda bu yozuvni o'qigan har kim uni qismlarga bo'lib o'rgana oladi.
Tizim shakli
Bitta port orqasidagi olti konteyner:
:8080 ──▶ gateway (nginx) ──┬─▶ /auth/* auth (Flask) ─┐
├─▶ /api/* blog (Flask) ─┼─▶ postgres
├─▶ /media/* blog (static) ─┘
└─▶ qolgani web (Flask + Jinja2 + HTMX)
docker-compose.yml ularni bir-biriga ulaydi. Nginx — bu yo'lning oldidagi yengil teskari proxy bo'lib, prefiks bo'yicha marshrutlaydi. Auth servis foydalanuvchilarni boshqaradi. Blog servis postlar va yuklangan rasmlar uchun. Web servis esa yagona HTML render qiladigan qism — u auth va blog bilan ichki Docker tarmog'i orqali muloqot qiladi. Postgres'da ikkita mantiqiy ma'lumotlar bazasi bor — har bir servis o'ziga tegishli sxemasiga egalik qiladi.
Aynan shu — chinakam mikroservis hikoyasi: har bir Flask ilovasi mustaqil deploy qilinadi, o'z migration tarixiga ega, va boshqalar bilan faqat HTTP orqali gaplashadi.
Servislar orasida auth qanday oqadi
Foydalanuvchi tizimga kiradi. Web servis /auth/login'ga POST qiladi. Auth servis bcrypt hashni tekshiradi va HS256 bilan, env'dan olingan umumiy maxfiy kalit yordamida imzolangan JWT qaytaradi. Web servis bu JWT'ni HttpOnly, SameSite=Lax sessiya cookie'sida saqlaydi.
Foydalanuvchi post yaratganda, web servis JWT'ni Authorization: Bearer <token> sarlavhasida blog servisga uzatadi. Blog servis JWT'ni lokal tarzda — xuddi shu umumiy maxfiy kalit bilan tasdiqlaydi, ya'ni har bir so'rovda auth'ga qaytib bormaydi. Bu standart JWT yondashuvi: bekor qilish moslashuvchanligini latency uchun almashtirish.
JWT'ning o'zi foydalanuvchi id'sini (sub da'vosi) va username'ni (biz issuance vaqtida qo'shadigan maxsus claim) olib yuradi. Blog servis ikkalasiga ham ishonadi, chunki imzo ularning auth'dan kelganligini isbotlaydi.
Pagination qanday qilingan
Pagination services/blog/app/routes.py ichida joylashgan:
@bp.get("/api/posts")
def list_posts():
page = max(1, int(request.args.get("page", 1)))
page_size = current_app.config["PAGE_SIZE"]
stmt = select(Post).where(Post.published.is_(True)).order_by(desc(Post.created_at))
total = db.session.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
posts = db.session.execute(stmt.offset((page - 1) * page_size).limit(page_size)).scalars().all()
return jsonify(items=[p.to_dict() for p in posts], page=page, page_size=page_size, total=total)
Ikkita query: bittasi sahifa bo'lagi uchun (offset + limit), ikkinchisi umumiy soni uchun (front-end "Sahifa 2 / 5" ni chiza olishi uchun). Web servis bu sonni iste'mol qiladi va Jinja shabloniga uzatadi, shablon esa orqaga/oldinga tugmalarini faqat ma'noli bo'lganda chizadi:
{% set total_pages = (total + page_size - 1) // page_size %}
{% if total_pages > 1 %}
...
{% endif %}
(total + page_size - 1) // page_size — bu standart "yuqoriga yaxlitlash" idiomasi: faqat butun sonlar bilan ceil(total / page_size) ni hisoblashning kalkulyatorsiz usuli.
Rasm yuklash qanday ishlaydi
O'xshab ko'ringan, lekin aslida boshqa-boshqa ikkita yo'l bor:
- To'g'ridan-to'g'ri API yuklash — Bearer token va multipart
filemaydoni bilanPOST /api/images. Avtomatlashtirilgan chaqiriqchilar uchun. - Brauzer muharriri yuklashi — EasyMDE Markdown muharriridagi rasm tugmasi web servisning
/upload/image'iga POST qiladi, u esa blog servisga proxy qiladi. Brauzer faqat CSRF tokenni ko'radi; JWT esa HttpOnly cookie'da yashaydi va server tomonidan qo'shiladi.
Bu ikkinchi yo'l muhim: agar brauzer JWT'ni to'g'ridan-to'g'ri /api/images'ga yuborishi kerak bo'lganida, biz JWT'ni JavaScript'ga ochishimizga to'g'ri kelar edi, ya'ni har qanday uchinchi tomon kutubxonadagi XSS xatosi uni o'g'irlashi mumkin edi. Web servis orqali proxy qilish bilan JWT cookie'dan hech qachon chiqmaydi.
Blog servis yuklamani Pillow yordamida tasdiqlaydi:
img = PILImage.open(io.BytesIO(raw))
img.verify() # tuzilmaviy aql tekshiruvi
img = PILImage.open(io.BytesIO(raw)) # qayta ochish — verify() oqimni tugatadi
if img.format not in ALLOWED_FORMATS: raise ImageError(...)
if img.width > max_width:
img = img.resize((max_width, int(img.height * max_width / img.width)), PILImage.LANCZOS)
Keyin yangi uuid4().hex fayl nomini yasaydi, /var/media'ga ulangan Docker named volume'ga yozadi va {"url": "/media/<filename>"} qaytaradi. Asl fayl nomi diskka tegmaydi — yo'l traversal va "men ../etc/passwd deb nomlangan faylni yukladim" kabi sho'xliklar shu tarzda bartaraf etiladi.
Xavfsiz Markdown render qilish qanday ishlaydi
Foydalanuvchi yozgan Markdown'ni xom holda render qilib bo'lmaydi — <script> hammaning brauzerida ishga tushadi. Ikki kutubxona bu ishni bajaradi:
raw_html = markdown.markdown(body, extensions=["fenced_code", "tables", "nl2br", "sane_lists"])
cleaned = bleach.clean(
raw_html,
tags=["a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li",
"strong", "em", "code", "pre", "blockquote", "img",
"table", "thead", "tbody", "tr", "th", "td", "span", "div", "br", "hr"],
attributes={"a": ["href", "title", "rel"], "img": ["src", "alt", "title", "width", "height"], ...},
protocols=["http", "https", "data", "mailto"],
strip=True,
)
Allowlist sukut bo'yicha ehtiyotkor. <script> ro'yxatda yo'q, demak olib tashlanadi. onclick hech qaysi tegning atribut ro'yxatida yo'q, demak olib tashlanadi. URL sxemalari faqat ma'lum bo'lgan xavfsizlariga cheklangan, shuning uchun javascript:alert(1) o'lik havola bo'lib qoladi. Omon qolganlari — bu foydalanuvchi haqiqatan ham yozgan narsadir.
Test to'plami uning ishlashini qanday isbotlaydi
Ikki to'plam, ikkalasi ham tirik docker compose up stack'ga qarshi ishlaydi:
tests/e2e/test_api.py — qora quti HTTP. Register, login, /me bo'yicha tekshirish; anonim uchun yozish 401 beradi; muallif o'z postini to'liq CRUD qila oladi, lekin boshqa foydalanuvchi tahrir/o'chirishda 403 oladi; rasm yuklash baytlar rasm sifatida dekod qilinadigan /media/... URL qaytaradi; pagination kutilgan sonlarni beradi; slug to'qnashuvlari avtomatik suffiks oladi.
tests/e2e/test_browser.py — Playwright orqali haqiqiy Chromium. Register oqimi → bosh sahifa; noto'g'ri parol → ko'rinadigan flash; muharrir orqali post yaratish (CodeMirror API'siga to'g'ridan-to'g'ri yozib); tahrirlash; o'chirish; boshqa birovning postini tahrir qila olmaslik (403 sahifasi); anonim o'qiy oladi.
Ikkala to'plam ham bitta Make maqsadi orqali ulanadi:
make test-e2e
Bu stack'ni ko'taradi, healthcheck'larni kutadi, migrationlarni ishga tushiradi, ikkala to'plamni ishlatadi, keyin to'xtatadi. Biror narsa muvaffaqiyatsiz bo'lsa, nolga teng bo'lmagan kod bilan chiqadi.
Yordamchi nima qildi va men nima qildim
Arxitektura, fayllar tuzilishi, xavfsizlik tanlovlari (bleach allowlist, yuklamalar uchun JWT proxy, formaning yashirin EasyMDE textarea'si yuborilishni bloklamasligi uchun novalidate), test to'plami, editorial Tailwind ko'rinishi — bularning hammasi bitta Claude Code seansidan chiqdi. Men yo'naltirdim: men jilolangan portfolio asari so'radim, u savol bergan joylarda variantlar tanladim va atrofiga bosib ko'rganimdan keyin tuzatish kerak bo'lgan ikkita narsani aytdim (cover rasm URL joylashtirilishi emas, yuklanadigan bo'lishi kerak edi; email_validator bitta servisning requirements'idan tushib qolgani uchun ro'yxatdan o'tish formasi to'g'ri kiritishni rad etayotgan edi).
Qiziq narsa LLM kod yozgani emas. Qiziq narsa — ish jarayoni: tizimni tasvirlab berdim, aniqlovchi savollarga javob berdim, plan yozilishini tomosha qildim, plan bajarilishini tomosha qildim, jonli stack ishlatdim, atrofidan bosib ko'rdim, ikkita bug yozdim, ularning tuzatilishini tomosha qildim. Vaqti-vaqti bilan code review kerak bo'ladigan tez harakatlanuvchi hamkasb bilan ishlashga o'xshaydi.
Sinab ko'rmoqchi bo'lsangiz: https://claude.com/claude-code.