From 7c0c2ea14a80b5cb2c9d158bbf78a7ca8eaa6dc5 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Fri, 15 May 2026 15:54:39 +0300 Subject: [PATCH] feat: vendor pages /vendor/ with SEO, sitemap updated, links from main --- matrix.db | Bin 352256 -> 356352 bytes static/js/index.js | 15 +++- templates/vendor.html | 194 ++++++++++++++++++++++++++++++++++++++++++ zkart_app/db.py | 2 +- zkart_app/routes.py | 58 ++++++++++++- 5 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 templates/vendor.html diff --git a/matrix.db b/matrix.db index e5e5217b9db3c86f83e429b31d310519ccb9099f..93ff5b01e9d2a6760369439a556863cd26444bc6 100644 GIT binary patch delta 5248 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 G6Z1ce6&0)i delta 3945 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 diff --git a/static/js/index.js b/static/js/index.js index 8c277cf..aeb778d 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -296,7 +296,8 @@ ? `${v.name}` : `${v.name.slice(0,2).toUpperCase()}`; let linksHtml = ''; - if (v.mont_page) linksHtml += `MONT ↗`; + if (v.slug) linksHtml += `Подробнее →`; + if (v.mont_page) linksHtml += `MONT ↗`; if (v.website) linksHtml += `Сайт ↗`; bar.innerHTML = ` @@ -313,7 +314,17 @@ const card = document.createElement("article"); card.className = "row-card"; const title = document.createElement("strong"); - title.textContent = row.vendor.name; + if (row.vendor.slug) { + const a = document.createElement("a"); + a.href = `/vendor/${row.vendor.slug}`; + a.textContent = row.vendor.name; + a.style.cssText = "color:inherit;text-decoration:none;"; + a.addEventListener("mouseenter", () => a.style.textDecoration = "underline"); + a.addEventListener("mouseleave", () => a.style.textDecoration = "none"); + title.appendChild(a); + } else { + title.textContent = row.vendor.name; + } card.appendChild(title); const tags = document.createElement("div"); tags.className = "tags"; diff --git a/templates/vendor.html b/templates/vendor.html new file mode 100644 index 0000000..6436c9e --- /dev/null +++ b/templates/vendor.html @@ -0,0 +1,194 @@ + + + + + + {{ vendor.name }} — вендор МОНТ | матрица продуктов MONT + + + + + + + + + {% if vendor.logo %}{% endif %} + + + + + + + + + + +
+ ← Все вендоры МОНТ + + + +
+
+ {% if vendor.logo %} + {{ vendor.name }} логотип + {% else %} + {{ vendor.name[:2].upper() }} + {% endif %} +
+
+

{{ vendor.name }}

+ {% if vendor.description %} +

{{ vendor.description }}

+ {% endif %} + +
+
+ + {% if products %} +
+

Продукты {{ vendor.name }} в корзине МОНТ

+
+ {% for p in products %} + {% if p.url %} + {{ p.name }} ↗ + {% else %} + {{ p.name }} + {% endif %} + {% endfor %} +
+
+ {% endif %} + + {% if categories %} +
+

Категории

+ {% for c in categories %} + {{ c }} + {% endfor %} +
+ {% endif %} +
+ + diff --git a/zkart_app/db.py b/zkart_app/db.py index efa9b54..ac759e2 100644 --- a/zkart_app/db.py +++ b/zkart_app/db.py @@ -411,7 +411,7 @@ def fetch_scope_data(scope: str) -> dict: conn = get_db() vendors = [dict(r) for r in conn.execute( f"SELECT id, name, COALESCE(logo,'') as logo, COALESCE(description,'') as description, " - f"COALESCE(website,'') as website, COALESCE(mont_page,'') as mont_page " + f"COALESCE(website,'') as website, COALESCE(mont_page,'') as mont_page, COALESCE(slug,'') as slug " f"FROM {tables['vendors']} ORDER BY lower(name)" )] categories = [dict(r) for r in conn.execute(f"SELECT id, name FROM {tables['categories']} ORDER BY lower(name)")] diff --git a/zkart_app/routes.py b/zkart_app/routes.py index 8b306a0..f5ca6b5 100644 --- a/zkart_app/routes.py +++ b/zkart_app/routes.py @@ -582,15 +582,69 @@ def sitemap_xml(): from flask import Response proto = request.headers.get("X-Forwarded-Proto", "https") base = f"{proto}://{request.host}" + conn = get_db() + slugs = [r[0] for r in conn.execute( + "SELECT slug FROM vendors WHERE slug IS NOT NULL " + "UNION SELECT slug FROM ib_vendors WHERE slug IS NOT NULL" + )] + conn.close() + urls = [f' {base}/weekly1.0'] + for slug in slugs: + urls.append(f' {base}/vendor/{slug}monthly0.7') body = ( '\n' '\n' - f' {base}/weekly1.0\n' - '' + + '\n'.join(urls) + '\n' ) return Response(body, mimetype="application/xml") +@bp.get("/vendor/") +def vendor_page(slug: str): + from flask import abort + conn = get_db() + # Search in both tables, prefer infra + vendor = conn.execute( + "SELECT id, name, COALESCE(logo,'') as logo, COALESCE(description,'') as description, " + "COALESCE(website,'') as website, COALESCE(mont_page,'') as mont_page, 'infra' as scope " + "FROM vendors WHERE slug = ?", (slug,) + ).fetchone() + if not vendor: + vendor = conn.execute( + "SELECT id, name, COALESCE(logo,'') as logo, COALESCE(description,'') as description, " + "COALESCE(website,'') as website, COALESCE(mont_page,'') as mont_page, 'ib' as scope " + "FROM ib_vendors WHERE slug = ?", (slug,) + ).fetchone() + if not vendor: + conn.close() + abort(404) + + vendor = dict(vendor) + tables = scope_tables(vendor['scope']) + + products = [dict(r) for r in conn.execute( + f"SELECT name, COALESCE(url,'') as url FROM {tables['products']} " + f"WHERE vendor_id = ? ORDER BY lower(name)", (vendor['id'],) + )] + categories = [r[0] for r in conn.execute( + f"SELECT c.name FROM {tables['categories']} c " + f"JOIN {tables['vendor_categories']} vc ON vc.category_id = c.id " + f"WHERE vc.vendor_id = ? ORDER BY lower(c.name)", (vendor['id'],) + )] + conn.close() + + proto = request.headers.get("X-Forwarded-Proto", "https") + base_url = f"{proto}://{request.host}" + canonical_url = f"{base_url}/vendor/{slug}" + return render_template("vendor.html", + vendor=vendor, + products=products, + categories=categories, + canonical_url=canonical_url, + base_url=base_url, + ) + + @bp.get("/api/data") def api_data(): scope = (request.args.get("scope") or "infra").strip().lower()