From 824ea638de70a64dbb98465b57fab205603d06ec Mon Sep 17 00:00:00 2001 From: Allison Date: Mon, 27 Apr 2026 19:52:36 -0400 Subject: [PATCH] =?UTF-8?q?feat(marketing):=20A-2.7b=20t=C3=A9moignages=20?= =?UTF-8?q?placeholder=20+=20FAQ=20accordion=20+=20CTA=20+=20JSON-LD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pre-launch testimonials section: 3 placeholder cards (avocat, CPA, municipal) with persona icons + 'Témoignage à venir' label — NO fabricated quotes (LPC art. 219). Expected publication mai-juin 2026 from T-4.1 interviews. - FAQ accordion: 7 verifiable Q&A using Alpine.js core (x-data + x-show + built-in x-transition; NO x-collapse plugin). Each item has @click toggle, :aria-expanded, aria-controls, role="region" panel, focusable button. - Schema.org FAQPage JSON-LD inline at end of FAQ section — striptags + replace(' ', ' ') to normalize entities for Google FAQ rich result. - CTA final: 'Réservez votre pré-inscription' (mailto + #tarifs anchor), cosmic orbs to mirror Hero (page closure), ghost variant secondary button. - Inline TESTIMONIALS and FAQ Python lists in src/marketing/routes.py (no PyYAML dep — YAGNI; T-4.1 can introduce it when real data warrants). - 8 new tests covering testimonials placeholders, forbidden fake names, 7 FAQ panels, Alpine bindings, JSON-LD schema, CTA wording, route data. --- src/marketing/routes.py | 62 ++++++++++- static/css/marketing.css | 21 ++++ templates/marketing/landing.html | 125 +++++++++++++++++++++++ tests/test_marketing_landing_template.py | 117 +++++++++++++++++++++ 4 files changed, 324 insertions(+), 1 deletion(-) diff --git a/src/marketing/routes.py b/src/marketing/routes.py index 16e062c..0e687cb 100644 --- a/src/marketing/routes.py +++ b/src/marketing/routes.py @@ -9,6 +9,62 @@ from flask import render_template from . import marketing_bp +# Pre-launch placeholder testimonials — T-4.1 will replace these with real +# pilot-client interviews (avocat + CPA + municipalité) in mai-juin 2026. +# Until then, render placeholder cards (LPC art. 219: no fabricated quotes). +TESTIMONIALS = [ + { + 'persona': 'avocat', + 'placeholder_label': 'Cabinet juridique pilote', + 'expected': 'Mai 2026', + }, + { + 'persona': 'cpa', + 'placeholder_label': 'Cabinet CPA pilote', + 'expected': 'Mai 2026', + }, + { + 'persona': 'municipal', + 'placeholder_label': 'Municipalité pilote', + 'expected': 'Juin 2026', + }, +] + + +# FAQ — 7 verifiable Q&A. Each question/answer must remain factually defensible +# (LPC art. 219). Answers reference public sources where applicable. +FAQ = [ + { + 'q': 'Comment DictIA est-il conforme à la Loi 25 (RPRP)?', + 'a': 'DictIA héberge les données chez OVHcloud Beauharnois (Québec), produit un audit trail intégré conforme à l\'art. 3.5 LPRPSP, fournit un modèle d\'évaluation des facteurs relatifs à la vie privée (EFVP) art. 3.3, et trace les consentements art. 14. Code source AGPL v3 entièrement vérifiable.', + }, + { + 'q': 'Quelle est la différence entre DictIA Cloud et DictIA on-premise?', + 'a': 'DictIA Cloud (à partir de 369 $/mois) est hébergé chez OVH Beauharnois — aucun matériel à gérer, opérationnel sous 48 h. DictIA on-premise (DictIA 8 et DictIA 16, à partir de 3 450 $ + 173 $/mois) tourne sur GPU dans vos murs — vos données ne sortent jamais de votre réseau.', + }, + { + 'q': 'Quelle précision puis-je attendre pour le français québécois?', + 'a': 'DictIA utilise WhisperX Large-v3 fine-tuné sur audio professionnel québécois. La précision typique observée sur nos jeux de tests internes dépasse 95 %. La méthodologie complète (corpus, métriques WER, conditions) est disponible sur demande : info@dictia.ca.', + }, + { + 'q': 'Pouvez-vous transcrire des audiences ou des interrogatoires confidentiels?', + 'a': 'Oui, à condition que vous respectiez les obligations de votre ordre (Barreau, Chambre des notaires, etc.) en matière de consentement et de confidentialité. DictIA on-premise est recommandé pour ce type d\'usage : les données ne quittent jamais votre infrastructure. Consultez votre ordre avant tout déploiement.', + }, + { + 'q': 'Quels formats d\'export sont supportés?', + 'a': 'DOCX, PDF, SRT, VTT, TXT, JSON et MD. Modèles spécifiques disponibles pour avocats (interrogatoire numéroté), notaires (procès-verbal d\'assemblée) et CPA (transcription d\'entrevue). Intégrations natives : Word, Outlook, Teams, Notion, Obsidian, Zapier, Make, n8n.', + }, + { + 'q': 'Que se passe-t-il si je résilie mon abonnement?', + 'a': 'Vos données restent exportables pendant 90 jours après résiliation (DOCX/PDF/JSON). Passé ce délai, suppression définitive avec confirmation écrite — politique conforme à l\'art. 23 LPRPSP. Aucun frais de résiliation. Détails dans nos conditions d\'utilisation.', + }, + { + 'q': 'Le code source est-il vraiment open source AGPL v3?', + 'a': 'Oui. Code source complet sur Gitea public. Conséquence pratique de l\'AGPL : tout fork hébergé doit publier ses modifications. Transparence vérifiable par vos auditeurs internes ou un tiers de confiance.', + }, +] + + @marketing_bp.route('/') def landing(): """Marketing landing page — public, indexable, French-Canadian. @@ -16,4 +72,8 @@ def landing(): Called directly (not via redirect) from src/api/recordings.py:index when the visitor is anonymous. See B-1.3 fix commit af29539 for context. """ - return render_template('marketing/landing.html') + return render_template( + 'marketing/landing.html', + testimonials=TESTIMONIALS, + faq=FAQ, + ) diff --git a/static/css/marketing.css b/static/css/marketing.css index d4515bb..f9d6bee 100644 --- a/static/css/marketing.css +++ b/static/css/marketing.css @@ -414,6 +414,9 @@ .right-1\.5 { right: calc(var(--spacing) * 1.5); } + .right-1\/3 { + right: calc(1 / 3 * 100%); + } .right-1\/4 { right: calc(1 / 4 * 100%); } @@ -741,6 +744,9 @@ .h-14 { height: calc(var(--spacing) * 14); } + .h-16 { + height: calc(var(--spacing) * 16); + } .h-20 { height: calc(var(--spacing) * 20); } @@ -762,6 +768,9 @@ .h-\[400px\] { height: 400px; } + .h-\[450px\] { + height: 450px; + } .h-\[500px\] { height: 500px; } @@ -897,6 +906,9 @@ .w-\[400px\] { width: 400px; } + .w-\[450px\] { + width: 450px; + } .w-\[500px\] { width: 500px; } @@ -933,6 +945,9 @@ .max-w-\[300px\] { max-width: 300px; } + .max-w-\[820px\] { + max-width: 820px; + } .max-w-\[1060px\] { max-width: 1060px; } @@ -1029,6 +1044,9 @@ --tw-scale-z: 100%; scale: var(--tw-scale-x) var(--tw-scale-y); } + .rotate-45 { + rotate: 45deg; + } .rotate-180 { rotate: 180deg; } @@ -2276,6 +2294,9 @@ .text-\[clamp\(2\.5rem\,4vw\,4rem\)\] { font-size: clamp(2.5rem, 4vw, 4rem); } + .text-\[clamp\(2\.25rem\,4vw\,3\.5rem\)\] { + font-size: clamp(2.25rem, 4vw, 3.5rem); + } .text-\[clamp\(2rem\,3vw\,2\.75rem\)\] { font-size: clamp(2rem, 3vw, 2.75rem); } diff --git a/templates/marketing/landing.html b/templates/marketing/landing.html index a24a240..0891706 100644 --- a/templates/marketing/landing.html +++ b/templates/marketing/landing.html @@ -430,6 +430,131 @@ + +{# ===== TÉMOIGNAGES (placeholder pré-lancement) ===== #} +
+
+
+

TÉMOIGNAGES

+

+ Premiers cabinets pilotes interviewés au printemps 2026. +

+

+ Trois pilotes confidentiels en cours : un cabinet juridique, un cabinet CPA, une municipalité. Témoignages publiés ici dès l'aboutissement des entrevues, accompagnés d'une métrique vérifiable. +

+
+ +
+ {% for t in testimonials %} +
+ +

{{ t.placeholder_label | safe }}

+

Témoignage à venir · {{ t.expected | safe }}

+

+ « Citation et métrique publiées après validation par le pilote. » +

+
+ {% endfor %} +
+
+
+ +{# ===== FAQ ===== #} +
+
+
+

FAQ

+

+ Vos questions les plus fréquentes. +

+

+ Une question qui n'est pas couverte ici ? Écrivez-nous : + info@dictia.ca. +

+
+ +
+ {% for item in faq %} +
+

+ +

+
+

{{ item.a | safe }}

+
+
+ {% endfor %} +
+
+ + {# Schema.org FAQPage JSON-LD for SEO/GEO — inline so it travels with this page only #} + +
+ +{# ===== CTA FINAL ===== #} +
+ {# Two warm cosmic orbs to mirror the Hero — visual closure of the page #} + + +
+

PRÊT ?

+

+ Réservez votre pré-inscription. +

+

+ Lancement printemps 2026. Les premiers utilisateurs bénéficient d'une remise de bienvenue et d'un accompagnement direct par notre équipe technique. Aucun engagement. +

+ +
+ {% from 'macros/button.html' import button %} + {{ button('Pré-inscription par courriel', href='mailto:info@dictia.ca?subject=Pré-inscription%20DictIA', variant='primary', size='lg', icon='✉️') }} + {{ button('Voir les forfaits', href='#tarifs', variant='ghost', size='lg') }} +
+ +

+ Pré-inscription par courriel jusqu'à l'ouverture de la plateforme. Inscription en ligne disponible dès le lancement. +

+
+
{% endblock %} {% block scripts %} diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py index 497e9b5..3738cd4 100644 --- a/tests/test_marketing_landing_template.py +++ b/tests/test_marketing_landing_template.py @@ -621,3 +621,120 @@ def test_comparatif_check_marks_consistently_mean_good(): assert 'Exposé au Cloud Act' not in body, "Row 2 must be reworded to positive convention" # Specifically check Teams gets ✗ for the territoriality criterion (was ⚠ before) assert '✗ Soumis Cloud Act' in body, "Teams must show ✗ for non-Loi-25-compliant transfer" + + +def test_testimonials_section_present_with_placeholder_cards(): + """Témoignages section present with 3 placeholder cards (no fabricated quotes).""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + assert 'testimonials-title' in body + assert 'Premiers cabinets pilotes' in body + # 3 personas + assert 'Cabinet juridique pilote' in body + assert 'Cabinet CPA pilote' in body + assert 'Municipalit' in body and 'pilote' in body + # No fabricated quotes — must say "Témoignage à venir" + assert 'Témoignage à venir' in body or 'Témoignage à venir' in body, \ + "Pre-launch testimonials must show 'Témoignage à venir' (LPC art. 219)" + # Expected publication months + assert 'Mai 2026' in body + assert 'Juin 2026' in body + + +def test_testimonials_use_personas_not_fake_names(): + """Testimonials must NOT contain fabricated names (Me Tremblay, Mme Bouchard, etc.).""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + forbidden_names = ['Me Tremblay', 'Mme Bouchard', 'M. Lefebvre', + 'Cabinet Pilote A', 'Cabinet Pilote B', 'Municipalité Pilote C'] + for name in forbidden_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.""" + 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): + 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'] + 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.""" + 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" + # Each toggle button has @click and :aria-expanded + assert body.count('@click="open = !open"') == 7 + assert body.count(':aria-expanded="open.toString()"') == 7 + # 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" + # aria-controls links button to panel + assert 'aria-controls="faq-panel-1"' in body + + +def test_faq_jsonld_schema_present(): + """FAQ section embeds Schema.org FAQPage JSON-LD for SEO/GEO.""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + # Inline JSON-LD script + assert '', body.find('"FAQPage"'))], \ + "JSON-LD must not contain raw ' ' entities — strip them server-side" + + +def test_cta_final_section(): + """CTA final section with 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é-inscription' in body + # 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 + assert 'border-white/[0.08]' in body # ghost button class + + +def test_cta_final_uses_safe_pre_launch_wording(): + """CTA must use 'Réservez votre pré-inscription' not 'Achetez maintenant' (pre-launch).""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + forbidden = ['Achetez maintenant', 'Acheter maintenant', 'Acheter immédiatement', + 'Acheter dès aujourd', 'Disponible immédiatement'] + for phrase in forbidden: + assert phrase not in body, f"Forbidden pre-launch CTA: {phrase}" + + +def test_routes_passes_testimonials_and_faq_to_template(): + """marketing.routes.landing() must pass testimonials and faq to render_template.""" + # Import the module to verify the data lists exist + from src.marketing import routes + 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" + # 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" + assert 'persona' in t and 'placeholder_label' in t and 'expected' in t + # Each FAQ entry must have 'q' and 'a' + for item in routes.FAQ: + assert 'q' in item and 'a' in item