Post

Як я зробив сайт який кожного разу інший: частина 1

Як я зробив сайт який кожного разу інший: частина 1

Одного дня я подумав: а що якби сайт при кожному відкритті показував щось нове? Не просто рандомну сторінку з бази — а повністю згенерований контент: унікальна стаття з картинкою, інший колір, інший лейаут. Щоразу інший.

Так народився madtech.work — сайт про комп’ютерні технології з гумором, де кожне відвідування це окремий досвід.

У цій серії з трьох статей я розповім як це зробити з нуля на VPS з CyberPanel та OpenLiteSpeed.

Що в результаті вийде

  • FastAPI бекенд на Python
  • Groq API (безкоштовно) для генерації тексту — модель Llama 3.3 70B
  • Pollinations.ai (безкоштовно, без ключа) для генерації зображень
  • SSE стримінг — текст з’являється поступово, як у ChatGPT
  • 6 випадкових тем з різними кольорами та лейаутами
  • Автоматичне визначення мови браузера

Частина 1: Структура проєкту і FastAPI

Що потрібно

  • VPS з CyberPanel (OpenLiteSpeed)
  • Python 3.9+
  • Безкоштовний акаунт на console.groq.com

Структура файлів

1
2
3
4
5
6
7
8
9
10
/home/madtech.work/
├── app/
│   ├── main.py
│   ├── requirements.txt
│   ├── .env
│   ├── static/
│   │   └── favicon.svg
│   └── templates/
│       └── index.html
└── venv/

Встановлення залежностей

1
2
3
4
5
6
7
8
9
10
# Створюємо директорію
mkdir -p /home/madtech.work/app
cd /home/madtech.work/app

# Віртуальне середовище
python3 -m venv /home/madtech.work/venv
source /home/madtech.work/venv/bin/activate

# Залежності
pip install fastapi uvicorn openai httpx python-dotenv jinja2 aiofiles

Важливо для Python 3.9: бібліотека openai нових версій конфліктує з httpx. Фіксуємо версії:

1
pip install "openai==1.54.4" "httpx==0.27.2"

Основний файл main.py

Ключова ідея: Groq має OpenAI-сумісний API, тому використовуємо AsyncOpenAI просто змінивши base_url:

1
2
3
4
5
6
from openai import AsyncOpenAI

client = AsyncOpenAI(
    api_key=os.getenv("GROQ_API_KEY"),
    base_url="https://api.groq.com/openai/v1",
)

Для генерації контенту просимо модель повертати строго JSON — це дозволяє парсити відповідь прямо під час стримінгу:

1
2
3
4
5
6
system_prompt = (
    f"You are a witty tech blogger. Write EXCLUSIVELY {lang_name}. "
    f"Response format — STRICTLY valid JSON (no markdown, no extra text):\n"
    f'title\n'
    f"body must contain 4-6 paragraphs in <p> tags."
)

SSE стримінг

Замість звичайного HTTP-відповіді використовуємо Server-Sent Events — браузер отримує дані шматками в реальному часі:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@app.get("/generate")
async def generate(lang: str = "en"):
    async def stream():
        # Спочатку відправляємо URL зображення
        yield f"data: {json.dumps({'type': 'image', 'url': image_url})}\n\n"

        # Потім стримімо текст
        stream_response = await client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            stream=True,
            messages=[...]
        )
        async for chunk in stream_response:
            delta = chunk.choices[0].delta.content or ""
            if delta:
                yield f"data: {json.dumps({'type': 'text', 'chunk': delta})}\n\n"

        yield f"data: {json.dumps({'type': 'done'})}\n\n"

    return StreamingResponse(
        stream(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )

Заголовок X-Accel-Buffering: no критично важливий — без нього nginx/OLS буферизує SSE і стримінг не працює.

Зображення через Pollinations.ai

Для зображень використовуємо безкоштовний сервіс без реєстрації — просто формуємо URL:

1
2
3
4
5
6
7
import urllib.parse, random

def get_image_url(topic: str) -> str:
    prompt = f"funny tech illustration about: {topic}, digital art"
    encoded = urllib.parse.quote(prompt)
    seed = random.randint(1, 999999)
    return f"https://image.pollinations.ai/prompt/{encoded}?width=1792&height=1024&seed={seed}&nologo=true"

Параметр seed гарантує що кожен раз зображення нове, навіть для однієї теми.

.env файл

1
GROQ_API_KEY=gsk_ваш_ключ_тут

Ключ отримати безкоштовно на console.groq.com/keys. Groq дає 6000 запитів на день — більш ніж достатньо.

Перший запуск

1
2
3
source /home/madtech.work/venv/bin/activate
cd /home/madtech.work/app
uvicorn main:app --host 127.0.0.1 --port 8001
1
2
3
# В іншому терміналі перевіряємо
curl http://127.0.0.1:8001/
# Має повернути HTML

У частині 2 розберемо як налаштувати OpenLiteSpeed в CyberPanel щоб проксіювати запити на FastAPI — це виявилось найцікавішою частиною.

This post is licensed under CC BY 4.0 by the author.