Skip to content

Commit b5d8811

Browse files
committed
refactor(blog): factorize common data to manage localized content with ease
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent b31483b commit b5d8811

File tree

26 files changed

+607
-242
lines changed

26 files changed

+607
-242
lines changed

.github/agents/blog-post.md

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,100 @@ Follow [../AGENTS.md](../AGENTS.md) before working in this repository.
7474
- Clarify: **Audience** (Platform engineers? SREs? CTOs?) | **Goal** (what action?) | **Angle** (unique perspective?)
7575
- Research: 3-5 key points, DORA/SPACE data, code examples, Hoverkraft services to link
7676

77-
### 2. Frontmatter
77+
### 2. Structure & Frontmatter
78+
79+
**Folder Structure**: Each blog post lives in its own folder with separate language files and shared metadata.
80+
81+
```txt
82+
/application/src/data/post/{translation-key}/
83+
fr.mdx # French content
84+
en.mdx # English content
85+
common.yaml # Shared metadata
86+
```
87+
88+
**common.yaml** (shared metadata):
7889

7990
```yaml
80-
---
8191
publishDate: 2025-01-15T00:00:00Z
82-
title: "Compelling title <60 chars" # FR: conversational; EN: direct, benefit-focused
83-
excerpt: "One-sentence summary with value/metric (140-160 chars)"
84-
image: ~/assets/images/blog/post-slug/preview.png
92+
translationKey: "translation-key" # Same as folder name - identifier for translation mapping
93+
image: ~/assets/images/blog/{translation-key}/preview.png
8594
tags: [platform-engineering, kubernetes, dora] # 3-6 tags, lowercase-hyphenated
8695
category: "Platform Engineering" # or "DevOps & SRE", "Cloud Native", "Developer Experience", "Open Source"
87-
author: "Équipe HoverKraft" # or "Hoverkraft Team" for EN
88-
lang: fr # or "en"
96+
```
97+
98+
**fr.mdx** frontmatter:
99+
100+
```yaml
101+
---
102+
title: "Titre accrocheur <60 caractères"
103+
excerpt: "Résumé en une phrase avec valeur/métrique (140-160 caractères)"
104+
author: "Équipe HoverKraft"
105+
lang: fr
106+
slug: "titre-francais-du-post"
89107
---
90108
```
91109

110+
**en.mdx** frontmatter:
111+
112+
```yaml
113+
---
114+
title: "Compelling title <60 chars"
115+
excerpt: "One-sentence summary with value/metric (140-160 chars)"
116+
author: "Hoverkraft Team"
117+
lang: en
118+
slug: "english-post-title"
119+
---
120+
```
121+
122+
**Critical Fields**:
123+
124+
- `slug`: **Localized** URL slug in MDX files (different per language)
125+
- `translationKey`: **Shared** identifier in `common.yaml` (folder name, same across languages) for translation mapping
126+
- `lang`: Language code in MDX files (`fr` or `en`)
127+
- All other metadata in `common.yaml` is shared between languages
128+
129+
**fr.mdx** frontmatter:
130+
131+
```yaml
132+
---
133+
title: "Titre accrocheur <60 caractères"
134+
excerpt: "Résumé en une phrase avec valeur/métrique (140-160 caractères)"
135+
author: "Équipe HoverKraft"
136+
lang: fr
137+
slug: "titre-francais-du-post"
138+
translationKey: "translation-key" # Same as folder name
139+
---
140+
```
141+
142+
**en.mdx** frontmatter:
143+
144+
```yaml
145+
---
146+
title: "Compelling title <60 chars"
147+
excerpt: "One-sentence summary with value/metric (140-160 chars)"
148+
author: "Hoverkraft Team"
149+
lang: en
150+
slug: "english-post-title"
151+
translationKey: "translation-key" # Same as folder name
152+
---
153+
```
154+
155+
**Critical Fields**:
156+
157+
- `slug`: **Localized** URL slug (different per language)
158+
- `translationKey`: **Shared** identifier (folder name, same across languages) for translation mapping
159+
- `lang`: Language code (`fr` or `en`)
160+
- All other metadata in `common.yaml` is shared between languages
161+
92162
### 3. Write Content
93163

164+
**Create Folder & Files**: Start by creating the folder structure:
165+
166+
```bash
167+
mkdir -p /application/src/data/post/{translation-key}
168+
touch /application/src/data/post/{translation-key}/{fr.mdx,en.mdx,common.yaml}
169+
```
170+
94171
**French first** (canonical): Use "nous" (we) for Hoverkraft, "vous" (formal) for readers. Keep technical terms in English when standard (Kubernetes, GitOps).
95172

96173
**Structure**: Opening blockquote → Intro (1-2 short ¶) → Main sections (3-5) → Conclusion (1 ¶) → CTAs (1-2 links)
@@ -196,18 +273,18 @@ Lighting: flat, clean, no noise.
196273
- ❌ Do not request screenshots or mimic brand logos
197274
- ❌ Avoid vague adjectives like “nice” or “cool”
198275

199-
**File Org**: Save to `/application/src/assets/images/blog/{post-slug}/`
276+
**File Org**: Save to `/application/src/assets/images/blog/{translation-key}/`
200277

201278
- `preview.png` (required, ≤2MB)
202279
- `descriptive-name.webp` (supporting visuals, 15-35KB target)
203-
- Use kebab-case, descriptive names
280+
- Use kebab-case, descriptive names (match `translationKey`, not localized slug)
204281

205282
### 5. Import Images
206283

207284
```typescript
208285
import Image from "~/components/common/Image.astro";
209-
import preview from "~/assets/images/blog/post-slug/preview.png";
210-
import diagram from "~/assets/images/blog/post-slug/architecture.webp";
286+
import preview from "~/assets/images/blog/{translation-key}/preview.png";
287+
import diagram from "~/assets/images/blog/{translation-key}/architecture.webp";
211288
```
212289

213290
Reference:
@@ -226,10 +303,14 @@ Reference:
226303

227304
### 7. Review Checklist
228305

229-
- [ ] French and English complete and semantically equivalent
230-
- [ ] Frontmatter valid with all fields (publishDate, title, excerpt, image, tags, category, author, lang)
306+
- [ ] Folder created with correct `translationKey` as name
307+
- [ ] `common.yaml` created with shared metadata (publishDate, translationKey, image, tags, category)
308+
- [ ] `translationKey` in `common.yaml` matches folder name
309+
- [ ] French (`fr.mdx`) and English (`en.mdx`) files complete and semantically equivalent
310+
- [ ] Each MDX has all required frontmatter fields (title, excerpt, author, lang, slug)
311+
- [ ] `slug` is **localized** (different per language) and descriptive
231312
- [ ] Opening blockquote compelling (<100 chars)
232-
- [ ] All images generated and saved correctly
313+
- [ ] All images generated and saved correctly in `/application/src/assets/images/blog/{translationKey}/`
233314
- [ ] Images imported with descriptive `alt` text
234315
- [ ] Image prompts produce clean, geometric visuals with Hoverkraft palette (≤3 colors)
235316
- [ ] Code blocks have language specified
@@ -277,12 +358,27 @@ make lint-fix
277358

278359
```txt
279360
/application/src/data/post/
280-
{post-slug-fr}.mdx # French
281-
{post-slug-en}.mdx # English
361+
{translation-key}/ # Folder named after shared translation key
362+
fr.mdx # French content with localized frontmatter
363+
en.mdx # English content with localized frontmatter
364+
common.yaml # Shared metadata (publishDate, image, tags, category)
365+
366+
/application/src/assets/images/blog/{translation-key}/
367+
preview.png # 1536×1024 social preview (required)
368+
*.webp # Diagrams, charts (~15-35KB each)
369+
```
282370

283-
/application/src/assets/images/blog/{post-slug}/
284-
preview.png # 1536×1024 social preview (required)
285-
*.webp # Diagrams, charts (~15-35KB each)
371+
**Example**:
372+
373+
```txt
374+
/application/src/data/post/modern-platform-characteristics/
375+
fr.mdx # slug: "caracteristiques-plateforme-moderne"
376+
en.mdx # slug: "platform-engineering-modern-characteristics"
377+
common.yaml # publishDate, image, tags, category
378+
379+
/application/src/assets/images/blog/modern-platform-characteristics/
380+
preview.png
381+
architecture.webp
286382
```
287383

288384
## Resources

.github/dependabot.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ version: 2
33
updates:
44
- package-ecosystem: "npm"
55
open-pull-requests-limit: 20
6-
directory: "application/"
6+
directories:
7+
- "application/"
8+
- ".github/actions/**/*"
79
versioning-strategy: increase
810
schedule:
911
interval: "weekly"

application/astro.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { AstroIntegration } from 'astro';
1414
import astrowind from './vendor/integration';
1515

1616
import { readingTimeRemarkPlugin, responsiveTablesRehypePlugin, lazyImagesRehypePlugin } from './src/utils/frontmatter';
17+
import { injectCommonData } from './src/loaders/inject-common-data';
1718

1819
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1920

@@ -75,7 +76,7 @@ export default defineConfig({
7576
},
7677

7778
markdown: {
78-
remarkPlugins: [readingTimeRemarkPlugin],
79+
remarkPlugins: [injectCommonData, readingTimeRemarkPlugin],
7980
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
8081
},
8182

application/src/components/blog/RelatedPosts.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import { getRelatedPosts } from '~/utils/blog';
55
import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
66
import type { Post } from '~/types';
77
import { getBlogPermalink } from '~/utils/permalinks';
8+
import { getLangFromUrl, useTranslations } from '~/i18n/utils';
89
910
export interface Props {
1011
post: Post;
1112
}
1213
1314
const { post } = Astro.props;
15+
const lang = getLangFromUrl(Astro.url);
16+
const t = useTranslations(lang);
1417
1518
const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
1619
---
@@ -22,8 +25,8 @@ const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
2225
container:
2326
'pt-0 lg:pt-0 md:pt-0 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
2427
}}
25-
title="Related Posts"
26-
linkText="View All Posts"
28+
title={t('blog.related-posts')}
29+
linkText={t('blog.view-all')}
2730
linkUrl={getBlogPermalink()}
2831
postIds={relatedPosts.map((post) => post.id)}
2932
/>

application/src/components/common/LanguageSelector.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const translatePath = useTranslatedPath(currentLang);
1515
1616
// Get current path without language prefix
1717
const pathname = Astro.url.pathname;
18-
const pathWithoutLang = pathname.replace(/^\/(en|fr)(\/|$)/, '/');
18+
// Remove language prefix if present (e.g., /en/blog/... or /fr/blog/...)
19+
const pathWithoutLang = pathname.replace(/^\/(en|fr)(?:\/|$)/, '');
1920
const normalizedPath = pathWithoutLang === '' ? '/' : pathWithoutLang;
2021
2122
// Function to get the equivalent path in the target language

application/src/content/config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z, defineCollection } from 'astro:content';
2-
import { glob } from 'astro/loaders';
2+
import { blogPostLoader } from '~/loaders/blog-post-loader';
33

44
const metadataDefinition = () =>
55
z
@@ -47,7 +47,7 @@ const metadataDefinition = () =>
4747
.optional();
4848

4949
const postCollection = defineCollection({
50-
loader: glob({ pattern: ['**/*.md', '**/*.mdx'], base: 'src/data/post' }),
50+
loader: blogPostLoader(),
5151
schema: z.object({
5252
publishDate: z.date().optional(),
5353
updateDate: z.date().optional(),
@@ -60,6 +60,7 @@ const postCollection = defineCollection({
6060
category: z.string().optional(),
6161
tags: z.array(z.string()).optional(),
6262
author: z.string().optional(),
63+
slug: z.string().optional(),
6364
lang: z.enum(['fr', 'en']).optional().default('fr'), // Default language is French
6465
translationKey: z.string().optional(),
6566

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
publishDate: 2025-08-25T00:00:00Z
2+
image: ~/assets/images/blog/images-bitnami-broadcom/preview.png
3+
tags:
4+
- kubernetes
5+
- sre
6+
- bitnami
7+
- broadcom
8+
- docker
9+
category: Platform Engineering
10+
translationKey: bitnami-broadcom-images

application/src/data/post/broadcom-bitnami-images.mdx renamed to application/src/data/post/bitnami-broadcom-images/en.mdx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
---
2-
publishDate: 2025-08-25T00:00:00Z
32
title: 'Broadcom pulls public Bitnami images: how Hoverkraft keeps your clusters safe'
43
excerpt: 'Broadcom removed anonymous access to Bitnami Docker images. Here is how we mitigate the impact for Kubernetes teams and buy time for a clean migration.'
5-
image: ~/assets/images/blog/images-bitnami-broadcom/preview.png
6-
tags:
7-
- kubernetes
8-
- sre
9-
- bitnami
10-
- broadcom
11-
- docker
12-
category: 'Platform Engineering'
4+
slug: broadcom-bitnami-images
135
author: 'Hoverkraft Team'
146
lang: en
15-
translationKey: bitnami-broadcom-images
167
---
178

189
> ⚠️ Coming back from holiday to discover your registries are empty is not fun. That is exactly what the Bitnami announcement created.

application/src/data/post/images-bitnami-broadcom.mdx renamed to application/src/data/post/bitnami-broadcom-images/fr.mdx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
---
2-
publishDate: 2025-08-25T00:00:00Z
3-
title: "Broadcom coupe l'accès aux images Bitnami : comment nous sécurisons vos clusters"
2+
title: "Broadcom coupe l'accès aux images Bitnami : comment nous sécurisons vos clusters"
43
excerpt: 'Broadcom met fin à la diffusion publique des images Docker Bitnami. Voici comment hoverkraft protège les équipes Kubernetes et gagne du temps de migration.'
5-
image: ~/assets/images/blog/images-bitnami-broadcom/preview.png
6-
tags:
7-
- kubernetes
8-
- sre
9-
- bitnami
10-
- broadcom
11-
- docker
4+
slug: images-bitnami-broadcom
125
lang: fr
13-
translationKey: bitnami-broadcom-images
146
---
157

168
> ⚠️ Retour de vacances mouvementé pour les équipes plateformes : Broadcom retire les images Docker Bitnami de l'accès public.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
publishDate: 2025-01-31T00:00:00Z
2+
image: ~/assets/images/blog/ci-dokumentor/preview.png
3+
tags:
4+
- ci-cd
5+
- documentation
6+
- automation
7+
- devex
8+
- open-source
9+
category: Developer Experience
10+
translationKey: ci-dokumentor

0 commit comments

Comments
 (0)