feat(marketing): round 3 — hero remplacé par 3-step flow canonique + 99+ langues + Cégeps spotlight + CyberPerformance + FAQ enrichie

Hero (templates/marketing/landing.html) :
- Reproduction fidèle de dictia.ca/solutions/dictai (source : Website-Sanity/components/sections/dictai-page-content.tsx lignes 260-518)
- REMPLACE le mockup app DictIA par le 3-step flow inline canonique (Importez → Texte 2 min → Résumé + actions)
- Wordmark large « DictIA » (style production) + H2 cyan « Transcription IA locale en 2 minutes — conforme Barreau, CPA Québec et ChAD »
- Sub canonique référençant OVH Beauharnois, Cadre IA MCN, 5 ordres à directives IA formelles
- Stats grid (4 col) : ~2 min · 5 ordres · 95 %+ · 0 $ (NBSP OQLF)
- Eyebrow back-link « Toutes les solutions »
- 5 animations Framer Motion → CSS pure + Alpine.js :
  1. 3-step flow auto-cycle 1→2→3 (setInterval 1.8 s, désactivé reduced-motion)
  2. Magnetic CTA primary (mousemove → translate max 8 px)
  3. Mouse parallax orb 3D (mousemove window → CSS transition)
  4. Shockwave on click (CSS pseudo-element scale 0→4 + opacity)
  5. Word-staggered title reveal (Dict + IA via animation-delay)

Sections enrichies / ajoutées :
- Pipeline : sous-titre « Du fichier au résumé — en temps réel » + hint canonique
- NEW « 99+ langues détectées » + carte « IA Mistral 7B (LOCAL) » 4 bullets
- Pricing : sous-titres canoniques par forfait + note « Tous les prix en CAD, taxes en sus (TPS 5 % + TVQ 9,975 %) »
- Conformité : 3 chips claims (~192 000 pros · 5 ordres · 0 donnée hors-Québec) + phrase secteurs réglementés
- NEW Cégeps spotlight « Conformité au 19 juin 2026 » avec Cadre IA MCN détaillé (7 bullets, 9 chips organismes, badge pulse glow)
- NEW Partenaire CyberPerformance (card horizontale + lien externe)
- FAQ : enrichie de 7 → 10 questions canoniques sourcées de dictai-page-content.tsx (Teams Copilot, Otter.ai, Barreau, Clio Manage, etc.)
- CTA final : « Prêt à protéger vos données ? » + bouton « Réserver ma démo gratuite » (préserve mailto pré-inscription)

Tests :
- Ajout tests/conftest.py (stub fcntl POSIX + env vars test) pour permettre exécution sur Windows
- Mise à jour 8 assertions liées au nouveau hero, FAQ 10 Q, CTA renforcé, NBSP OQLF dans eyebrow
- 61 passed / 3 pré-existant échecs baseline (/blog dans nav + footer + trust-bar phrasing)

Contraintes respectées : zéro JS externe, aucun emoji (SVG inline aria-hidden), V3 radii (rounded/rounded-full), brand tokens, OQLF NBSP partout, WCAG (aria-labels, focus-visible, prefers-reduced-motion désactive toutes les animations hero).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-28 12:43:57 -04:00
parent 69baa1be2f
commit 529bd2263b
6 changed files with 770 additions and 465 deletions

View File

@@ -83,22 +83,30 @@ def test_landing_no_login_redirect_for_anonymous():
def test_hero_has_h1_with_grad_text_accent():
"""Hero H1 contains grad-text span on the brand tagline."""
"""Hero H1 (round 3) contains the brand wordmark with grad-text accent on 'IA'.
Round 3 replaces the old tagline ('sans risquer votre permis') with the canonical
DictIA wordmark + the H2 phrase 'Transcription IA locale en 2 minutes'.
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'id="hero-title"' in body, "Missing hero-title id on H1"
assert 'grad-text' in body, "Missing grad-text class somewhere"
assert 'sans risquer votre permis' in body, "Missing key brand tagline"
# New canonical brand H2 phrase (cyan/grad on key claim)
assert 'Transcription IA locale en 2' in body, "Missing canonical H2 phrase"
# Hero word-staggered reveal hook on the wordmark
assert 'hero-h1-word' in body, "Missing word-staggered reveal class"
def test_hero_has_dual_cta():
"""Hero has both primary (Réserver une démo) and ghost (Voir les tarifs) CTAs."""
"""Hero (round 3) has primary (Réserver une démo) and ghost (Voir les forfaits) CTAs."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'href="/contact"' in body
assert 'href="/tarifs"' in body
assert 'Réserver une démo' in body or 'R&eacute;server une d&eacute;mo' in body
assert 'Voir les tarifs' in body
# Round 3 canonical wording: 'Voir les forfaits' (matches dictia.ca/solutions/dictai)
assert 'Voir les forfaits' in body, "Round 3 secondary CTA must say 'Voir les forfaits'"
def test_hero_has_cosmic_orbs_background():
@@ -121,25 +129,28 @@ def test_hero_has_social_proof_microcopy():
def test_hero_has_staggered_animations():
"""Hero elements use tc-fade-in-up with staggered delays."""
"""Hero (round 3) elements use tc-fade-in-up with staggered delays — canonical cadence.
Round 3 staggers : 0 (back-link), 75 (eyebrow), 200 (3-step flow),
280 (H2 phrase), 360 (sub), 440 (stats), 520 (CTAs), 600 (social proof).
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'animate-tc-fade-in-up' in body, "Missing fade-in animation"
assert 'animation-delay: 0ms' in body
assert 'animation-delay: 75ms' in body
assert 'animation-delay: 150ms' in body
assert 'animation-delay: 300ms' in body
assert 'animation-delay: 400ms' in body
for delay in ['0ms', '75ms', '200ms', '280ms', '360ms', '440ms', '520ms', '600ms']:
assert f'animation-delay: {delay}' in body, f"Missing staggered delay {delay}"
assert 'animation-fill-mode: backwards' in body, \
"Missing animation-fill-mode (causes flash before delay fires)"
def test_hero_eyebrow_has_brand_messaging():
"""Hero eyebrow declares the 3 brand pillars."""
"""Hero eyebrow declares the 3 brand pillars (round 3 uses OQLF NBSP : LOI&nbsp;25)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'TRANSCRIPTION IA' in body
assert 'CONFORME LOI 25' in body
# OQLF-conformant : non-breaking space before "25" (NBSP entity)
assert 'CONFORME LOI&nbsp;25' in body or 'CONFORME LOI 25' in body, \
"Missing 'CONFORME LOI 25' eyebrow (with or without NBSP)"
assert 'QU' in body # Either QUÉBEC or QU&Eacute;BEC
@@ -665,31 +676,32 @@ def test_testimonials_use_personas_not_fake_names():
assert name not in body, f"Forbidden fabricated testimonial name: {name}"
def test_faq_section_with_7_questions():
"""FAQ section present with 7 questions."""
def test_faq_section_with_10_questions():
"""FAQ section (round 3) present with 10 canonical questions from dictai-page-content.tsx."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'faq-title' in body
# 7 panel IDs (loop.index is 1-indexed)
for i in range(1, 8):
# 10 panel IDs (loop.index is 1-indexed)
for i in range(1, 11):
assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}"
# Question topic anchors
topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats',
'résilie', 'AGPL']
# Round 3 canonical topic anchors (sourced from dictai-page-content.tsx)
topics = ['Comment fonctionne la transcription', 'formats audio', '1&nbsp;heure d',
'confidentielle', 'Teams Copilot', 'Otter.ai', 'Barreau du Qu',
'Clio Manage', 'connaissances techniques', 'open source']
for topic in topics:
assert topic in body, f"FAQ missing topic anchor: {topic}"
def test_faq_alpine_accordion_bindings():
"""FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion."""
"""FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion (10 items)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# 7 x-data="{ open: false }" instances
assert body.count('x-data="{ open: false }"') == 7, \
"FAQ must have 7 independent Alpine accordion items"
# 10 x-data="{ open: false }" instances (round 3 enrichment)
assert body.count('x-data="{ open: false }"') == 10, \
"FAQ must have 10 independent Alpine accordion items"
# Each toggle button has @click and :aria-expanded
assert body.count('@click="open = !open"') == 7
assert body.count(':aria-expanded="open.toString()"') == 7
assert body.count('@click="open = !open"') == 10
assert body.count(':aria-expanded="open.toString()"') == 10
# Use built-in x-transition (NOT x-collapse plugin which is not bundled)
assert 'x-collapse' not in body, "Must NOT use x-collapse plugin (not loaded — use x-transition)"
assert 'x-transition.opacity' in body, "FAQ panels must use built-in x-transition"
@@ -720,7 +732,8 @@ def test_faq_jsonld_schema_present():
assert parsed['@context'] == 'https://schema.org'
assert parsed['@type'] == 'FAQPage'
assert isinstance(parsed['mainEntity'], list)
assert len(parsed['mainEntity']) == 7, "FAQPage must contain exactly 7 questions"
# Round 3: enriched to 10 canonical questions from dictai-page-content.tsx
assert len(parsed['mainEntity']) == 10, "FAQPage must contain exactly 10 questions (round 3)"
for q in parsed['mainEntity']:
assert q['@type'] == 'Question'
assert q['acceptedAnswer']['@type'] == 'Answer'
@@ -729,18 +742,25 @@ def test_faq_jsonld_schema_present():
def test_cta_final_section():
"""CTA final section with mailto pré-inscription + ghost button to #tarifs."""
"""CTA final (round 3) — primary démo gratuite + mailto pré-inscription + ghost button to #tarifs."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'cta-title' in body
assert 'pré-inscription' in body or 'pr&eacute;-inscription' in body
# Round 3 wording reinforced: "Prêt à protéger vos données" + démo gratuite
assert 'prot' in body and 'donn' in body, "Missing 'protéger vos données' headline"
assert 'd&eacute;mo gratuite' in body or 'démo gratuite' in body, \
"Round 3 primary CTA must say 'démo gratuite'"
# Pré-inscription wording (any case) preserved as secondary path
assert 'pré-inscription' in body or 'pr&eacute;-inscription' in body \
or 'Pré-inscription' in body or 'Pr&eacute;-inscription' in body, \
"Pré-inscription wording must be preserved"
# mailto with subject
assert 'href="mailto:info@dictia.ca?subject=Pr%C3%A9-inscription%20DictIA"' in body or \
'href="mailto:info@dictia.ca?subject=Pré-inscription%20DictIA"' in body, \
"CTA must have mailto with subject prefilled"
# Anchor link to existing #tarifs section
assert 'href="#tarifs"' in body, "Secondary CTA must anchor to pricing"
# Ghost variant button
# Ghost variant button still in use (mailto + #tarifs)
assert 'border-white/[0.08]' in body # ghost button class
@@ -839,12 +859,16 @@ def test_round2_no_external_js_libs_added():
def test_round2_preserves_existing_sections():
"""Round 2 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité."""
"""Round 2 + 3 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité.
NOTE: round 3 replaced the hero copy ('sans risquer votre permis' → canonical wordmark
+ 'Transcription IA locale en 2 minutes'). The hero ID + pipeline are still required.
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Hero (round 0)
# Hero (round 3 canonical hero replaces round 0)
assert 'hero-title' in body
assert 'sans risquer votre permis' in body
assert 'Transcription IA locale en 2' in body, "Round 3 hero canonical phrase missing"
# Pipeline (round 1) — auto-advance + 4 nodes
assert 'pipeline-title' in body
assert 'Du fichier au résumé' in body
@@ -880,7 +904,8 @@ def test_routes_passes_testimonials_and_faq_to_template():
assert hasattr(routes, 'TESTIMONIALS'), "Module must define TESTIMONIALS list"
assert hasattr(routes, 'FAQ'), "Module must define FAQ list"
assert len(routes.TESTIMONIALS) == 3, "Must have 3 placeholder testimonials"
assert len(routes.FAQ) == 7, "Must have 7 FAQ entries"
# Round 3: enriched FAQ from 7 to 10 canonical questions (sourced from dictai-page-content.tsx)
assert len(routes.FAQ) == 10, "Must have 10 FAQ entries (round 3)"
# Each testimonial must NOT contain a 'quote' field (no fabricated quotes pre-launch)
for t in routes.TESTIMONIALS:
assert 'quote' not in t, "Pre-launch testimonials must not contain quote fields"