
blog-writer
Write bilingual blog articles for the personal website. Use when creating a new blog post, article,
Blog Article Writer
Create bilingual (English/Spanish) blog articles for javieraguilar.ai.
File Locations
- English:
src/content/blog/en/[slug].md - Spanish:
src/content/blog/es/[slug].md - Images:
public/blog/[image-name].png(referenced as/blog/[image-name].png)
Required Frontmatter Format
Both EN and ES files must include this exact frontmatter:
---
title: "Article Title Here"
description: "A concise description for SEO and preview cards (1-2 sentences)."
pubDate: YYYY-MM-DD
tags: ["Tag1", "Tag2", "Tag3"]
lang: en # or es
translationKey: article-slug
heroImage: "/blog/article-slug.png"
---
Field Requirements
| Field | Required | Notes |
|---|---|---|
title | Yes | Translated per language |
description | Yes | Translated, SEO-friendly, 1-2 sentences |
pubDate | Yes | Same date for both languages. Must be the date the article actually gets merged to main — if the publication is scheduled via scheduled-publish-<slug>.yml, use that cron's date, and update it if the schedule moves. The blog index sorts and displays by pubDate, so a stale draft date makes the article appear under an older date instead of as the newest post. |
tags | Yes | Translated (e.g., "AI" → "IA") |
lang | Yes | Must be en or es |
translationKey | Yes | Same value for EN/ES pair (kebab-case) |
heroImage | Yes | Path to thumbnail (e.g., /blog/my-article.png). Generate with script below. |
LinkedIn Automation (Optional)
| Campo | Tipo | Descripción |
|---|---|---|
linkedinImage | string | Ruta a imagen para LinkedIn (ej: /blog/linkedin-card.png) |
repoUrl | string | URL del repo/código del artículo (ej: https://github.com/JaviMaligno/...) |
linkedinImage:
- Campo opcional
- Solo se usa para auto-publicación en LinkedIn
- Si se omite, se usa
heroImagecomo fallback (publish-to-linkedin.js:92hacelinkedinImage || heroImage); el post solo sale sin imagen si tampoco hay hero - Ruta debe apuntar a archivo en
public/blog/ - Formatos: PNG, JPG, WEBP
- Tamaño recomendado: 1200x627px
repoUrl (importante para artículos con código):
- Si el artículo tiene un repo/fork asociado, ponlo siempre en el frontmatter de EN y ES.
buildPostTextenscripts/linkedin/utils.jsañade una línea💻 Code: <url>al post de LinkedIn solo si este campo existe. Sin él, el enlace al código no sale y hay que editar el post a mano tras publicar (le pasó acoding-agents-structureyrestart-vs-iterate).- Regla: cualquier enlace externo relevante (repo, fork, paper) debe llegar al post de LinkedIn —
repoUrlpara el código,linkedinLinkspara papers/preprints.
Decide la imagen de LinkedIn ANTES de publicar. El post se genera en el push del merge; una vez publicado no se puede cambiar la imagen — hay que borrar el post en LinkedIn y repostearlo a mano con scripts/linkedin/post-standalone.js. Así que la elección se hace en el frontmatter, no después.
Recorre las imágenes del artículo y pregúntate cuál para más el scroll. Si alguna es más fuerte que el hero, ponla como linkedinImage en EN y ES:
- Gráficos generados (resultados, curvas, benchmarks): un dato concreto suele ganar a una ilustración. Precedente:
forgetting-you-dont-measureusalinkedinImage: /blog/forgetting-mixing-curve.png(la curva de mixing del experimento) mientras el hero es una ilustración. - Memes y capturas (tuits, pantallazos, imágenes citadas en el artículo): si el artículo se apoya en un meme o una captura reconocible, casi siempre es la mejor imagen para el feed. Precedente:
death-of-prompt-engineeringsalió con el hero porque el campo estaba vacío, y hubo que borrar el post y repostearlo con/blog/conjecture-disproved-meme.jpg(la captura del tuit original que cita el artículo).
Si eliges una imagen que ya está dentro del artículo, usa el archivo original (la captura o figura real), no una versión regenerada.
Ejemplo:
---
title: "Mi Artículo"
description: "Descripción del artículo"
pubDate: 2026-01-07
tags: ["AI", "Automation"]
lang: en
translationKey: mi-articulo
heroImage: "/blog/mi-articulo.png"
linkedinImage: /blog/linkedin-card.png # Opcional
---
Hero Image Generation
Every article must have a heroImage. Generate it using Codex CLI, which has access to OpenAI's image generation models (gpt-image-2).
How to Generate
Delegate the final prompt to Codex. Codex knows its own image model better than we do: don't hand it a rigid pre-baked prompt — give it the article context plus the style constraints below and let it write and run the final image prompt itself.
codex exec --full-auto "Read the article at src/content/blog/en/article-slug.md. Craft an image-generation prompt for its hero image following these style constraints: [STYLE CONSTRAINTS BELOW]. Then generate the image with your image_gen tool and save it to public/blog/article-slug.png. Finally, print the exact prompt you used."
Codex will use its built-in image_gen tool, generate the image, and copy it to the specified path in the project.
Prompt Style Constraints
Do NOT use the old "minimalist isometric illustration" template. It produced images that were visually clean but simplistic and schematic. The proven style is a rich technical editorial illustration (see public/blog/bootstrap-cloud-environments.png on main as the reference result, and docs/marketing/image-prompts.md for its exact prompt).
Anatomy of a good prompt (based on the reference):
Create a 1020x510 blog hero image for a technical article titled "[TITLE]".
Style: refined technical editorial illustration, dark but not monochrome, showing [CONCRETE SCENE — the article's core concept as a working system, not an abstract metaphor].
Visual motifs: [3-6 SPECIFIC ELEMENTS — e.g. terminal window with readable but generic lines like "session-start.sh", labeled package boxes, arrows between components, a laptop, a CI runner], clean geometric composition.
No logos, no brand names, no people, no text-heavy poster.
Use crisp bitmap illustration, high contrast, professional AI/developer blog aesthetic, balanced teal, amber, graphite, and off-white accents, no purple gradient blobs, no bokeh.
Key learnings vs the old template:
- Readable generic text is GOOD: terminal lines, box labels, checklists with plausible-but-generic content make the image concrete and credible. Only avoid text-heavy posters (the image shouldn't be a wall of text).
- Concrete scene over abstract metaphor: depict the actual system/workflow the article describes (terminals, workspaces, pipelines, charts) rather than floating nodes and glowing cubes.
- Density is a feature: several distinct zones (a terminal, a diagram, a desk with objects) beat a single centered icon.
- Palette: balanced teal, amber, graphite, off-white on dark — explicitly ban purple gradient blobs and bokeh.
- Size: 1020x510 (≈2:1 hero ratio).
Workflow
- After writing the article content, run Codex with the article path + style constraints (command above)
- Show the generated image to the user for review (use Read tool on the PNG)
- If approved, add
heroImage: "/blog/article-slug.png"to both EN and ES frontmatter - If not approved, adjust the constraints and regenerate
- Record the exact prompt used in
docs/marketing/image-prompts.md(article path, image path, generation date, prompt in a code block — follow the existing entries' format; create the file if missing) - Codex image quality is trusted — no need for comparison with other generators
Requirements
- Codex CLI installed (
codexcommand available) - OpenAI API key configured in Codex
Bilingual Workflow
- Always create both files with matching
translationKey - Use the same
pubDatefor synchronized release - Translate tags appropriately (common: AI→IA, Automation→Automatización)
- Keep
translationKeyidentical in both files
Content Structure
Follow this pattern:
Opening paragraph establishing context and the problem/topic.
## Section Heading
Content with clear explanations. Focus on "why" not just "how".
### Subsection (if needed)
- Bullet points for lists
- Keep them concise
## Another Section
Include practical examples:
```language
code block with syntax highlighting
Conclusion/Next Steps
Wrap up with actionable takeaways or links.
Footer with links to resources, repos, etc.
## Writing Style
- **Tone**: Professional but conversational
- **Focus**: Practical value, real examples
- **Length**: 800-2000 words typically
- **Structure**: Clear headings, scannable sections
- **Code**: Include relevant code snippets with language identifiers
- **Images**: Always use **absolute URLs** for inline images: `https://www.javieraguilar.ai/blog/image-name.png`. Dev.to cannot resolve relative paths (`/blog/...`), and the publish script auto-converts relative paths but using absolute URLs from the start avoids issues. The `heroImage` frontmatter field can remain a relative path (it's converted by the publish script).
## Video Embeds
### For the Website (javieraguilar.ai)
Use HTML iframe embeds for video platforms. The website supports full HTML.
**Loom example:**
```html
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe src="https://www.loom.com/embed/VIDEO_ID"
frameborder="0"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
</div>
YouTube example:
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
frameborder="0"
allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
</div>
Dev.to Compatibility
Important: Dev.to filters HTML iframes for security. The publish script (scripts/devto/publish-to-devto.js) automatically transforms video embeds:
- Loom iframes → Markdown links with note
- YouTube iframes → Could be transformed to
{% youtube %}liquid tags (not implemented yet)
What gets sent to Dev.to:
**🎥 [Watch the video demo on Loom](https://www.loom.com/share/VIDEO_ID)**
> _Note: Interactive video player available on the [original article](CANONICAL_URL)_
Best Practice:
- Always use iframe embeds in the markdown
- The Dev.to publish script handles the transformation automatically
- Don't manually create different versions for Dev.to
Tag Conventions
Common tag translations:
| English | Spanish |
|---|---|
| AI | IA |
| Automation | Automatización |
| Machine Learning | Machine Learning |
| Development | Desarrollo |
| Architecture | Arquitectura |
Example Frontmatter Pair
English (src/content/blog/en/my-new-post.md):
---
title: "Building Something Cool"
description: "How I built a tool that solves a real problem."
pubDate: 2025-01-03
tags: ["AI", "Automation", "Claude"]
lang: en
translationKey: my-new-post
heroImage: "/blog/my-new-post.png"
---
Spanish (src/content/blog/es/my-new-post.md):
---
title: "Construyendo Algo Genial"
description: "Cómo construí una herramienta que resuelve un problema real."
pubDate: 2025-01-03
tags: ["IA", "Automatización", "Claude"]
lang: es
translationKey: my-new-post
heroImage: "/blog/my-new-post.png"
---
Writing from LinkedIn Posts
When repurposing a LinkedIn post into a blog article:
- Fetch the post using WebFetch to extract the content
- Download any images from the post to
public/blog/ - Expand the content - LinkedIn posts are short; blog articles should:
- Add more context and background
- Include code examples if relevant
- Expand on points that were condensed
- Add sections the post didn't have room for
- Keep the core message but make it more comprehensive
- Use original post date as
pubDatefor authenticity
LinkedIn Image Download
Images from LinkedIn posts should be:
- Downloaded to
public/blog/[descriptive-name].png - Named descriptively (e.g.,
azure-content-filter-demo.png) - Referenced in markdown as
/blog/[name].png
Render for Review (do this by default)
Never hand back a finished article as raw markdown and stop there. Always render it and give the user a URL, so they review it the way a reader will see it — typography, headings, code blocks, hero image in place, links resolving.
npm run build # validates frontmatter against the content schema and catches broken pages
npm run dev # run in background; serves on http://localhost:4321
Then give the user both language URLs explicitly:
http://localhost:4321/en/blog/<slug>http://localhost:4321/es/blog/<slug>
Rules:
- Build before serving. A frontmatter or schema error fails the build; catching it here beats the user hitting an error page.
- Verify the article actually generated:
ls dist/en/blog/ | grep <slug>and the same fores. - Show the hero image inline (Read tool on the PNG) as well, since it is easy to miss in a page scroll, and call out any visible text artefacts in the generated image — image models frequently mangle labels.
- Only after the user has reviewed the rendered pages should you consider the article done, commit it, or schedule publication.
Checklist Before Publishing
-
npm run buildpasses and both pages appear indist/en/blog/anddist/es/blog/ - Rendered pages reviewed by the user at
localhost:4321(both languages) — not just the markdown - Both EN and ES files created
- Matching
translationKeyin both - Same
pubDatein both -
pubDateequals the actual publication date (thescheduled-publish-<slug>.ymlcron date, or today if merging manually) — not the date the draft was written - Tags translated appropriately
-
langfield matches file location - Hero image generated, reviewed by user, and placed in
public/blog/ -
heroImagefield set in both EN and ES frontmatter - Image prompt recorded in
docs/marketing/image-prompts.md -
linkedinImagedecidido antes de publicar — revisadas todas las imágenes del artículo (gráficos, memes, capturas) y puesta la más llamativa en EN y ES si supera al hero. Sin este campo se publica el hero (linkedinImage || heroImage), y cambiarlo después obliga a borrar el post y repostearlo a mano. - If the article involves code,
repoUrl:set in both EN and ES frontmatter — the LinkedIn auto-post (scripts/linkedin/utils.js) adds a "💻 Code:" line only when this field is present. Omitting it means editing the LinkedIn post by hand after publish. - Links are valid and functional