feat(marketing): base.html layout + glassmorphism header + button macro
- templates/macros/button.html: 3 variants (primary gradient/glow, secondary, ghost) x 3 sizes for reuse across marketing/billing/legal/auth templates - templates/marketing/base.html: Tailwind v4-scoped layout with FlexiHub glassmorphism header (62px, navy/.97, backdrop-blur-xl, .045 border), sticky positioning, OG/Twitter meta, Inter font preload, marketing.css link, Alpine.js defer, 5-item main nav + Connexion/Demarrer CTAs - templates/marketing/_footer.html: minimal Phase 2 placeholder with legal links + Inverness QC address + info@dictia.ca (full footer in A-2.7) - templates/marketing/landing.html: minimal hero placeholder (replaced in A-2.2 with full hero + cosmic orbs) - src/marketing/routes.py: landing() now render_template instead of inline HTML - 7 tests verify template structure, FlexiHub markers, nav, CTAs, legal links, no login redirect for anonymous users - Tailwind CSS rebuilt with new template content scope (cssnano-minified)
This commit is contained in:
@@ -1,20 +1,19 @@
|
|||||||
"""Marketing routes - minimal Phase 1 placeholder.
|
"""Marketing routes — Phase 2 templated landing.
|
||||||
|
|
||||||
Real templates and content arrive in Tasks A-2.1 through A-2.8.
|
Phase 2 (A-2.1+): renders templates/marketing/landing.html.
|
||||||
|
Tasks A-2.2 through A-2.7 will progressively enrich the landing template.
|
||||||
|
Tasks A-2.8 will add /tarifs, /fonctionnalites, /conformite, /contact routes.
|
||||||
"""
|
"""
|
||||||
from flask import Response
|
from flask import render_template
|
||||||
|
|
||||||
from . import marketing_bp
|
from . import marketing_bp
|
||||||
|
|
||||||
|
|
||||||
@marketing_bp.route('/')
|
@marketing_bp.route('/')
|
||||||
def landing():
|
def landing():
|
||||||
"""Placeholder root route.
|
"""Marketing landing page — public, indexable, French-Canadian.
|
||||||
|
|
||||||
Phase 1: returns a minimal HTML response so the route exists for tests.
|
Called directly (not via redirect) from src/api/recordings.py:index
|
||||||
Phase 2 (A-2.1): replaced with proper template render.
|
when the visitor is anonymous. See B-1.3 fix commit af29539 for context.
|
||||||
"""
|
"""
|
||||||
return Response(
|
return render_template('marketing/landing.html')
|
||||||
'<!DOCTYPE html><html><body><p>DictIA marketing - Phase 1 bootstrap</p></body></html>',
|
|
||||||
mimetype='text/html'
|
|
||||||
)
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
20
templates/macros/button.html
Normal file
20
templates/macros/button.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{# Reusable button macro — primary (gradient + glow), secondary (white outline), ghost (transparent on dark) #}
|
||||||
|
{%- macro button(text, href='#', variant='primary', size='md', icon=None, target=None, rel=None) -%}
|
||||||
|
{%- set classes = {
|
||||||
|
'primary': 'grad-bg shadow-cta hover:shadow-cta-hover hover:-translate-y-px',
|
||||||
|
'secondary': 'bg-white text-brand-navy border border-brand-border hover:bg-brand-bg',
|
||||||
|
'ghost': 'text-white border border-white/[0.08] hover:bg-white/[0.05]'
|
||||||
|
}[variant] -%}
|
||||||
|
{%- set sizing = {
|
||||||
|
'sm': 'px-3 py-1.5 text-sm',
|
||||||
|
'md': 'px-5 py-2.5 text-[15px]',
|
||||||
|
'lg': 'px-6 py-3 text-base'
|
||||||
|
}[size] -%}
|
||||||
|
<a href="{{ href }}"
|
||||||
|
class="inline-flex items-center justify-center gap-2 rounded-[0.75rem] font-semibold transition-all duration-200 {{ classes }} {{ sizing }}"
|
||||||
|
{% if target %}target="{{ target }}"{% endif %}
|
||||||
|
{% if rel %}rel="{{ rel }}"{% endif %}>
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
{%- if icon -%}<span class="ml-0.5" aria-hidden="true">{{ icon | safe }}</span>{%- endif -%}
|
||||||
|
</a>
|
||||||
|
{%- endmacro -%}
|
||||||
13
templates/marketing/_footer.html
Normal file
13
templates/marketing/_footer.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<footer class="bg-brand-navy2 text-white py-12 mt-20">
|
||||||
|
<div class="max-w-[1200px] mx-auto px-6 text-center">
|
||||||
|
<p class="text-sm text-white/60">
|
||||||
|
© 2026 DictIA Inc. · 77 ch. de la Seigneurie, Inverness QC G0S 1K0 ·
|
||||||
|
<a href="mailto:info@dictia.ca" class="hover:text-white">info@dictia.ca</a>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-white/40 mt-2">
|
||||||
|
<a href="/legal/conditions" class="hover:text-white">Conditions</a> ·
|
||||||
|
<a href="/legal/confidentialite" class="hover:text-white">Confidentialité (Loi 25)</a> ·
|
||||||
|
<a href="/legal/cookies" class="hover:text-white">Cookies</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
71
templates/marketing/base.html
Normal file
71
templates/marketing/base.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr-CA">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#060d1a">
|
||||||
|
|
||||||
|
<title>{% block title %}DictIA — Transcription IA conforme Loi 25 | Avocats, CPA, secteur public{% endblock %}</title>
|
||||||
|
<meta name="description" content="{% block description %}Transcription IA 100% locale, conforme Loi 25. Pour avocats, CPA, ChAD et 6 autres ordres professionnels. Hébergé au Québec, zéro Cloud Act.{% endblock %}">
|
||||||
|
<link rel="canonical" href="{% block canonical %}https://dictia.pages.dev{{ request.path }}{% endblock %}">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="{{ self.title() }}">
|
||||||
|
<meta property="og:description" content="{{ self.description() }}">
|
||||||
|
<meta property="og:image" content="{% block og_image %}https://dictia.pages.dev/static/images/og/og-default.png{% endblock %}">
|
||||||
|
<meta property="og:url" content="https://dictia.pages.dev{{ request.path }}">
|
||||||
|
<meta property="og:locale" content="fr_CA">
|
||||||
|
<meta property="og:site_name" content="DictIA">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="{{ self.title() }}">
|
||||||
|
<meta name="twitter:description" content="{{ self.description() }}">
|
||||||
|
<meta name="twitter:image" content="{{ self.og_image() }}">
|
||||||
|
|
||||||
|
<!-- Preload critical fonts -->
|
||||||
|
<link rel="preload" href="/static/fonts/Inter-Variable.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
|
|
||||||
|
<!-- Marketing CSS (Tailwind v4 buildé) -->
|
||||||
|
<link rel="stylesheet" href="/static/css/marketing.css">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg">
|
||||||
|
|
||||||
|
{% block schema %}{% endblock %}
|
||||||
|
{% block head_extra %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body class="bg-white">
|
||||||
|
<!-- Glassmorphism header (FlexiHub style: 62px, navy/.97 + backdrop-blur-xl + 0.045 border) -->
|
||||||
|
<header class="fixed top-0 inset-x-0 z-50 h-[62px] bg-brand-navy/[0.97] backdrop-blur-xl border-b border-white/[0.045]">
|
||||||
|
<div class="max-w-[1200px] mx-auto h-full px-6 flex items-center justify-between">
|
||||||
|
<a href="/" class="font-black text-xl tracking-tight grad-text" aria-label="DictIA — accueil">DictIA</a>
|
||||||
|
|
||||||
|
<nav class="hidden md:flex gap-8 text-sm font-medium text-white/80" aria-label="Navigation principale">
|
||||||
|
<a href="/fonctionnalites" class="hover:text-white transition">Fonctionnalités</a>
|
||||||
|
<a href="/conformite" class="hover:text-white transition">Conformité</a>
|
||||||
|
<a href="/tarifs" class="hover:text-white transition">Tarifs</a>
|
||||||
|
<a href="/blog" class="hover:text-white transition">Blog</a>
|
||||||
|
<a href="/contact" class="hover:text-white transition">Contact</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="/login" class="hidden sm:inline-block text-sm font-medium text-white/80 hover:text-white">Connexion</a>
|
||||||
|
{% from 'macros/button.html' import button %}
|
||||||
|
{{ button('Démarrer →', href='/signup', variant='primary', size='sm') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="pt-[62px]">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% include 'marketing/_footer.html' %}
|
||||||
|
|
||||||
|
<!-- Alpine.js for interactivity (FAQ accordion, ROI calculator, mobile menu) -->
|
||||||
|
<script src="/static/js/alpine.min.js" defer></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
templates/marketing/landing.html
Normal file
20
templates/marketing/landing.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'marketing/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="relative overflow-hidden bg-brand-navy text-white py-24 md:py-32">
|
||||||
|
<div class="max-w-[1200px] mx-auto px-6 text-center">
|
||||||
|
<p class="eyebrow grad-text mb-6">DICTIA · TRANSCRIPTION IA · CONFORME LOI 25</p>
|
||||||
|
<h1 class="text-[clamp(2.5rem,4vw,4rem)] font-black leading-[1.05] mb-6">
|
||||||
|
Refonte en cours — <span class="grad-text">printemps 2026</span>.
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-white/70 max-w-2xl mx-auto mb-10">
|
||||||
|
La page marketing complète est en construction. Toutes les fonctionnalités de DictIA restent disponibles via votre compte.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
{% from 'macros/button.html' import button %}
|
||||||
|
{{ button('Réserver une démo', href='/contact', variant='primary', size='lg') }}
|
||||||
|
{{ button('Connexion', href='/login', variant='ghost', size='lg') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
82
tests/test_marketing_landing_template.py
Normal file
82
tests/test_marketing_landing_template.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Verify the marketing landing template renders correctly."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
|
||||||
|
os.environ.setdefault('SECRET_KEY', 'test-secret-key')
|
||||||
|
|
||||||
|
from src.app import app # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_renders_template_not_inline_html():
|
||||||
|
"""GET / renders templates/marketing/landing.html (not inline HTML from Phase 1)."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/', follow_redirects=False)
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
# Phase 2 template hallmarks
|
||||||
|
assert '<!DOCTYPE html>' in body, "Missing DOCTYPE — base.html not rendering"
|
||||||
|
assert 'lang="fr-CA"' in body, "Missing lang=fr-CA"
|
||||||
|
assert '/static/css/marketing.css' in body, "Missing marketing.css link"
|
||||||
|
assert '/static/fonts/Inter-Variable.woff2' in body, "Missing Inter font preload"
|
||||||
|
assert '/static/js/alpine.min.js' in body, "Missing Alpine.js script"
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_has_canonical_url():
|
||||||
|
"""OG + canonical metadata present."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/')
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
assert 'rel="canonical"' in body
|
||||||
|
assert 'og:type' in body
|
||||||
|
assert 'og:locale' in body and 'fr_CA' in body
|
||||||
|
assert 'twitter:card' in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_has_glassmorphism_header():
|
||||||
|
"""FlexiHub-style header present (navy + backdrop-blur)."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/')
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
assert 'bg-brand-navy/[0.97]' in body or 'bg-brand-navy' in body
|
||||||
|
assert 'backdrop-blur-xl' in body
|
||||||
|
assert 'border-white/[0.045]' in body, "Missing FlexiHub-style 0.045 border opacity"
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_has_main_nav():
|
||||||
|
"""Main nav has 5 links: Fonctionnalités, Conformité, Tarifs, Blog, Contact."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/')
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
for link in ['/fonctionnalites', '/conformite', '/tarifs', '/blog', '/contact']:
|
||||||
|
assert f'href="{link}"' in body, f"Missing nav link: {link}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_has_login_and_signup_ctas():
|
||||||
|
"""Login + Signup CTAs present in header."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/')
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
assert 'href="/login"' in body
|
||||||
|
assert 'href="/signup"' in body
|
||||||
|
assert 'Démarrer' in body or 'Démarrer' in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_footer_has_legal_links():
|
||||||
|
"""Footer placeholder includes legal links (full footer in A-2.7)."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/')
|
||||||
|
body = response.data.decode('utf-8')
|
||||||
|
assert '/legal/conditions' in body
|
||||||
|
assert '/legal/confidentialite' in body
|
||||||
|
assert 'info@dictia.ca' in body, "Missing canonical email info@dictia.ca"
|
||||||
|
assert 'Inverness' in body, "Missing Inverness QC address"
|
||||||
|
|
||||||
|
|
||||||
|
def test_landing_no_login_redirect_for_anonymous():
|
||||||
|
"""Anonymous user GET / must see template (regression check from B-1.3)."""
|
||||||
|
client = app.test_client()
|
||||||
|
response = client.get('/', follow_redirects=False)
|
||||||
|
assert response.status_code == 200, \
|
||||||
|
f"Expected 200, got {response.status_code} — possibly login_required regression"
|
||||||
Reference in New Issue
Block a user