Trois surfaces partagent désormais le même design system, Tailwind v4
et React 19 — au lieu d'avoir landing en HTML vanilla, app en React, et
blog en Adonis SSR :
* packages/ui — design system partagé (tokens Tailwind v4 + composants
TSX) extrait depuis apps/web : Brand, Gem, Button, Card, Chip, Eyebrow,
EmptyState. apps/web migre 41 imports vers @rubis/ui.
* apps/landing — nouvelle app Astro 6 SSR (rubis.pro), remplace l'ancienne
landing nginx vanilla. Embarque :
- Landing complète portée en sections React (Hero, Stats, Promise,
HowItWorks, Gamification, Legal, Pricing, FAQ, FinalCTA, Footnotes)
- Pages légales (mentions, confidentialité, CGV) via LegalLayout.astro
- Blog SSR (/blog, /blog/:slug) qui consomme /api/v1/posts
- sitemap.xml, blog/rss.xml, robots.txt en endpoints Astro
- SEO complet (canonical, hreflang, OG, Twitter Card, JSON-LD
Article/BreadcrumbList/Blog/SoftwareApplication)
* apps/api — BlogController réduit à 2 endpoints JSON (GET /api/v1/posts
+ GET /api/v1/posts/:slug). Suppression des templates SSR Adonis
(apps/api/app/blog/), de l'alias #blog/*, des deps react-dom et
@types/react-dom. PostTransformer + PostSummaryTransformer ajoutés.
Le service blog_renderer + le seeder + les 3 articles fondateurs
restent intacts (réutilisés par futurs admin + cron IA).
* Infra :
- Dockerfile.landing (multi-stage Node 22 + tini, Astro standalone)
- k3s/app/landing.yml (Deployment + Service rubis-landing:4321 +
ConfigMap avec API_URL=http://rubis-api.rubis.svc.cluster.local:3333)
- .gitea/workflows/deploy.yml mis à jour pour build rubis-landing
- .gitea/workflows/deploy-web.yml + Dockerfile.web : prennent en
compte packages/ui/ comme dépendance
- Suppression du Dockerfile nginx legacy + k3s/{deployment,service}.yml
- Suppression de landing/ (assets favicons migrés vers
apps/landing/public/)
* Docs : architecture.md (vue d'ensemble + §4bis apps/landing complet,
§3 endpoints JSON blog, layout monorepo), CLAUDE.md (stack technique,
documents associés, déploiement).
Note infra : l'ancien Deployment "rubis" (nginx) et son Service ne sont
PAS supprimés par la CI — à nettoyer manuellement après validation que
Traefik a été repointé sur rubis-landing:4321 dans le repo proxmox.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
import { BaseCommand, flags } from '@adonisjs/core/ace'
|
|
import type { CommandOptions } from '@adonisjs/core/types/ace'
|
|
import { DateTime } from 'luxon'
|
|
|
|
import Post from '#models/post'
|
|
import { renderPost } from '#services/blog_renderer'
|
|
import { seedArticles } from '#database/seeders/blog_seed/index'
|
|
|
|
/**
|
|
* Insère / met à jour les articles fondateurs du blog en DB.
|
|
*
|
|
* node ace seed:blog # idempotent, upsert par slug
|
|
* node ace seed:blog --reset # supprime tous les posts avant
|
|
*
|
|
* À lancer une fois en local et en prod après le déploiement de PR1
|
|
* (avant la mise en route du routing Traefik en PR2). Les ré-exécutions
|
|
* sont sans effet de bord — utile si on retouche les MD source.
|
|
*/
|
|
export default class SeedBlog extends BaseCommand {
|
|
static commandName = 'seed:blog'
|
|
static description = 'Seed des 3 articles fondateurs du blog (idempotent par slug)'
|
|
|
|
static options: CommandOptions = {
|
|
startApp: true,
|
|
}
|
|
|
|
@flags.boolean({
|
|
description: 'Supprime tous les posts existants avant le seed',
|
|
default: false,
|
|
})
|
|
declare reset: boolean
|
|
|
|
async run() {
|
|
if (this.reset) {
|
|
const deleted = await Post.query().delete()
|
|
this.logger.warning(`${deleted} posts supprimés (--reset).`)
|
|
}
|
|
|
|
let created = 0
|
|
let updated = 0
|
|
|
|
for (const draft of seedArticles) {
|
|
const { contentHtml, wordCount, readingTimeMinutes } = renderPost(draft.contentMd)
|
|
const publishedAt = DateTime.now().minus({ days: draft.publishedDaysAgo }).toUTC().startOf('minute')
|
|
|
|
const existing = await Post.findBy('slug', draft.slug)
|
|
const payload = {
|
|
slug: draft.slug,
|
|
title: draft.title,
|
|
excerpt: draft.excerpt,
|
|
contentMd: draft.contentMd,
|
|
contentHtml,
|
|
authorName: draft.authorName,
|
|
tags: draft.tags,
|
|
status: 'published' as const,
|
|
publishedAt,
|
|
wordCount,
|
|
readingTimeMinutes,
|
|
aiGenerated: false,
|
|
noindex: false,
|
|
}
|
|
|
|
if (existing) {
|
|
existing.merge(payload)
|
|
await existing.save()
|
|
updated += 1
|
|
} else {
|
|
await Post.create(payload)
|
|
created += 1
|
|
}
|
|
|
|
this.logger.success(`✓ ${draft.slug} (${wordCount} mots, ${readingTimeMinutes} min)`)
|
|
}
|
|
|
|
this.logger.info(`\nFait : ${created} créé(s), ${updated} mis à jour, ${seedArticles.length} total.`)
|
|
}
|
|
}
|