Моя історія: Як я став менеджером власного хобі
Статичний генератор сайтів (SSG), побудований на базі фреймворку Flask,
який кожен раз після внесення в нього змін перезбирає частини сайту
+- за мить (https://eggfamily.com.ua)
Так, почну з самого початку. Я не програміст, і все, що я знав про сайти — це поверхневі CSS та HTML, які писав ще в студентські часи. Я не знав, з чого почати та як працювати з хостингом, але мав досвід у менеджменті, тому — заменеджерив сам себе.
Наявних знань для того, щоб швидко «перелити» базу даних книг, сформовану на маркетплейсі, було замало. Тому я склав гнучкий і самовпевнений план без конкретики:
- Зробити проєкт максимально цікавим для себе (люблю незвичні завдання).
- Свобода дій: я не хотів залежати від платформ-конструкторів.
- Сайт на все життя: амбітний план щодо ведення блогу (цікаво, чи вистачить мене надовго?).
- GO!
Вивчити весь код так швидко, щоб реалізувати такий сайт за місяць-два — нереально. Тому на допомогу прийшов ШІ. Я використовував Gemini, щоб зрозуміти логіку програмування на Python: як поєднати базу даних, генератор та сайт.
Почав із вибору середовища — взяв Visual Studio Code. Вже наприкінці дізнався, що в ній є ШІ-агенти, які б прискорили процес, але це й на краще: тепер я справді розумію, як усе працює.
Я сформував початкову базу даних: папку з зображеннями та окремий файл JSON (картки товарів було витягнуто з акаунту на маркетплейсі). Оскільки початковий формат файлу був XML, я написав окремий код для цієї задачі. Нижче орієнтовний код, який ви можете підлаштувати під себе:
Чому я обрав JSON базу для Egg Family?
При розробці я відмовився від традиційних серверних баз даних на користь формату JSON. Це рішення стало основою архітектури сайту-генератора. Ось 5 ключових причин такого вибору:
- Він зчитує дані з JSON-файлів;
- Наповнює ними шаблони Jinja2;
- Створює готові HTML-сторінки.
1. Відсутність серверної бази даних
Я мінімізував залежність від стороннього програмного забезпечення на сервері. Весь контент зберігається у простому текстовому форматі.
2. Пряме читання в Python
Мова Python має чудову вбудовану підтримку JSON. Скрипт
generator.py (про нього в наступних частинах блогу) завантажує дані безпосередньо у структури даних Python.
3. Архітектура «Сайт-генератор»
Основою мого проєкту є скрипт
generator.py, який працює як статичний генератор (SSG):
4. Економічність та продуктивність
Завдяки тому, що веб-сервер просто віддає вже готові статичні файли, навантаження на процесор сервера практично відсутнє.
5. NoSQL підхід: гнучкість даних
Формат JSON не має жорсткої схеми. Це дозволяє легко додавати нові поля до товарів без переналаштування всієї бази.
Продовження про мій сайт — в наступній частині!
▶ Натисніть, щоб розгорнути код Python (База JSON та імпорт фото)
import xml.etree.ElementTree as ET
import json, os, re, unicodedata, requests
YML_FILE = "products_feed.xml"
DB_FILE = "database.json"
IMAGES_DIR = "images"
DEFAULT_CATEGORIES = {
"all": {"id": "all", "name": "Всі книги", "parent": None},
"131265796": {"id": "131265796", "name": "Кулінарія", "parent": "all"},
"127535484": {"id": "127535484", "name": "Прикраси \"hand made\"", "parent": None},
"111638377": {"id": "111638377", "name": "Художня література", "parent": "all"}
}
def slugify(text):
text = unicodedata.normalize("NFKD", text.lower()).encode("ascii", "ignore").decode("ascii")
return re.sub(r"\s+", "-", re.sub(r"[^a-z0-9\s-]", "", text).strip())
def get_text_anywhere(offer, *tag_names):
for elem in offer:
if any(elem.tag.lower().endswith(name.lower()) for name in tag_names):
return elem.text.strip() if elem.text else ""
return ""
def build_database():
if not os.path.exists(YML_FILE): return
existing_db = {}
if os.path.exists(DB_FILE):
with open(DB_FILE, "r", encoding="utf-8") as f: existing_db = json.load(f)
os.makedirs(IMAGES_DIR, exist_ok=True)
root = ET.parse(YML_FILE).getroot()
products, used_slugs = [], set()
for offer in root.findall(".//offer"):
name, price = get_text_anywhere(offer, "name"), get_text_anywhere(offer, "price")
vendor = offer.get("id") or get_text_anywhere(offer, "vendorCode")
if not name or not price: continue
img_name, pic_elem = "no-image.jpg", offer.find("picture")
if pic_elem is not None:
image_url = pic_elem.text.strip()
img_name = image_url.split("/")[-1]
img_path = os.path.join(IMAGES_DIR, img_name)
if not os.path.exists(img_path):
try:
res = requests.get(image_url, stream=True)
if res.status_code == 200:
with open(img_path, 'wb') as f:
for chunk in res.iter_content(1024): f.write(chunk)
except: pass
slug = slugify(name)
if slug in used_slugs: slug = f"{slug}-{vendor}"
used_slugs.add(slug)
products.append({
"id": vendor, "slug": slug, "name": name, "price": int(float(price)),
"category": get_text_anywhere(offer, "categoryId") or "all",
"description": get_text_anywhere(offer, "description"),
"image": img_name, "images": [img_name], "status": "in_stock", "params": {}
})
new_db = {
"categories": existing_db.get("categories", DEFAULT_CATEGORIES),
"products": products,
}
with open(DB_FILE, "w", encoding="utf-8") as f: json.dump(new_db, f, ensure_ascii=False, indent=4)
if __name__ == "__main__": build_database()