| 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 |
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) |