SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%

SEO : pages programmatiques /plateformes + /plateforme/{p} rendues en HTML

- creaka/seo.py : menu des plateformes + top 100 créateurs par plateforme
  (ItemList + BreadcrumbList, contenu dans #app), sitemap-pages enrichi
  (plateformes >= 3 créateurs)
- frontend : routeur SPA — /plateforme/{p} ouvre l annuaire filtré sur la
  plateforme ; shell reconstruit (build_frontend.py)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 0e13e08

5 changed files +124 −4

modified creaka/seo.py +116 −4
@@ -290,6 +290,114 @@ for _path, (_t, _d, _noidx) in _STATIC_PAGES.items():
290 290 router.get(_path, include_in_schema=False)(_mk())
291 291
292 292
293 +# --- Pages programmatiques plateformes : /plateformes + /plateforme/{p} ---------
294 +
295 +# seuil d'inclusion d'une page plateforme (pages trop maigres exclues du sitemap)
296 +MIN_CREATORS_PAGE = 3
297 +
298 +
299 +def _platform_rows() -> list[dict]:
300 + """[{platform, label, n (créateurs actifs)}] trié par volume décroissant."""
301 + rows = _db().execute(
302 + """SELECT a.platform, COUNT(DISTINCT a.creator_id) n
303 + FROM accounts a JOIN creators c ON c.id=a.creator_id
304 + WHERE c.status='active' AND c.is_minor=0
305 + GROUP BY a.platform ORDER BY n DESC""").fetchall()
306 + return [{"platform": r["platform"],
307 + "label": PLAT_LABEL.get(r["platform"], r["platform"]),
308 + "n": r["n"]}
309 + for r in rows if r["platform"] in PLAT_LABEL]
310 +
311 +
312 +@router.get("/plateformes", include_in_schema=False)
313 +def plateformes_page() -> HTMLResponse:
314 + plats = _platform_rows()
315 + total = sum(p["n"] for p in plats)
316 + title = (f"Créateurs de contenu québécois par plateforme "
317 + f"({len(plats)} plateformes) | Créa-Ka")
318 + description = (
319 + f"L'annuaire Créa-Ka par plateforme : {_fmt_int(total)} présences de "
320 + f"créateurs québécois sur {len(plats)} plateformes — YouTube, "
321 + "Instagram, TikTok, Twitch, Facebook, X et plus.")
322 + body = (f"<h1>Créateurs québécois par plateforme — {len(plats)} "
323 + "plateformes</h1><ul>"
324 + + "".join(
325 + f'<li><a href="/plateforme/{p["platform"]}">Créateurs québécois '
326 + f'sur {_e(p["label"])}</a> — {_fmt_int(p["n"])} créateurs</li>'
327 + for p in plats)
328 + + '</ul><p><a href="/">Annuaire des créateurs de contenu '
329 + "québécois</a></p>")
330 + jsonld = [_breadcrumb([("Accueil", "/"), ("Plateformes", "/plateformes")]),
331 + {"@context": "https://schema.org", "@type": "ItemList",
332 + "name": "Créateurs de contenu québécois par plateforme",
333 + "numberOfItems": len(plats),
334 + "itemListElement": [
335 + {"@type": "ListItem", "position": i + 1,
336 + "name": f"Créateurs québécois sur {p['label']}",
337 + "url": f"{BASE_URL}/plateforme/{p['platform']}"}
338 + for i, p in enumerate(plats)]}]
339 + return _render(title=title, description=description, path="/plateformes",
340 + jsonld=jsonld, body=body)
341 +
342 +
343 +@router.get("/plateforme/{plat}", include_in_schema=False)
344 +def plateforme_page(plat: str) -> HTMLResponse:
345 + if plat not in PLAT_LABEL:
346 + return _not_found(f"/plateforme/{plat}")
347 + label = PLAT_LABEL[plat]
348 + rows = _db().execute(
349 + """SELECT c.id, c.display_name, a.handle, a.followers
350 + FROM accounts a JOIN creators c ON c.id=a.creator_id
351 + WHERE a.platform=? AND c.status='active' AND c.is_minor=0
352 + ORDER BY a.followers IS NULL, a.followers DESC LIMIT 100""",
353 + (plat,)).fetchall()
354 + n = _db().execute(
355 + """SELECT COUNT(DISTINCT a.creator_id) n
356 + FROM accounts a JOIN creators c ON c.id=a.creator_id
357 + WHERE a.platform=? AND c.status='active' AND c.is_minor=0""",
358 + (plat,)).fetchone()["n"]
359 + path = f"/plateforme/{plat}"
360 + if n == 0:
361 + return _not_found(path)
362 +
363 + title = f"Créateurs québécois sur {label} — {_fmt_int(n)} profils | Créa-Ka"
364 + description = (
365 + f"{_fmt_int(n)} créateurs de contenu québécois actifs sur {label}, "
366 + "classés par audience : profils, niches, autres plateformes et "
367 + "statistiques dans l'annuaire Créa-Ka.")
368 + lis = []
369 + for r in rows:
370 + extra = (f" — {_fmt_int(r['followers'])} abonnés"
371 + if r["followers"] else "")
372 + handle = f" (@{_e(r['handle'])})" if r["handle"] else ""
373 + lis.append(f'<li><a href="/createur/{_e(quote(r["id"]))}">'
374 + f'{_e(r["display_name"])}</a>{handle}{extra}</li>')
375 + others = [p for p in _platform_rows() if p["platform"] != plat][:12]
376 + body = (f"<h1>Créateurs québécois sur {_e(label)} — {_fmt_int(n)} "
377 + "profils</h1>"
378 + f"<p>Les créateurs de contenu québécois présents sur {_e(label)}, "
379 + "classés par taille d'audience. Chaque fiche relie tous les "
380 + "comptes du créateur sur les autres plateformes.</p>"
381 + "<h2>Profils</h2><ul>" + "".join(lis) + "</ul>"
382 + + ("<h2>Autres plateformes</h2><ul>" + "".join(
383 + f'<li><a href="/plateforme/{p["platform"]}">{_e(p["label"])}</a>'
384 + f' — {_fmt_int(p["n"])}</li>' for p in others) + "</ul>"
385 + if others else "")
386 + + '<p><a href="/plateformes">Toutes les plateformes</a> · '
387 + '<a href="/">Annuaire des créateurs québécois</a></p>')
388 + crumbs = [("Accueil", "/"), ("Plateformes", "/plateformes"), (label, path)]
389 + jsonld = [_breadcrumb(crumbs),
390 + {"@context": "https://schema.org", "@type": "ItemList",
391 + "name": f"Créateurs québécois sur {label}", "numberOfItems": n,
392 + "itemListElement": [
393 + {"@type": "ListItem", "position": i + 1,
394 + "name": r["display_name"],
395 + "url": f"{BASE_URL}/createur/{quote(r['id'])}"}
396 + for i, r in enumerate(rows[:50])]}]
397 + return _render(title=title, description=description, path=path,
398 + jsonld=jsonld, body=body)
399 +
400 +
293 401 # --- robots.txt & sitemaps -------------------------------------------------------
294 402
295 403 @router.get("/robots.txt", include_in_schema=False)
@@ -339,10 +447,14 @@ def sitemap_pages() -> Response:
339 447 last = _db().execute("SELECT MAX(updated_at) m FROM creators "
340 448 "WHERE status='active'").fetchone()["m"]
341 449 lm = (last or "")[:10] or None
342 − return _urlset([(BASE_URL + "/", lm),
343 − (BASE_URL + "/stats", lm),
344 − (BASE_URL + "/retrait", None),
345 − (BASE_URL + "/contact", None)])
450 + urls = [(BASE_URL + "/", lm),
451 + (BASE_URL + "/plateformes", lm),
452 + (BASE_URL + "/stats", lm),
453 + (BASE_URL + "/retrait", None),
454 + (BASE_URL + "/contact", None)]
455 + urls += [(f"{BASE_URL}/plateforme/{p['platform']}", lm)
456 + for p in _platform_rows() if p["n"] >= MIN_CREATORS_PAGE]
457 + return _urlset(urls)
346 458
347 459
348 460 @router.get("/sitemap-createurs-{n}.xml", include_in_schema=False)
modified frontend/dist/index.html +2 −0
@@ -2346,9 +2346,11 @@ function bindNav(root){
2346 2346 function route(){
2347 2347 const p=location.pathname;
2348 2348 const m=p.match(/^\/createur\/([^/]+)$/);
2349 + const pl=p.match(/^\/plateforme\/([^/]+)$/); /* pages SEO /plateforme/{p} */
2349 2350 const qq=new URLSearchParams(location.search).get("q");if(qq&&!state.q)state.q=qq;
2350 2351 let r;
2351 2352 if(m)r=renderCreator(decodeURIComponent(m[1]));
2353 + else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();}
2352 2354 else if(p==="/retrait")r=renderOptout();
2353 2355 else if(p==="/stats")r=renderStats();
2354 2356 else if(p==="/compte")r=renderCompte();
modified frontend/dist/ka-agent.js +2 −0
@@ -454,3 +454,5 @@
454 454 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); }
455 455 });
456 456 })();
457 +
458 +;/*KA_ANALYTICS*/(function(){try{if(window.__kaAn)return;window.__kaAn=1;var k="KA_VID",v=localStorage.getItem(k);if(!v){v=Date.now().toString(36)+Math.random().toString(36).slice(2,8);localStorage.setItem(k,v);}var b=function(){var i=new Image();i.src="https://www.administration-ka.com/collect?site=crea-ka&vid="+v+"&p="+encodeURIComponent(location.pathname)+"&t="+Date.now();};b();setInterval(b,60000);}catch(e){}})();
modified frontend/src/index.template.html +2 −0
@@ -1708,9 +1708,11 @@ function bindNav(root){
1708 1708 function route(){
1709 1709 const p=location.pathname;
1710 1710 const m=p.match(/^\/createur\/([^/]+)$/);
1711 + const pl=p.match(/^\/plateforme\/([^/]+)$/); /* pages SEO /plateforme/{p} */
1711 1712 const qq=new URLSearchParams(location.search).get("q");if(qq&&!state.q)state.q=qq;
1712 1713 let r;
1713 1714 if(m)r=renderCreator(decodeURIComponent(m[1]));
1715 + else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();}
1714 1716 else if(p==="/retrait")r=renderOptout();
1715 1717 else if(p==="/stats")r=renderStats();
1716 1718 else if(p==="/compte")r=renderCompte();
modified frontend/src/ka-agent.js +2 −0
@@ -454,3 +454,5 @@
454 454 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); ask(); }
455 455 });
456 456 })();
457 +
458 +;/*KA_ANALYTICS*/(function(){try{if(window.__kaAn)return;window.__kaAn=1;var k="KA_VID",v=localStorage.getItem(k);if(!v){v=Date.now().toString(36)+Math.random().toString(36).slice(2,8);localStorage.setItem(k,v);}var b=function(){var i=new Image();i.src="https://www.administration-ka.com/collect?site=crea-ka&vid="+v+"&p="+encodeURIComponent(location.pathname)+"&t="+Date.now();};b();setInterval(b,60000);}catch(e){}})();
457 459