Skip to content

Commit c68c893

Browse files
Crash--claude
andcommitted
Initial commit: bilingual FR/EN engineering blog
Astro 5 + Tailwind 4 + MDX static blog with: - Per-language Markdown articles (<slug>.fr.md / <slug>.en.md) - Conditional hreflang and language switcher - Pagefind client-side search, Shiki syntax highlighting - Auto-generated SVG covers, dark mode, RSS per language - Tags and authors collections - Dockerfile (multi-stage → nginx) + GitHub Actions for build & GHCR push Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 parents  commit c68c893

46 files changed

Lines changed: 11212 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
dist
3+
.astro
4+
.git
5+
.github
6+
.vscode
7+
.DS_Store
8+
README.md
9+
*.log

.github/workflows/deploy.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Build & Push Image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
packages: write
11+
12+
env:
13+
REGISTRY: ghcr.io
14+
IMAGE_NAME: ${{ github.repository }}
15+
16+
jobs:
17+
build-and-push:
18+
name: Build and publish container image
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Docker Buildx
24+
uses: docker/setup-buildx-action@v3
25+
26+
- name: Log in to GHCR
27+
uses: docker/login-action@v3
28+
with:
29+
registry: ${{ env.REGISTRY }}
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Extract image metadata
34+
id: meta
35+
uses: docker/metadata-action@v5
36+
with:
37+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
38+
tags: |
39+
type=sha,prefix=sha-,format=short
40+
type=raw,value=latest,enable={{is_default_branch}}
41+
42+
- name: Build and push
43+
uses: docker/build-push-action@v6
44+
with:
45+
context: .
46+
push: true
47+
tags: ${{ steps.meta.outputs.tags }}
48+
labels: ${{ steps.meta.outputs.labels }}
49+
cache-from: type=gha
50+
cache-to: type=gha,mode=max
51+
52+
# TODO: brancher l'étape pull/run sur l'infra Linagora.
53+
# Exemples possibles :
54+
# - SSH sur la VM cible et `docker compose pull && docker compose up -d`
55+
# - Mise à jour d'un Deployment Kubernetes via kubectl
56+
# - Webhook vers un orchestrateur interne

.github/workflows/pr-check.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: PR Check
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build:
12+
name: Build & typecheck
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: npm
21+
22+
- run: npm ci --no-audit --no-fund
23+
24+
- name: Type-check
25+
run: npm run typecheck
26+
27+
- name: Build site
28+
run: npm run build

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# build output
2+
dist/
3+
.astro/
4+
5+
# dependencies
6+
node_modules/
7+
8+
# logs
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
pnpm-debug.log*
13+
14+
# environment variables
15+
.env
16+
.env.production
17+
.env.local
18+
19+
# macOS
20+
.DS_Store
21+
22+
# editors
23+
.vscode/*
24+
!.vscode/extensions.json
25+
.idea
26+
*.swp
27+
28+
# pagefind output (regenerated at build)
29+
public/pagefind/

Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# syntax=docker/dockerfile:1.7
2+
3+
# ---- Build stage ----
4+
FROM node:20-alpine AS builder
5+
WORKDIR /app
6+
7+
# Install deps with cache-friendly layering
8+
COPY package.json package-lock.json* ./
9+
RUN --mount=type=cache,target=/root/.npm \
10+
npm ci --no-audit --no-fund
11+
12+
# Copy source and build (Astro + Pagefind index)
13+
COPY . .
14+
RUN npm run build
15+
16+
# ---- Runtime stage ----
17+
FROM nginx:1.27-alpine AS runtime
18+
19+
# Drop default nginx config and copy ours
20+
RUN rm /etc/nginx/conf.d/default.conf
21+
COPY nginx.conf /etc/nginx/conf.d/blog.conf
22+
23+
# Copy the built static site
24+
COPY --from=builder /app/dist /usr/share/nginx/html
25+
26+
# Run as non-root for security: shift ownership and use the bundled nginx user
27+
RUN chown -R nginx:nginx /usr/share/nginx/html
28+
29+
EXPOSE 8080
30+
31+
# nginx 1.25+ allows running as non-root via this user; rely on the default CMD.
32+
CMD ["nginx", "-g", "daemon off;"]

README.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Linagora Engineering Blog
2+
3+
Le blog d'ingénierie de Linagora — articles techniques bilingues (FR / EN) générés statiquement à partir de fichiers Markdown.
4+
5+
> _The Linagora engineering blog — bilingual (FR/EN) technical articles, statically generated from Markdown files._
6+
7+
---
8+
9+
## Stack
10+
11+
- [**Astro 5**](https://astro.build) — generateur statique
12+
- [**Tailwind CSS 4**](https://tailwindcss.com) — styling via tokens CSS
13+
- [**MDX**](https://mdxjs.com) — Markdown enrichi pour les articles
14+
- [**Shiki**](https://shiki.style) — coloration syntaxique
15+
- [**Pagefind**](https://pagefind.app) — recherche full-text côté client
16+
- **TypeScript** strict partout
17+
18+
---
19+
20+
## Démarrage rapide
21+
22+
```bash
23+
npm install
24+
npm run dev # http://localhost:4321
25+
npm run build # build statique + index Pagefind dans dist/
26+
npm run preview # serveur local pour tester le build
27+
```
28+
29+
> En `dev`, l'index de recherche Pagefind n'est pas généré : la recherche affiche un fallback.
30+
> Pour la tester en local, faire `npm run build && npm run preview`.
31+
32+
---
33+
34+
## Écrire un article
35+
36+
### 1. Nommage du fichier
37+
38+
```
39+
src/content/posts/<slug>.<lang>.md
40+
```
41+
42+
- `<slug>` : url-friendly, en kebab-case, **identique entre FR et EN** pour lier les traductions.
43+
- `<lang>` : `fr` ou `en`.
44+
45+
Exemples :
46+
47+
```
48+
src/content/posts/migrer-vers-astro.fr.md
49+
src/content/posts/migrer-vers-astro.en.md
50+
src/content/posts/post-mortem-incident-mai.fr.md # FR uniquement, OK
51+
```
52+
53+
Un article qui n'existe que dans une langue **n'apparaîtra pas dans la liste de l'autre langue** et le bouton de switch sera désactivé.
54+
55+
### 2. Frontmatter
56+
57+
```yaml
58+
---
59+
title: Mon super article
60+
description: Un résumé court (~160 caractères) qui sert pour le SEO et les cards.
61+
date: 2026-05-04 # ISO date (obligatoire)
62+
updated: 2026-06-01 # optionnel
63+
authors: [linagora-team] # IDs de fichiers dans src/content/authors/ (au moins 1)
64+
tags: [astro, devops] # optionnel
65+
cover: /covers/mon-article.jpg # optionnel — chemin dans public/
66+
coverAlt: Description alt # recommandé si cover
67+
draft: false # optionnel, défaut false
68+
---
69+
```
70+
71+
### 3. Corps
72+
73+
Markdown standard ou MDX. Coloration syntaxique automatique :
74+
75+
````markdown
76+
```ts
77+
const hello = (name: string) => `Hello ${name}!`;
78+
```
79+
````
80+
81+
### 4. Ajouter un auteur
82+
83+
```
84+
src/content/authors/<id>.md
85+
```
86+
87+
```yaml
88+
---
89+
name: Jane Doe
90+
title: Senior Software Engineer
91+
bio: Une bio courte.
92+
avatar: /authors/jane-doe.jpg # optionnel, dans public/
93+
github: janedoe # optionnel
94+
linkedin: janedoe # optionnel (slug LinkedIn)
95+
mastodon: https://floss.social/@janedoe
96+
website: https://janedoe.dev
97+
---
98+
```
99+
100+
Référencez l'auteur dans le frontmatter de votre article : `authors: [jane-doe]`.
101+
102+
### 5. Workflow
103+
104+
1. **Forkez** le dépôt (ou créez une branche si vous avez les droits)
105+
2. **Ajoutez** votre/vos fichier(s) `.md`
106+
3. **Testez** localement : `npm run build`
107+
4. **Ouvrez une Pull Request** vers `main`
108+
109+
La CI valide le build et le typecheck. Après merge sur `main`, l'image Docker est publiée sur GHCR.
110+
111+
---
112+
113+
## Structure du projet
114+
115+
```
116+
src/
117+
├── content/
118+
│ ├── posts/ # ← articles .md (un par langue)
119+
│ └── authors/ # ← profils auteurs
120+
├── content.config.ts # schémas Zod (frontmatter)
121+
├── i18n/ # strings UI + helpers de routing
122+
├── layouts/ # Base + Post layouts
123+
├── components/ # composants Astro réutilisables
124+
├── pages/[lang]/ # pages dynamiques par langue
125+
├── lib/posts.ts # helpers de requête sur la collection
126+
├── styles/global.css # tokens CSS + styles prose
127+
└── site.config.ts # nom, URL, infos du repo GitHub
128+
```
129+
130+
---
131+
132+
## Personnaliser
133+
134+
| Quoi ||
135+
|---|---|
136+
| Nom du blog, repo GitHub, URLs sociaux | `src/site.config.ts` |
137+
| Tokens couleur, typo, dark mode | `src/styles/global.css` |
138+
| Strings d'interface (FR/EN) | `src/i18n/ui.ts` |
139+
| Page À propos | `src/pages/[lang]/about.astro` |
140+
| Logo / favicon | `public/favicon.svg` |
141+
| Image OG par défaut | `public/og-default.svg` _(remplaçable par `.png` 1200×630)_ |
142+
143+
---
144+
145+
## Déploiement
146+
147+
### Image Docker
148+
149+
```bash
150+
docker build -t linagora/engineering-blog:latest .
151+
docker run -p 8080:8080 linagora/engineering-blog:latest
152+
```
153+
154+
L'image expose le site sur le port `8080` via nginx.
155+
156+
### CI/CD
157+
158+
Le workflow `.github/workflows/deploy.yml` builde et pousse l'image sur **GHCR** (`ghcr.io/linagora/engineering-blog`) à chaque push sur `main`.
159+
160+
**À brancher sur l'infra Linagora** (étape laissée en TODO dans le workflow) :
161+
- Soit SSH + `docker compose pull && docker compose up -d`
162+
- Soit `kubectl set image` sur un Deployment Kubernetes
163+
- Soit webhook vers un orchestrateur interne
164+
165+
### Variables / configuration
166+
167+
| Endroit | Quoi |
168+
|---|---|
169+
| `src/site.config.ts` | URL canonique du site (modifie `SITE.url`), org/repo GitHub pour les liens "Edit on GitHub" |
170+
| `astro.config.ts` | Config i18n, redirects, intégrations |
171+
172+
---
173+
174+
## Recherche
175+
176+
Pagefind indexe le contenu balisé `data-pagefind-body` (= chaque article). L'index est généré à la fin de `npm run build` dans `dist/pagefind/`.
177+
178+
Le filtre par langue est automatique : Pagefind respecte l'attribut `lang` du `<html>` de chaque page.
179+
180+
Raccourci : <kbd>⌘K</kbd> / <kbd>Ctrl+K</kbd>.
181+
182+
---
183+
184+
## Licence
185+
186+
Le code de ce blog est sous licence [AGPL-3.0]. Les articles publiés appartiennent à leurs auteurs et sont diffusés sous [CC BY-SA 4.0] sauf mention contraire dans le frontmatter (`license: ...`).
187+
188+
[AGPL-3.0]: https://www.gnu.org/licenses/agpl-3.0.html
189+
[CC BY-SA 4.0]: https://creativecommons.org/licenses/by-sa/4.0/

astro.config.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { defineConfig } from 'astro/config';
2+
import mdx from '@astrojs/mdx';
3+
import sitemap from '@astrojs/sitemap';
4+
import tailwindcss from '@tailwindcss/vite';
5+
6+
import { SITE } from './src/site.config';
7+
import { remarkReadingTime } from './src/lib/remark-reading-time';
8+
9+
export default defineConfig({
10+
site: SITE.url,
11+
trailingSlash: 'never',
12+
build: {
13+
format: 'directory',
14+
},
15+
i18n: {
16+
defaultLocale: 'fr',
17+
locales: ['fr', 'en'],
18+
routing: { prefixDefaultLocale: true },
19+
},
20+
redirects: {
21+
'/': '/fr',
22+
},
23+
integrations: [
24+
mdx(),
25+
sitemap({
26+
i18n: {
27+
defaultLocale: 'fr',
28+
locales: { fr: 'fr-FR', en: 'en-US' },
29+
},
30+
}),
31+
],
32+
vite: {
33+
// Tailwind v4 Vite plugin types currently mismatch Astro's Vite version.
34+
// It works at runtime; cast to silence the type-check.
35+
plugins: [tailwindcss()] as never,
36+
},
37+
markdown: {
38+
remarkPlugins: [remarkReadingTime],
39+
shikiConfig: {
40+
themes: {
41+
light: 'github-light',
42+
dark: 'github-dark-dimmed',
43+
},
44+
wrap: true,
45+
},
46+
},
47+
});

0 commit comments

Comments
 (0)