Vue d'ensemble
Certaines pages publiques (articles, fiches "pistes d'études") sont indexables par les moteurs de recherche via un rendu SSR maison : NestJS + Handlebars (hbs), sans rapport avec le mode SSR de Quasar/Vue.
Le SSR est "full back" : ces pages ne chargent aucun bundle JS Vue et ne sont jamais hydratées. C'est du HTML généré par Express/NestJS, servi tel quel. Le frontend n'intervient que pour rediriger vers ces URLs (voir plus bas) — il n'y a pas de logique de rendu partagée entre front et back.
Le bloc ssr: {...} dans front/quasar.config.js (mode SSR natif de Quasar) est du boilerplate mort : pas de src-ssr/, build en mode SPA (quasar build, sans -m ssr), dist/spa copié dans le Dockerfile. Ne pas le confondre avec le SSR décrit ici.
Stack et emplacement du code
| Quoi | Où |
|---|---|
| Controllers, services, middlewares | api/src/public-pages/ |
| Vues (Handlebars) | api/src/public-pages/views/ (layouts/public.hbs, partials/, articles/, pistes/) |
| Moteur de rendu | hbs configuré dans api/src/main.ts (app.setViewEngine('hbs'), layout par défaut layouts/public) |
| Routes SSR exposées | GET /public/articles..., GET /public/etudes..., GET /sitemap*.xml, GET /robots.txt |
| Routes SPA équivalentes (Vue Router) | front/src/router/routes.ts (/etudes/:id/slug/:slug, /articles/:id, etc.) — mêmes contenus, rendus côté client via API JSON, URLs différentes du SSR |
Créer une nouvelle page SSR
- Ajouter la route dans le controller concerné (
public-articles.controller.tsoupublic-pistes.controller.ts) avec@Render('dossier/vue'). - Écrire la vue
.hbsdansviews/articles/ouviews/pistes/, elle hérite automatiquement du layoutlayouts/public.hbs(header/footer/breadcrumb via partials). - Renseigner le SEO via l'objet
SeoMeta(titre, description, canonical, robots, OG/Twitter) — consommé par le partialpartials/seo-meta.hbs. - Si la page doit porter du JSON-LD (schema.org), utiliser
helpers/schema-builder.ts. - Le
PublicPageCacheInterceptor(Cache-Control: public, s-maxage=300) s'applique déjà à tout le controller — pas besoin de le regérer par route.
Pas de build/hydratation à prévoir : la page est servie dès que l'API tourne (yarn start:dev / yarn start:prod), les .hbs et assets sont copiés dans dist/ au build Nest (nest-cli.json → assets: ["public-pages/views/**/*"]).
Activer une page pour le trafic public
L'exposition des routes /public/... côté NestJS ne suffit pas à rendre une page publique : l'activation se fait au niveau nginx, piste par piste (rollout progressif), pas via un flag applicatif.
- Ajouter un
locationdansfront/nginx/nginx.conf.template(le seul fichier réellement utilisé, voir section suivante) :nginx location = /etudes/122/slug/but-information-communication { rewrite ^ /public/etudes/but-information-communication last; } - Recharger nginx (rebuild/redeploy du front).
- Répercuter l'id de la piste côté front, dans
front/src/router/index.ts:ts const SSR_ENABLED_PISTE_IDS = new Set<string>(['122']);Ce set force unwindow.location.href(rechargement complet, donc repassage par nginx/SSR) quand un lien interne de la SPA pointe vers une piste listée ici. Les deux doivent rester synchronisés à la main — rien ne le vérifie automatiquement.
Front vs back : qui fait quoi
- Clic sur un lien dans la SPA : Vue Router intercepte, navigation 100% client-side, aucune requête HTML — sauf si la piste ciblée est dans
SSR_ENABLED_PISTE_IDS(rechargement forcé, cf. ci-dessus). - Refresh / URL tapée directement : repasse par nginx à chaque fois.
- URL SPA classique sans activation SSR → nginx sert
index.html(fallback SPA), pas de HTML pré-rendu. - URL avec
locationSSR active, ou URL/public/etudes|articles/...directe → proxifiée vers NestJS, HTML Handlebars complet.
C'est le piège à retenir : accéder à une page via un lien interne et y accéder en tapant/rafraîchissant l'URL ne suivent pas le même chemin. Un comportement qui marche au clic peut différer au refresh (et inversement), selon l'état d'activation nginx.
Utilisateur déjà connecté (cookie de session présent) sur une page /public/etudes|articles/... → redirigé en 302 vers /app/... (SPA) par RedirectIfAuthenticatedMiddleware. Voir points d'attention ci-dessous sur ce mécanisme.
Environnements : local / staging / prod
| Local (dev) | Staging / Prod | |
|---|---|---|
| Serveur front | Quasar dev server (Vite), port 8080 | nginx (nginx:stable-alpine) servant dist/spa |
Routage /public/* → API |
Proxy Vite, devServer.proxy dans front/quasar.config.js — toujours actif, cible API_URL (défaut http://localhost:3236) |
location nginx dans nginx.conf.template |
| Activation par piste | Aucune notion équivalente — toutes les pistes sont proxifiées pareil | location dédiée par piste (rollout manuel, cf. ci-dessus) |
| Fichier nginx utilisé | — | front/nginx/nginx.conf.template, templaté via envsubst (${API_HOST}) au démarrage du conteneur (front/Dockerfile) |
front/nginx/nginx.conf (sans .template) n'est utilisé nulle part — ni Dockerfile, ni docker-compose, ni CI. C'est un fichier orphelin, qui a divergé de nginx.conf.template : il montre un état où le SSR est actif, alors que le fichier réellement déployé le désactive (voir section suivante). Pour changer le comportement nginx en staging/prod, éditer nginx.conf.template, jamais nginx.conf.
API_HOST (défaut back.inspire-orientation.org) n'est pas surchargé par la CI (.gitlab-ci.yml) : staging et prod utilisent la même valeur par défaut, pas de différenciation visible à ce niveau dans ce repo.
État actuel & points d'attention (2026-08)
- Le SSR public est désactivé en prod aujourd'hui :
nginx.conf.templatea unlocation ~ ^/public/(etudes|articles)(/|$) { return 404; }et aucunelocationde rollout piste active. Le code backend/Handlebars est fonctionnel et testé (api/test/public-pages.e2e-spec.ts), mais aucune page SSR n'est accessible depuis le domaine public tant que ce fichier n'est pas modifié. - Mismatch probable de nom de cookie de session :
RedirectIfAuthenticatedMiddlewarelitprocess.env.SESSION_COOKIE_NAME ?? 'inspire_token', mais le front stocke le JWT sous le cookie`${process.env.APP_NAME}`(=inspire-v2-api).SESSION_COOKIE_NAMEn'est défini nulle part dans ce repo. Tant que ce n'est pas aligné, la redirection des utilisateurs connectés vers la SPA ne se déclenche probablement jamais. - Conflit de route
/sitemap.xml: deux controllers différents (general/sitemap/etpublic-pages/) déclarent tous les deuxGET sitemap.xml. L'ordre d'import dansapp.module.ts(GeneralModuleavantPublicPagesModule) fait que celui depublic-pagesest probablement inatteignable sur ce chemin précis. robots.txtà 3 couches : servi par nginx (map $host $robots_content, prioritaire en prod), par le controllerpublic-pages, et par un fichier statique (api/src/public/robots.txt). Seule la couche nginx est visible depuis le domaine public.legacy-redirect.middleware.tsn'est jamais appliqué (code écrit, jamais enregistré dans un module) — les anciennes URLs qu'il est censé rediriger en 301 ne le sont pas.- Maillage interne cassé : les liens "pistes similaires" dans
pistes/detail.hbspointent vers le format SPA (/etudes/:id/slug/:slug), pas vers l'URL SSR canonique (/public/etudes/:slug). Si la piste liée n'a pas delocationnginx dédiée, le lien retombe sur la SPA — casse la continuité SSR pour le crawl SEO.
Articulation avec la SPA Vue
Fait :
- Routes miroir en Vue Router pour les mêmes contenus (rendu client via API JSON), coexistant avec les routes SSR.
- Redirection des utilisateurs connectés depuis les pages SSR vers la SPA (RedirectIfAuthenticatedMiddleware), sous réserve du mismatch cookie ci-dessus.
- Bascule forcée en rechargement complet pour les pistes listées dans SSR_ENABLED_PISTE_IDS, pour que la navigation interne repasse bien par le SSR quand il est actif.
À faire / flou :
- Synchronisation nginx.conf.template ↔ SSR_ENABLED_PISTE_IDS entièrement manuelle, rien ne la vérifie ni ne l'automatise.
- Le lien "Connexion | Inscription" du header SSR (partials/header.hbs) pointe vers /, pas vers un vrai flux SSO — probablement un placeholder non finalisé.
Bloquant avant réactivation du SSR en prod :
- Vérifier/aligner SESSION_COOKIE_NAME avec le cookie réellement posé par le front.
- Trancher le conflit /sitemap.xml entre les deux modules.
- Décider si nginx.conf (fichier orphelin) doit être supprimé ou resynchronisé avec nginx.conf.template, pour éviter que quelqu'un l'édite par erreur en pensant qu'il est utilisé.