Add HTML email generator

This commit is contained in:
Pascal Le Merrer 2025-12-26 22:25:22 +01:00
parent 35b999c8c6
commit f1864d8841
14 changed files with 1748 additions and 16 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
output
__pycache__
mdtosendy
mail/

View file

@ -39,6 +39,7 @@ help:
@echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 '
@echo ' make devserver [PORT=8000] serve and regenerate together '
@echo ' make devserver-global regenerate and serve on 0.0.0.0 '
@echo ' make mail file=FILEPATH generate HTML email '
@echo ' '
@echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html '
@echo 'Set the RELATIVE variable to 1 to enable relative urls '
@ -72,4 +73,7 @@ publish:
ssh:
ssh craftletter@ssh-craftletter.alwaysdata.net
.PHONY: html help clean regenerate serve serve-global devserver devserver-global publish
mail:
PYTHONPATH=PWD venv/bin/python ./prepare_email.py $(file)
.PHONY: html help clean regenerate serve serve-global devserver devserver-global publish mail

View file

@ -0,0 +1,450 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Craft Letter n°4</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; background: linear-gradient(to bottom, #0a1a26, #0f3d5f); background-color: #0a1a26; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(to bottom, #0a1a26, #0f3d5f); background-color: #0a1a26">
<tr>
<td align="center" style="padding: 20px 0;">
<!-- Main Container -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600"
style="background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)">
<!-- Header Image -->
<tr>
<td align="center" style="padding: 0;">
<img alt="Logo Craft Letter" src="https://www.craftletter.fr/images/craftletter.svg" width="600"
height="185"
style="display: block; width: 600px; height: 185px; border: 0; border-radius: 4px 4px 0 0;" />
</td>
</tr>
<!-- Content Area -->
<tr>
<td style="padding: 40px 40px 30px 40px;">
<body>
<meta charset="utf-8">
<title>Lettre n°4 - 29 Décembre 2025</title>
<meta name="date" content="2025-12-29 09:00">
<meta name="category" content="Newsletter">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"name": "Lettre n°4 - 29 Décembre 2025",
"description": "Lettre de veille technologique en développement logiciel",
"image": [
"https://www.craftletter.fr/images/craftletter.svg"
],
"datePublished": "Mon Dec 29 2025 09:00:00 GMT+0200 (Coordinated Universal Time)",
"author": {
"@type": "Person",
"name": "Pascal Le Merrer",
"url": "https://www.linkedin.com/in/pascal-le-merrer/"
}
}
</script>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td align="center" style="padding: 0;">
<img class="logo" alt="Logo Craft Letter" src="{static}/images/craftletter.svg" style="max-width: 100%; height: auto; border: 0; display: block; margin: 1em 0">
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h1 id="craftlettern°4" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Craft Letter n°4</h1>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="Édito" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Édito</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Cette semaine encore, le contenu de cette newsletter est varié. Je vais en effet vous parler doutillage, de langages fonctionnels, de recrutement, de sécurité, dapprentissage ou de révision, et, pour finir, darchitecture basée sur des cellules. Concernant les langages fonctionnels, ceux que jévoquerai sont mes deux langages favoris : ils associent la simplicité à un système de type qui élimine des classes entières derreurs. Le tout accompagné dune expérience développeur particulièrment agréable.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Passez de bonnes fêtes de fin dannée, et bonne lecture !
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="ty" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">ty</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Après <a href="https://github.com/astral-sh/ruff" style="color: #0066cc; text-decoration: underline">ruff</a> (linter et formateur) et <a href="https://github.com/astral-sh/uv" style="color: #0066cc; text-decoration: underline">uv</a> (un gestionnaire de dépendances), <a href="https://astral.sh/" style="color: #0066cc; text-decoration: underline">Astral</a> nous propose un troisième outil pour Python : <a href="https://docs.astral.sh/ty/" style="color: #0066cc; text-decoration: underline">ty</a>, un vérificateur de types (<em style="font-style: italic">type checker</em>) et serveur de langage. Comme ruff et uv, ty est écrit en Rust, et bénéficie de performances bien meilleures que celles des outils concurrents. ruff et uv étant des réussites, ty mérite quon le regarde de près.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="visualiserdeslogsavechl" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Visualiser des logs avec HL</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
<a href="https://github.com/pamburus/hl?tab=readme-ov-file#performing-complex-queries%5D" style="color: #0066cc; text-decoration: underline">hl</a> est un outil de visualisation de log, écrit en Rust —cest devenu un argument marketing. Il est capable de capable visualiser, filtrer et requêter rapidement des fichiers de plusieurs gigaoctets.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="unelistedoutilsautourdejujutsu" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Une liste doutils autour de Jujutsu</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
<a href="https://www.jj-vcs.dev/" style="color: #0066cc; text-decoration: underline">Jujutsu</a> —jj— est une alternative à Git, à la fois plus puissant et plus simple dutilisation. Il utilise Git comme back-end, ce qui veut dire que vous pouvez lutiliser de façon transparente sur un projet dont les autres contributeurs utilisent Git. <a href="https://github.com/Necior/awesome-jj" style="color: #0066cc; text-decoration: underline">Awesome JJ</a> liste des outils de lécosystème naissant autour de cet outil.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Jutilise Jujutsu depuis 8 mois, sur des projets personnels pour linstant, et je le préfère largement à Git. Le nommage des commandes et options est bien plus clair, ce qui fait quon les retient ou retrouve plus facilement. Le workflow est plus simple —car il y a moins de concepts. Malgré cela, il est plus puissant que Git. Par exemple, il ne requiert de résoudre quune seule fois les conflits lors dun rebase, là où Git peut vous demander de les résoudre à plusieurs reprises.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="commenttrouverdesdéveloppeurspourunlangagefonctionnel" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Comment trouver des développeurs pour un langage fonctionnel</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Dans sa <a href="https://www.youtube.com/watch?v=9OtN4iiFBsQ" style="color: #0066cc; text-decoration: underline">keynote lors des Scala Days 2025 🇬🇧</a>, Evan Czaplicki explique comment <a href="https://www.youtube.com/watch?v=9OtN4iiFBsQ" style="color: #0066cc; text-decoration: underline">Elm</a>, le langage quil a créé il y a bientôt 14 ans, peut servir de porte dentrée pour des langages fonctionnels plus complexes, comme Haskell ou Scala. Elm est volontairement simple : les concepts y sont limités au strict minimum. Malgré une syntaxe qui peut intimider au départ, lapprentissage est rapide, notamment parce que lexpérience développeur est vraiment agréable. Vous trouvez sympathiques les messages derreur de Rust ? Ils sont inspirés de ceux de Elm.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Evan partage dans cette keynote les stratégies de recrutement dentreprises florissantes qui utilisent Elm. Certaines recrutent les développeurs ou développeuses de modules populaires. Dautres embauchent des débutants et les forment. Les qualités du langage font que ces juniors sont opérationnels et productifs en quelques semaines seulement —nallez pas croire que cest exagéré, mon expérience avec Elm me confirme que cest une réalité. Recruter des débutants disposés à se former est une manière de sassurer quils ont la culture adéquate : leur volonté dapprendre sera utile par la suite sur dautres sujets. Cela répond aussi à linquiétude de nombreux managers : comment recruter pour une techno de niche ?
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="gleamattirelattentiondesdéveloppeurs" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Gleam attire lattention des développeurs</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
À propos de langages fonctionnels, <a href="https://gleam.run/" style="color: #0066cc; text-decoration: underline">Gleam</a> est le <a href="https://survey.stackoverflow.co/2025/technology#admired-and-desired" style="color: #0066cc; text-decoration: underline">second langage le plus admiré</a> par les répondants au dernier sondage de Stack Overflow, juste derrière Rust. Rust truste le sommet de ce classement depuis plusieurs années, ce nest donc pas une surprise de le retrouver en tête. Gleam, qui est beaucoup plus récent, entre directement à la seconde place de ce sondage, et cest une vraie surprise pour moi. En effet, je ne mattendais pas à ce quautant de monde connaisse ce langage. Par contre, ses qualités font que je ne suis pas étonné quil soit aussi apprécié. Si vous voulez en savoir plus sur Gleam, je vous renvoie vers la <a href="https://www.youtube.com/watch?v=nTzNDM-dvRc" style="color: #0066cc; text-decoration: underline">conférence que jai donnée</a> 🇫🇷 lan dernier à ce sujet (27 minutes).
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="filtrerlescandidatsenremote" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Filtrer les candidats en remote</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Jose Zarazua utilise <a href="https://josezarazua.com/im-a-former-cto-here-is-the-15-sec-coding-test-i-used-to-instantly-filter-out-50-of-unqualified-applicants" style="color: #0066cc; text-decoration: underline">une astuce pour distinguer instantanément les candidats 🇬🇧</a> qui réfléchissent et les autres, lors dun test de recrutement à distance.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="desimagesdockerdurcies" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Des images docker durcies</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Des centaines d'<a href="https://hub.docker.com/hardened-images/catalog" style="color: #0066cc; text-decoration: underline">images Docker durcies</a>, auparavant payantes, sont dorénavant <a href="https://www.docker.com/blog/docker-hardened-images-for-every-developer" style="color: #0066cc; text-decoration: underline">mises à disposition gratuitement</a> 🇬🇧 par Docker Inc. Une image durcie est une image dont la sécurité est renforcée, en éliminant tout ce qui nest pas indispensable, ce qui réduit la surface dattaque.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Les images en question sont basées sur Debian et Alpine. Des <a href="https://blog.stephane-robert.info/docs/conteneurs/orchestrateurs/outils/helm/" style="color: #0066cc; text-decoration: underline">charts Helm</a> 🇫🇷 sont également disponibles aux mêmes conditions. Un chart Helm décrit le déploiement et la configuration dune application sous Kubernetes. Les charts, comme les images, sont durcis, et mis à jour en moins dune semaine quand une faille de sécurité est détectée. Ces images et charts sont sous llicense Apache 2, une license bien connue et très permissive, donc il ny a pas de mauvaise surprise à attendre de ce côté là.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="expositioninvolontairededonnéesavecsupabase" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Exposition involontaire de données avec Supabase</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Si vous utilisez <a href="https://supabase.com/" style="color: #0066cc; text-decoration: underline">Supabase</a> (Base de données SAAS), vous devriez vérifier que les données des utilisateurs ne sont pas accessibles librement. <a href="https://skilldeliver.com/your-supabase-is-public" style="color: #0066cc; text-decoration: underline">Supabase ne met pas suffisamment en garde contre ce risque 🇬🇧</a>.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Cet article me rappelle des problèmes similaires avec <a href="https://www.tenable.com/plugins/nessus/100634" style="color: #0066cc; text-decoration: underline">Redis</a> et <a href="https://satoricyber.com/mongodb-security/6-mongodb-authentication-features-you-must-know-about/#how-to-enable-authentication-in-mongodb" style="color: #0066cc; text-decoration: underline">MongoDB</a>, pour lesquels lauthentification nest pas activée par défaut.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="apprendreouréviserunetechnoenquelquesminutes" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Apprendre ou réviser une techno en quelques minutes</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
<del>Jean-Pierre Liégeois (jeune lecteur du Var)</del> <a href="https://www.linkedin.com/in/erwannedellec/" style="color: #0066cc; text-decoration: underline">Erwan</a> ma rappelé lexistence du site <a href="https://learnxinyminutes.com" style="color: #0066cc; text-decoration: underline">Learn X in Y minutes</a>.
Plutôt que de sen servir pour apprendre une nouvelle technologie, il lutilise pour se rafraichir la mémoire lorsquil doit utiliser un langage quil na pas pratiqué depuis longtemps.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
A cette occasion, jai découvert que les fiches proposées par ce site ne se limitent pas à des langages de programmation. Certaines concernent en effet des formats de données (JSON, XML, YAML, Cue…), des langages de balisage légers (Markdown, Restructured Text…), des shells (Bash, Fish), des frameworks et bibliothèques (Jquery, OpenCV…), des outils (Git, Docker), etc.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0;">
<h2 id="architecturebaséesurdescellules" style="margin: 0; padding: 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28px; font-weight: bold; color: #333333; line-height: 1.3">Architecture basée sur des cellules</h2>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Une cellule est un ensemble de microservices et des services dont ils dépendent (cache, base de données, stockage…). Ils forment un ensemble cohérent et (idéalement) autonome —cest-à-dire qui na pas ou peu de dépendances externes.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Dans une architecture basée sur des cellules (<em style="font-style: italic">cell based architecture</em>), chaque cellule est répliquée à lidentique autant de fois que nécessaire pour supporter la charge (scalabilité). Ces cellules sont déployées dans des régions différentes afin dassurer la résilience du service.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
<a href="https://bencane.com/about" style="color: #0066cc; text-decoration: underline">Benjamin Cane</a> explique dans <a href="https://itnext.io/cell-boundaries-defining-the-scope-of-a-cell-f76c5c4a52dc" style="color: #0066cc; text-decoration: underline">Cell Boundaries: Defining the Scope of a Cell in Cell-based Architecture</a> 🇬🇧 comment définir le contenu dune cellule.
</td>
</tr>
</table>
<hr>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Et voilà, cest tout pour cette semaine !
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Retrouvez les numéros précédents sur <a href="https://www.craftletter.fr" style="color: #0066cc; text-decoration: underline">le site web Craft Letter</a> 🇫🇷.
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Si vous navez pas reçu cette lettre par email, ou si elle vous a été transmise par un tiers, vous pouvez vous abonner sur le site.
</td>
</tr>
</table>
</body>
<!-- Signature -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0; height: 20px; line-height: 20px; font-size: 20px;">&nbsp;
</td>
</tr>
<tr>
<td style="padding: 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="padding: 0 15px 0 0; vertical-align: top;">
<img src="" alt=""
width="0"
height="0"
style="display: block; width: 0px; height: 0px; border: 0; border-radius: 50%;" />
</td>
<td style="padding: 0; vertical-align: middle;">
<div style="font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333; padding-top: 20px">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 20px 0; font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 16px; line-height: 1.6; color: #333333;">
Pascal Le Merrer
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f9f9f9; border-top: 1px solid #e5e5e5; border-radius: 0 0 4px 4px; padding: 30px 40px">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="padding: 20px 40px;">
<div style="font-size: 13px; line-height: 1.5; color: #666666; padding: 0">
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -27,32 +27,29 @@ Category: Newsletter
## Édito
Cette semaine encore, le contenu de cette newsletter est varié. Je vais en effet vous parler d'outillage, de langages fonctionnels, de recrutement, de sécurité, d'apprentissage ou de révision, et, pour finir, d'architecture basée sur des cellules. Concernant les langages fonctionnels, ceux que j'évoquerai sont mes deux langages favoris : ils associent la simplicité à un système de type qui élimine des classes entières d'erreurs. Le tout accompagné d'une expérience développeur particulièrment agréable.
Cette semaine encore, le contenu de cette newsletter est varié. Je vais en effet vous parler d'outillage, de langages fonctionnels, de recrutement, de sécurité, d'apprentissage ou de révision, et, pour finir, d'architecture basée sur des cellules. Concernant les langages fonctionnels, ceux que j'évoquerai sont mes deux langages favoris : ils associent la simplicité à un système de type qui élimine des classes entières d'erreurs. Le tout accompagné d'une expérience développeur particulièrement agréable.
Passez de bonnes fêtes de fin d'année, et bonne lecture !
## ty
Après [ruff](https://github.com/astral-sh/ruff) (linter et formateur) et [uv](https://github.com/astral-sh/uv) (un gestionnaire de dépendances), [Astral](https://astral.sh/) nous propose un troisième outil pour Python : [ty](https://docs.astral.sh/ty/), un vérificateur de types (_type checker_) et serveur de langage. Comme ruff et uv, ty est écrit en Rust, et bénéficie de performances bien meilleures que celles des outils concurrents. ruff et uv étant des réussites, ty mérite qu'on le regarde de près.
Après [Ruff](https://github.com/astral-sh/ruff) (linter et formateur) et [uv](https://github.com/astral-sh/uv) (un gestionnaire de dépendances), [Astral](https://astral.sh/) nous propose un troisième outil pour Python : [ty](https://docs.astral.sh/ty/), un vérificateur de types (_type checker_) et serveur de langage. Comme Ruff et uv, ty est écrit en Rust, et bénéficie de performances bien meilleures que celles des outils concurrents. Ruff et uv étant des réussites, ty mérite qu'on le regarde de près. Il est encore en version bêta, mais Astral s'est montré rapide à corriger les problèmes de ses produits jusqu'ici.
## Visualiser des logs avec HL
[hl](https://github.com/pamburus/hl?tab=readme-ov-file#performing-complex-queries%5D) est un outil de visualisation de log, écrit en Rust —c'est devenu un argument marketing. Il est capable de capable visualiser, filtrer et requêter rapidement des fichiers de plusieurs gigaoctets.
[hl](https://github.com/pamburus/hl?tab=readme-ov-file#performing-complex-queries%5D) est un outil de visualisation de logs, écrit en Rust —c'est devenu un argument marketing. Il est capable de capable visualiser, filtrer et requêter rapidement des fichiers de plusieurs gigaoctets.
## Une liste d'outils autour de Jujutsu
[Jujutsu](https://www.jj-vcs.dev/) —jj— est une alternative à Git, à la fois plus puissant et plus simple dutilisation. Il utilise Git comme back-end, ce qui veut dire que vous pouvez lutiliser de façon transparente sur un projet dont les autres contributeurs utilisent Git. [Awesome JJ](https://github.com/Necior/awesome-jj) liste des outils de lécosystème naissant autour de cet outil.
[Jujutsu](https://www.jj-vcs.dev/) —jj— est une alternative à Git, à la fois plus puissante et plus simple dutilisation. Il emploie Git comme back-end, ce qui veut dire que vous pouvez ladopter de façon transparente sur un projet dont les autres contributeurs utilisent Git. [Awesome JJ](https://github.com/Necior/awesome-jj) liste des outils de lécosystème naissant autour de cet outil.
Jutilise Jujutsu depuis 8 mois, sur des projets personnels pour linstant, et je le préfère largement à Git. Le nommage des commandes et options est bien plus clair, ce qui fait quon les retient ou retrouve plus facilement. Le workflow est plus simple —car il y a moins de concepts. Malgré cela, il est plus puissant que Git. Par exemple, il ne requiert de résoudre qu'une seule fois les conflits lors d'un rebase, là où Git peut vous demander de les résoudre à plusieurs reprises.
Jutilise Jujutsu depuis 8 mois, sur des projets personnels pour linstant, et je le préfère largement à Git. Le nommage des commandes et options est bien plus clair, ce qui fait quon les retient ou retrouve plus facilement. Le workflow est plus simple —car il y a moins de concepts. Malgré cela, il est plus puissant que Git. Par exemple, il ne requiert de résoudre qu'une seule fois les conflits lors d'un rebase, là où Git peut vous demander de les résoudre à plusieurs reprises. Ou encore, le fait d'avoir des conflits n'empêche pas de reporter leur résolution à plus tard, et de travailler sur une autre "branche" —j'emploie des guillemets, car ce concept n'existe pas vraiment avec JJ.
## Comment trouver des développeurs pour un langage fonctionnel
Dans sa [keynote lors des Scala Days 2025 🇬🇧](https://www.youtube.com/watch?v=9OtN4iiFBsQ), Evan Czaplicki explique comment [Elm](https://www.youtube.com/watch?v=9OtN4iiFBsQ), le langage qu'il a créé il y a bientôt 14 ans, peut servir de porte d'entrée pour des langages fonctionnels plus complexes, comme Haskell ou Scala. Elm est volontairement simple : les concepts y sont limités au strict minimum. Malgré une syntaxe qui peut intimider au départ, l'apprentissage est rapide, notamment parce que l'expérience développeur est vraiment agréable. Vous trouvez sympathiques les messages d'erreur de Rust ? Ils sont inspirés de ceux de Elm.
Dans sa [keynote lors des Scala Days 2025 🇬🇧](https://www.youtube.com/watch?v=9OtN4iiFBsQ), Evan Czaplicki explique comment [Elm](https://elm-lang.org), le langage qu'il a créé il y a bientôt 14 ans, peut servir de porte d'entrée pour des langages fonctionnels plus complexes, comme Haskell ou Scala. Elm est volontairement simple : les concepts y sont limités au strict minimum. Malgré une syntaxe qui peut intimider au départ, l'apprentissage est rapide, notamment parce que l'expérience développeur est vraiment soignée. Vous trouvez sympathiques les messages d'erreur de Rust ? Ils sont inspirés de ceux de Elm.
Evan partage dans cette keynote les stratégies de recrutement d'entreprises florissantes qui utilisent Elm. Certaines recrutent les développeurs ou développeuses de modules populaires. D'autres embauchent des débutants et les forment. Les qualités du langage font que ces juniors sont opérationnels et productifs en quelques semaines seulement —n'allez pas croire que c'est exagéré, mon expérience avec Elm me confirme que c'est une réalité. Recruter des débutants disposés à se former est une manière de s'assurer qu'ils ont la culture adéquate : leur volonté d'apprendre sera utile par la suite sur d'autres sujets. Cela répond aussi à l'inquiétude de nombreux managers : comment recruter pour une techno de niche ?
@ -68,9 +65,9 @@ Jose Zarazua utilise [une astuce pour distinguer instantanément les candidats
Des centaines d'[images Docker durcies](https://hub.docker.com/hardened-images/catalog), auparavant payantes, sont dorénavant [mises à disposition gratuitement](https://www.docker.com/blog/docker-hardened-images-for-every-developer) 🇬🇧 par Docker Inc. Une image durcie est une image dont la sécurité est renforcée, en éliminant tout ce qui nest pas indispensable, ce qui réduit la surface dattaque.
Des centaines d[images Docker durcies](https://hub.docker.com/hardened-images/catalog), auparavant payantes, sont dorénavant [mises à disposition gratuitement](https://www.docker.com/blog/docker-hardened-images-for-every-developer) 🇬🇧 par Docker Inc. Une image durcie est une image dont la sécurité est renforcée, en éliminant tout ce qui nest pas indispensable, ce qui réduit la surface dattaque.
Les images en question sont basées sur Debian et Alpine. Des [charts Helm](https://blog.stephane-robert.info/docs/conteneurs/orchestrateurs/outils/helm/) 🇫🇷 sont également disponibles aux mêmes conditions. Un chart Helm décrit le déploiement et la configuration dune application sous Kubernetes. Les charts, comme les images, sont durcis, et mis à jour en moins dune semaine quand une faille de sécurité est détectée. Ces images et charts sont sous llicense Apache 2, une license bien connue et très permissive, donc il n'y a pas de mauvaise surprise à attendre de ce côté là.
Les images en question sont basées sur Debian et Alpine. Des [charts Helm](https://blog.stephane-robert.info/docs/conteneurs/orchestrateurs/outils/helm/) 🇫🇷 sont également disponibles aux mêmes conditions. Un chart Helm décrit le déploiement et la configuration dune application sous Kubernetes. Les charts, comme les images, sont durcis, et mis à jour en moins dune semaine quand une faille de sécurité est détectée. Ces images et charts sont sous licence Apache 2, une licence bien connue et très permissive, donc il n'y a pas de mauvaise surprise à attendre de ce côté-là.
## Exposition involontaire de données avec Supabase

View file

@ -0,0 +1,94 @@
Title: Lettre n°4 - 29 Décembre 2025
Date: 2025-12-29 09:00
Category: Newsletter
}
# Craft Letter n°4
## Édito
Cette semaine encore, le contenu de cette newsletter est varié. Je vais en effet vous parler d'outillage, de langages fonctionnels, de recrutement, de sécurité, d'apprentissage ou de révision, et, pour finir, d'architecture basée sur des cellules. Concernant les langages fonctionnels, ceux que j'évoquerai sont mes deux langages favoris : ils associent la simplicité à un système de type qui élimine des classes entières d'erreurs. Le tout accompagné d'une expérience développeur particulièrment agréable.
Passez de bonnes fêtes de fin d'année, et bonne lecture !
## ty
Après ruff (https://github.com/astral-sh/ruff) (linter et formateur) et uv (https://github.com/astral-sh/uv) (un gestionnaire de dépendances), Astral (https://astral.sh/) nous propose un troisième outil pour Python : ty (https://docs.astral.sh/ty/), un vérificateur de types (_type checker_) et serveur de langage. Comme ruff et uv, ty est écrit en Rust, et bénéficie de performances bien meilleures que celles des outils concurrents. ruff et uv étant des réussites, ty mérite qu'on le regarde de près.
## Visualiser des logs avec HL
hl (https://github.com/pamburus/hl?tab=readme-ov-file#performing-complex-queries%5D) est un outil de visualisation de log, écrit en Rust —c'est devenu un argument marketing. Il est capable de capable visualiser, filtrer et requêter rapidement des fichiers de plusieurs gigaoctets.
## Une liste d'outils autour de Jujutsu
Jujutsu (https://www.jj-vcs.dev/) —jj— est une alternative à Git, à la fois plus puissant et plus simple dutilisation. Il utilise Git comme back-end, ce qui veut dire que vous pouvez lutiliser de façon transparente sur un projet dont les autres contributeurs utilisent Git. Awesome JJ (https://github.com/Necior/awesome-jj) liste des outils de lécosystème naissant autour de cet outil.
Jutilise Jujutsu depuis 8 mois, sur des projets personnels pour linstant, et je le préfère largement à Git. Le nommage des commandes et options est bien plus clair, ce qui fait quon les retient ou retrouve plus facilement. Le workflow est plus simple —car il y a moins de concepts. Malgré cela, il est plus puissant que Git. Par exemple, il ne requiert de résoudre qu'une seule fois les conflits lors d'un rebase, là où Git peut vous demander de les résoudre à plusieurs reprises.
## Comment trouver des développeurs pour un langage fonctionnel
Dans sa keynote lors des Scala Days 2025 🇬🇧 (https://www.youtube.com/watch?v=9OtN4iiFBsQ), Evan Czaplicki explique comment Elm (https://www.youtube.com/watch?v=9OtN4iiFBsQ), le langage qu'il a créé il y a bientôt 14 ans, peut servir de porte d'entrée pour des langages fonctionnels plus complexes, comme Haskell ou Scala. Elm est volontairement simple : les concepts y sont limités au strict minimum. Malgré une syntaxe qui peut intimider au départ, l'apprentissage est rapide, notamment parce que l'expérience développeur est vraiment agréable. Vous trouvez sympathiques les messages d'erreur de Rust ? Ils sont inspirés de ceux de Elm.
Evan partage dans cette keynote les stratégies de recrutement d'entreprises florissantes qui utilisent Elm. Certaines recrutent les développeurs ou développeuses de modules populaires. D'autres embauchent des débutants et les forment. Les qualités du langage font que ces juniors sont opérationnels et productifs en quelques semaines seulement —n'allez pas croire que c'est exagéré, mon expérience avec Elm me confirme que c'est une réalité. Recruter des débutants disposés à se former est une manière de s'assurer qu'ils ont la culture adéquate : leur volonté d'apprendre sera utile par la suite sur d'autres sujets. Cela répond aussi à l'inquiétude de nombreux managers : comment recruter pour une techno de niche ?
## Gleam attire l'attention des développeurs
À propos de langages fonctionnels, Gleam (https://gleam.run/) est le second langage le plus admiré (https://survey.stackoverflow.co/2025/technology#admired-and-desired) par les répondants au dernier sondage de Stack Overflow, juste derrière Rust. Rust truste le sommet de ce classement depuis plusieurs années, ce n'est donc pas une surprise de le retrouver en tête. Gleam, qui est beaucoup plus récent, entre directement à la seconde place de ce sondage, et c'est une vraie surprise pour moi. En effet, je ne m'attendais pas à ce qu'autant de monde connaisse ce langage. Par contre, ses qualités font que je ne suis pas étonné qu'il soit aussi apprécié. Si vous voulez en savoir plus sur Gleam, je vous renvoie vers la conférence que j'ai donnée (https://www.youtube.com/watch?v=nTzNDM-dvRc) 🇫🇷 l'an dernier à ce sujet (27 minutes).
## Filtrer les candidats en remote
Jose Zarazua utilise une astuce pour distinguer instantanément les candidats 🇬🇧 (https://josezarazua.com/im-a-former-cto-here-is-the-15-sec-coding-test-i-used-to-instantly-filter-out-50-of-unqualified-applicants) qui réfléchissent et les autres, lors d'un test de recrutement à distance.
## Des images docker durcies
Des centaines d'images Docker durcies (https://hub.docker.com/hardened-images/catalog), auparavant payantes, sont dorénavant mises à disposition gratuitement (https://www.docker.com/blog/docker-hardened-images-for-every-developer) 🇬🇧 par Docker Inc. Une image durcie est une image dont la sécurité est renforcée, en éliminant tout ce qui nest pas indispensable, ce qui réduit la surface dattaque.
Les images en question sont basées sur Debian et Alpine. Des charts Helm (https://blog.stephane-robert.info/docs/conteneurs/orchestrateurs/outils/helm/) 🇫🇷 sont également disponibles aux mêmes conditions. Un chart Helm décrit le déploiement et la configuration dune application sous Kubernetes. Les charts, comme les images, sont durcis, et mis à jour en moins dune semaine quand une faille de sécurité est détectée. Ces images et charts sont sous llicense Apache 2, une license bien connue et très permissive, donc il n'y a pas de mauvaise surprise à attendre de ce côté là.
## Exposition involontaire de données avec Supabase
Si vous utilisez Supabase (https://supabase.com/) (Base de données SAAS), vous devriez vérifier que les données des utilisateurs ne sont pas accessibles librement. Supabase ne met pas suffisamment en garde contre ce risque 🇬🇧 (https://skilldeliver.com/your-supabase-is-public).
Cet article me rappelle des problèmes similaires avec Redis (https://www.tenable.com/plugins/nessus/100634) et MongoDB (https://satoricyber.com/mongodb-security/6-mongodb-authentication-features-you-must-know-about/#how-to-enable-authentication-in-mongodb), pour lesquels l'authentification n'est pas activée par défaut.
## Apprendre ou réviser une techno en quelques minutes
Jean-Pierre Liégeois (jeune lecteur du Var) Erwan (https://www.linkedin.com/in/erwannedellec/) m'a rappelé l'existence du site Learn X in Y minutes (https://learnxinyminutes.com).
Plutôt que de s'en servir pour apprendre une nouvelle technologie, il l'utilise pour se rafraichir la mémoire lorsqu'il doit utiliser un langage qu'il n'a pas pratiqué depuis longtemps.
A cette occasion, j'ai découvert que les fiches proposées par ce site ne se limitent pas à des langages de programmation. Certaines concernent en effet des formats de données (JSON, XML, YAML, Cue...), des langages de balisage légers (Markdown, Restructured Text...), des shells (Bash, Fish), des frameworks et bibliothèques (Jquery, OpenCV...), des outils (Git, Docker), etc.
## Architecture basée sur des cellules
Une cellule est un ensemble de microservices et des services dont ils dépendent (cache, base de données, stockage...). Ils forment un ensemble cohérent et (idéalement) autonome —c'est-à-dire qui n'a pas ou peu de dépendances externes.
Dans une architecture basée sur des cellules (_cell based architecture_), chaque cellule est répliquée à l'identique autant de fois que nécessaire pour supporter la charge (scalabilité). Ces cellules sont déployées dans des régions différentes afin d'assurer la résilience du service.
Benjamin Cane (https://bencane.com/about) explique dans Cell Boundaries: Defining the Scope of a Cell in Cell-based Architecture (https://itnext.io/cell-boundaries-defining-the-scope-of-a-cell-f76c5c4a52dc) 🇬🇧 comment définir le contenu d'une cellule.
---
Et voilà, c'est tout pour cette semaine !
Retrouvez les numéros précédents sur le site web Craft Letter (https://www.craftletter.fr) 🇫🇷.
Si vous n'avez pas reçu cette lettre par email, ou si elle vous a été transmise par un tiers, vous pouvez vous abonner sur le site.
Pascal Le Merrer
---
Web Version: [webversion]
Unsubscribe: [unsubscribe]

View file

@ -1,12 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": ["python313Packages.pip@latest"],
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [
"python313Packages.pip@latest",
"ruby@latest",
"hunspellDicts.fr-moderne@latest",
"hunspell@latest"
],
"shell": {
"init_hook": [
". venv/bin/activate.fish" ],
". venv/bin/activate.fish"
],
"scripts": {
"preview": [
"pelican -r -l"
"pelican -r -l"
]
}
}

View file

@ -5,6 +5,154 @@
"last_modified": "2025-11-26T06:22:50Z",
"resolved": "github:NixOS/nixpkgs/bb813de6d2241bcb1b5af2d3059f560c66329967?lastModified=1764138170&narHash=sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI%2FeWmf3tk%3D"
},
"hunspell@latest": {
"last_modified": "2025-12-15T04:22:15Z",
"resolved": "github:NixOS/nixpkgs/09b8fda8959d761445f12b55f380d90375a1d6bb#hunspell",
"source": "devbox-search",
"version": "1.7.2",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/63hvq606dc72ngk78l19r8xrz34x36a6-hunspell-1.7.2-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/n9bdbbyingd6lr1iw5bqzzb7cd6qp8dp-hunspell-1.7.2-man",
"default": true
},
{
"name": "dev",
"path": "/nix/store/3rywdqcwnmdkawjksafv0vihs4g8gb3m-hunspell-1.7.2-dev"
},
{
"name": "out",
"path": "/nix/store/yjgzc5ssvq9hsl81ja17l71lbwpjgk7m-hunspell-1.7.2"
}
],
"store_path": "/nix/store/63hvq606dc72ngk78l19r8xrz34x36a6-hunspell-1.7.2-bin"
},
"aarch64-linux": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/dfrmd1klkvfl0fcrc7bga9yy1nzhlh5s-hunspell-1.7.2-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/xx2255x8j2af7qg00pp8hxxamqwn2jgw-hunspell-1.7.2-man",
"default": true
},
{
"name": "dev",
"path": "/nix/store/lrm70njc6yfdgq57i6pxh81q59l3wlkx-hunspell-1.7.2-dev"
},
{
"name": "out",
"path": "/nix/store/1dd6ldclg2xfahccc5lpmhkxl0l80y8x-hunspell-1.7.2"
}
],
"store_path": "/nix/store/dfrmd1klkvfl0fcrc7bga9yy1nzhlh5s-hunspell-1.7.2-bin"
},
"x86_64-darwin": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/qac3jcll1h3w9bnd561plk9i6hbgxm3c-hunspell-1.7.2-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/1g97nnmxpx7abdgr96xkrbgv36ynbl3s-hunspell-1.7.2-man",
"default": true
},
{
"name": "dev",
"path": "/nix/store/4xvzsnvz51mv2wmhcc20fzyn2n1pwz1f-hunspell-1.7.2-dev"
},
{
"name": "out",
"path": "/nix/store/pw4ypg20vdif1c2kp9q115dyi0fz3imq-hunspell-1.7.2"
}
],
"store_path": "/nix/store/qac3jcll1h3w9bnd561plk9i6hbgxm3c-hunspell-1.7.2-bin"
},
"x86_64-linux": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/kj5r28bziid0bj50qx3sqkmb30vr4177-hunspell-1.7.2-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/pawjaviqm6zkjdlmpm20zw9npy1840g7-hunspell-1.7.2-man",
"default": true
},
{
"name": "out",
"path": "/nix/store/9vvyjxq3ya12bpsmq9dyda59w94s8j71-hunspell-1.7.2"
},
{
"name": "dev",
"path": "/nix/store/c2gyymxki26whjb9wwmi5cbzpxgch13j-hunspell-1.7.2-dev"
}
],
"store_path": "/nix/store/kj5r28bziid0bj50qx3sqkmb30vr4177-hunspell-1.7.2-bin"
}
}
},
"hunspellDicts.fr-moderne@latest": {
"last_modified": "2025-11-23T21:50:36Z",
"resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#hunspellDicts.fr-moderne",
"source": "devbox-search",
"version": "5.3",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/zri7spxcvfrd959k7j91py1fbdx8dcyb-hunspell-dict-fr-moderne-dicollecte-5.3",
"default": true
}
],
"store_path": "/nix/store/zri7spxcvfrd959k7j91py1fbdx8dcyb-hunspell-dict-fr-moderne-dicollecte-5.3"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/0x0g0mqb1wcp99yfq4sg913i9h6r3k0n-hunspell-dict-fr-moderne-dicollecte-5.3",
"default": true
}
],
"store_path": "/nix/store/0x0g0mqb1wcp99yfq4sg913i9h6r3k0n-hunspell-dict-fr-moderne-dicollecte-5.3"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/mk53yqf654asxbvv7897df1gw2b8g7c8-hunspell-dict-fr-moderne-dicollecte-5.3",
"default": true
}
],
"store_path": "/nix/store/mk53yqf654asxbvv7897df1gw2b8g7c8-hunspell-dict-fr-moderne-dicollecte-5.3"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/8sih1iaql4h01ihs23f8yfy2k9yzyyjg-hunspell-dict-fr-moderne-dicollecte-5.3",
"default": true
}
],
"store_path": "/nix/store/8sih1iaql4h01ihs23f8yfy2k9yzyyjg-hunspell-dict-fr-moderne-dicollecte-5.3"
}
}
},
"python313Packages.pip@latest": {
"last_modified": "2025-11-23T21:50:36Z",
"resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#python313Packages.pip",
@ -88,6 +236,71 @@
"store_path": "/nix/store/6k8ghavfzfpcgs6angp98gy71xh2mrip-python3.13-pip-25.0.1"
}
}
},
"ruby@latest": {
"last_modified": "2025-12-19T06:18:24Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/7d853e518814cca2a657b72eeba67ae20ebf7059#ruby_3_4",
"source": "devbox-search",
"version": "3.4.8",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/l7y8sk5wjy059vb4p2zqimhfg81y7k56-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/ml55wc4rwakndj55iw39shbc0pxvpkn2-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/l7y8sk5wjy059vb4p2zqimhfg81y7k56-ruby-3.4.8"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/1rack9mx33cqdm8z908yz1a80q153hb1-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/9njsxja1ps0vq25xkaxyb10v01q0lnjd-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/1rack9mx33cqdm8z908yz1a80q153hb1-ruby-3.4.8"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/iwgzafzagsjjdbj6h1pj9xdxxqr8akrl-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/zarwsrj4k6a250v3hd2cvmch5nqnap2q-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/iwgzafzagsjjdbj6h1pj9xdxxqr8akrl-ruby-3.4.8"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/gphbrsb5wd49qwg9bxby194wwm47awks-ruby-3.4.8",
"default": true
},
{
"name": "devdoc",
"path": "/nix/store/hhzf48bh7drk9a46qzc7d0cwrfisr8yl-ruby-3.4.8-devdoc"
}
],
"store_path": "/nix/store/gphbrsb5wd49qwg9bxby194wwm47awks-ruby-3.4.8"
}
}
}
}
}

View file

@ -0,0 +1,55 @@
# Email Generator Configuration
# Copy this file to config.yml and fill in your values
# Sendy API Configuration
sendy:
api_url: 'https://your-domain.com/api/campaigns/create.php'
api_key: 'your-api-key-here'
brand_id: '1'
list_ids: 'your-list-id-here'
# Email Settings
email:
from_name: 'Your Name'
from_email: 'your-email@example.com'
reply_to: 'your-email@example.com'
# Campaign Settings
campaign:
track_opens: '1'
track_clicks: '1'
default_timezone: 'America/Chicago'
# Template Settings
template:
header_image_url: 'https://your-domain.com/img/email/header.jpg'
header_image_alt: 'Email Header'
header_image_width: 600
header_image_height: 185
signature_image_url: 'https://your-domain.com/img/signature.png'
signature_image_alt: 'Signature'
signature_image_width: 98
signature_image_height: 98
signature_text: '-Your Name'
# Optional: Primary footer appears after signature, before unsubscribe section
# Can contain Markdown or HTML (e.g., product links, promotional content)
# primary_footer: |
# [![](/mas.jpg)](example.com "Purchase on the Mac App Store") | [![](/paddle.jpg)](example.com "Purchase directly")
# Footer text - can contain Markdown and will preserve <webversion> and <unsubscribe> tags
# Sendy will replace these tags when sending the email
# footer_text: |
# Copyright © 2025 Your Name<br>
# 123 Main St<br>
# City, State ZIP
#
# <webversion>View on the web</webversion> | <unsubscribe>Unsubscribe</unsubscribe>
# File Paths
paths:
template_file: 'email-template.html'
styles_file: 'styles.css'
# Markdown Processor
markdown:
processor: 'apex' # or 'pandoc', 'kramdown', etc.

View file

@ -0,0 +1,55 @@
# Email Generator Configuration
# Copy this file to config.yml and fill in your values
# Sendy API Configuration
sendy:
api_url: ''
api_key: ''
brand_id: ''
list_ids: ''
# Email Settings
email:
from_name: 'Pascal Le Merrer'
from_email: 'contact@craftletter.fr'
reply_to: 'contact@craftletter.fr'
# Campaign Settings
campaign:
track_opens: '0'
track_clicks: '0'
default_timezone: 'Europe/Paris'
# Template Settings
template:
header_image_url: 'https://www.craftletter.fr/images/craftletter.svg'
header_image_alt: 'Logo Craft Letter'
header_image_width: 600
header_image_height: 185
signature_image_url: ''
signature_image_alt: ''
signature_image_width: 0
signature_image_height: 0
signature_text: ''
# Optional: Primary footer appears after signature, before unsubscribe section
# Can contain Markdown or HTML (e.g., product links, promotional content)
# primary_footer: |
# [![](/mas.jpg)](example.com "Purchase on the Mac App Store") | [![](/paddle.jpg)](example.com "Purchase directly")
# Footer text - can contain Markdown and will preserve <webversion> and <unsubscribe> tags
# Sendy will replace these tags when sending the email
# footer_text: |
# Copyright © 2025 Your Name<br>
# 123 Main St<br>
# City, State ZIP
#
# <webversion>View on the web</webversion> | <unsubscribe>Unsubscribe</unsubscribe>
# File Paths
paths:
template_file: 'email-template.html'
styles_file: 'styles.css'
# Markdown Processor
markdown:
processor: 'multimarkdown' # or 'apex', pandoc', 'kramdown', etc.

View file

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{TITLE}}</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: {{FONT_FAMILY}} !important;}
</style>
<![endif]-->
</head>
<body style="{{BODY_STYLE}}">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="{{WRAPPER_STYLE}}">
<tr>
<td align="center" style="padding: 20px 0;">
<!-- Main Container -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600"
style="{{CONTENT_WRAPPER_STYLE}}">
<!-- Header Image -->
<tr>
<td align="center" style="padding: 0;">
<img alt="{{HEADER_IMAGE_ALT}}" src="{{HEADER_IMAGE_URL}}" width="{{HEADER_IMAGE_WIDTH}}"
height="{{HEADER_IMAGE_HEIGHT}}"
style="display: block; width: {{HEADER_IMAGE_WIDTH}}px; height: {{HEADER_IMAGE_HEIGHT}}px; border: 0; border-radius: 4px 4px 0 0;" />
</td>
</tr>
<!-- Content Area -->
<tr>
<td style="padding: 40px 40px 30px 40px;">
{{CONTENT}}
<!-- Signature -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0; height: 20px; line-height: 20px; font-size: 20px; ">&nbsp;
</td>
</tr>
<tr>
<td style="padding: 0;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="padding: 0; vertical-align: middle;">
<div style="{{SIGNATURE_STYLE}}">
{{SIGNATURE_TEXT}}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{PRIMARY_FOOTER}}
</td>
</tr>
<!-- Footer -->
<tr>
<td style="{{FOOTER_STYLE}}">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody style="font-size: 13px; line-height: 1.5; color: #666666; padding: 0">
<tr>
<td align="center" style="padding: 20px 40px;">
<a href="https://www.craftletter.fr">Craft Letter</a> © 2025 par <a href="https://www.linkedin.com/in/pascal-le-merrer/">Pascal Le Merrer</a><br>est sous licence <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/">CC BY-NC-ND 4.0</a></p><div><img alt="" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" style="max-width:1em;max-height:1em;margin-left:.2em"><img alt="" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" style="max-width:1em;max-height:1em;margin-left:.2em"><img alt="" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" style="max-width:1em;max-height:1em;margin-left:.2em"><img alt="" src="https://mirrors.creativecommons.org/presskit/icons/nd.svg" style="max-width:1em;max-height:1em;margin-left:.2em">
</td>
</tr>
<tr>
<td align="center">
<a href="{{ unsubscribe_link }}" style="color: #000000; font-family: Arial, Helvetica, sans-serif; font-size: 12px; text-decoration: underline;">Se désabonner</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

240
mdtosendy_config/styles.css Normal file
View file

@ -0,0 +1,240 @@
/* Email Styles Configuration
* These styles will be converted to inline styles for email compatibility
*/
/* Body and wrapper styles */
body {
margin: 0;
padding: 0;
/* background: linear-gradient(to bottom, #0a1a26, #0f3d5f); */
background-color: #ffffff;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
.content-wrapper {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: justify;
}
/* Typography */
h1 {
text-align: center;
margin: 2rem 0 2rem 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 28px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h1 td {
padding: 0 0 20px 0;
}
h2 {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 24px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h2 td {
padding: 0 0 20px 0;
}
h3 {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 20px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h3 td {
padding: 0 0 15px 0;
}
p {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
p td {
padding: 0 0 20px 0;
text-align: justify;
}
/* Links */
a {
color: #0066cc;
text-decoration: underline;
}
/* Buttons */
a.button {
display: inline-block;
padding: 16px 40px;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 18px;
font-weight: bold;
color: #ffffff !important;
text-decoration: none !important;
border-radius: 16px;
background: linear-gradient(to right, #ff9f5a, #ff6b1a);
background-color: #ff6b1a;
border: 1px solid #e85a00;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
a.button span {
color: #ffffff;
font-weight: bold;
text-decoration: none;
}
a.button td {
background: linear-gradient(to right, #ff9f5a, #ff6b1a);
background-color: #ff6b1a;
border-radius: 16px;
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
a.button-wrapper {
padding: 0 0 20px 0;
}
a.button-fallback {
padding: 0 0 25px 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #666666;
}
a.button-fallback a {
color: #0066cc;
text-decoration: underline;
font-size: 13px;
}
/* Images */
img {
max-width: 100%;
height: auto;
border: 0;
}
img.full-width {
display: block;
margin: 1em 0;
}
img.float-left {
max-width: 30%;
float: left;
margin: 0 1em 1em 0;
}
img.float-right {
max-width: 30%;
float: right;
margin: 0 0 1em 1em;
}
/* Lists */
ul,
ol {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
ul td,
ol td {
padding: 0 0 10px 0;
}
li {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
li td {
padding: 0 0 8px 0;
}
li.bullet {
width: 20px;
vertical-align: top;
color: #333333;
}
li.content {
padding: 0 0 8px 0;
vertical-align: top;
}
/* Text formatting */
strong,
b {
font-weight: bold;
}
em,
i {
font-style: italic;
}
/* Footer */
.footer {
background-color: #f9f9f9;
border-top: 1px solid #e5e5e5;
border-radius: 0 0 4px 4px;
padding: 30px 40px;
}
.footer p {
font-size: 13px;
line-height: 1.5;
color: #666666;
padding: 0;
}
.footer p:first-child {
padding: 0 0 10px 0;
}
/* Signature */
.signature {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
padding-top: 20px; /* This padding-top is applied to the signature table cell */
}
.signature img {
width: 98px;
height: 98px;
border-radius: 50%;
border: 0;
display: block;
}

View file

@ -0,0 +1,240 @@
/* Email Styles Configuration
* These styles will be converted to inline styles for email compatibility
*/
/* Body and wrapper styles */
body {
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #0a1a26, #0f3d5f);
background-color: #0a1a26;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
.wrapper {
background: linear-gradient(to bottom, #0a1a26, #0f3d5f);
background-color: #0a1a26;
}
.content-wrapper {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Typography */
h1 {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 28px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h1 td {
padding: 0 0 20px 0;
}
h2 {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 28px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h2 td {
padding: 0 0 20px 0;
}
h3 {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 20px;
font-weight: bold;
color: #333333;
line-height: 1.3;
}
h3 td {
padding: 0 0 15px 0;
}
p {
margin: 0;
padding: 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
p td {
padding: 0 0 20px 0;
}
/* Links */
a {
color: #0066cc;
text-decoration: underline;
}
/* Buttons */
a.button {
display: inline-block;
padding: 16px 40px;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 18px;
font-weight: bold;
color: #ffffff !important;
text-decoration: none !important;
border-radius: 16px;
background: linear-gradient(to right, #ff9f5a, #ff6b1a);
background-color: #ff6b1a;
border: 1px solid #e85a00;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
a.button span {
color: #ffffff;
font-weight: bold;
text-decoration: none;
}
a.button td {
background: linear-gradient(to right, #ff9f5a, #ff6b1a);
background-color: #ff6b1a;
border-radius: 16px;
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
a.button-wrapper {
padding: 0 0 20px 0;
}
a.button-fallback {
padding: 0 0 25px 0;
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #666666;
}
a.button-fallback a {
color: #0066cc;
text-decoration: underline;
font-size: 13px;
}
/* Images */
img {
max-width: 100%;
height: auto;
border: 0;
}
img.full-width {
display: block;
margin: 1em 0;
}
img.float-left {
max-width: 30%;
float: left;
margin: 0 1em 1em 0;
}
img.float-right {
max-width: 30%;
float: right;
margin: 0 0 1em 1em;
}
/* Lists */
ul,
ol {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
ul td,
ol td {
padding: 0 0 10px 0;
}
li {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
}
li td {
padding: 0 0 8px 0;
}
li.bullet {
width: 20px;
vertical-align: top;
color: #333333;
}
li.content {
padding: 0 0 8px 0;
vertical-align: top;
}
/* Text formatting */
strong,
b {
font-weight: bold;
}
em,
i {
font-style: italic;
}
/* Footer */
.footer {
background-color: #f9f9f9;
border-top: 1px solid #e5e5e5;
border-radius: 0 0 4px 4px;
padding: 30px 40px;
}
.footer p {
font-size: 13px;
line-height: 1.5;
color: #666666;
padding: 0;
}
.footer p:first-child {
padding: 0 0 10px 0;
}
/* Signature */
.signature {
font-family: Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333333;
padding-top: 20px; /* This padding-top is applied to the signature table cell */
}
.signature img {
width: 98px;
height: 98px;
border-radius: 50%;
border: 0;
display: block;
}

37
prepare_email.py Executable file
View file

@ -0,0 +1,37 @@
#!python
# Copies the given input file, removing the parts specific to the Web,
# so it can be converted to an email
import subprocess
import argparse
import re
from pathlib import Path
MAIL_GENERATOR = "/opt/homebrew/bin/mdtosendy"
parser = argparse.ArgumentParser()
parser.add_argument("source")
args = parser.parse_args()
root = Path.cwd()
print(f"Root: {root}")
source = Path(root / args.source)
if not source.is_file():
print(f"ERROR: file not found {source}")
exit(1)
print(f"Processing {source}")
input = source.read_text()
regex = re.compile(r"Title.*svg\">", re.S)
output = re.sub(regex, "", input)
destination = Path.cwd() / "mail" / source.name
print(f"Writing {destination}")
destination.write_text(output)
subprocess.run([MAIL_GENERATOR, "--preview", str(destination)])

249
setup_mdtosendy.sh Executable file
View file

@ -0,0 +1,249 @@
#!/bin/bash
# Bootstrap installer for mdtosendy
# Can be run via: curl -s https://github.com/ttscoff/mdtosendy/raw/main/bootstrap.sh | bash
# Or saved locally and run with: ./bootstrap.sh --update
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored messages
info() {
echo -e "${BLUE}${NC} $1"
}
success() {
echo -e "${GREEN}${NC} $1"
}
warning() {
echo -e "${YELLOW}${NC} $1"
}
error() {
echo -e "${RED}${NC} $1"
}
# Function to show help
show_help() {
echo "Bootstrap installer for mdtosendy"
echo
echo "Usage:"
echo " curl -s https://github.com/ttscoff/mdtosendy/raw/main/bootstrap.sh | bash"
echo " ./bootstrap.sh [OPTIONS]"
echo
echo "Options:"
echo " --update, -u Update existing installation by running git pull"
echo " --help, -h Show this help message"
echo
echo "Examples:"
echo " # Install mdtosendy"
echo " curl -s https://github.com/ttscoff/mdtosendy/raw/main/bootstrap.sh | bash"
echo
echo " # Update existing installation"
echo " ./bootstrap.sh --update"
echo
echo "For more information, visit: https://github.com/ttscoff/mdtosendy"
}
# Function to find real path of a file (following symlinks)
get_real_path() {
local file="$1"
if command -v realpath >/dev/null 2>&1; then
realpath "$file"
elif command -v readlink >/dev/null 2>&1; then
readlink -f "$file" 2>/dev/null || {
# Fallback for systems without readlink -f
local dir=$(dirname "$file")
local base=$(basename "$file")
cd "$dir" 2>/dev/null || return 1
local target=$(readlink "$base" 2>/dev/null)
if [ -n "$target" ]; then
if [ "${target:0:1}" = "/" ]; then
get_real_path "$target"
else
get_real_path "$dir/$target"
fi
else
echo "$(pwd)/$base"
fi
}
else
# Last resort: try to resolve manually
local dir=$(dirname "$file")
local base=$(basename "$file")
cd "$dir" 2>/dev/null || return 1
echo "$(pwd)/$base"
fi
}
# Function to update existing installation
update_installation() {
info "Updating mdtosendy installation..."
echo
# Try to find mdtosendy in PATH
if ! command -v mdtosendy >/dev/null 2>&1; then
error "mdtosendy not found in PATH"
echo "Please ensure mdtosendy is installed and in your PATH"
exit 1
fi
# Get the real path of the mdtosendy script
MDTOSENDY_PATH=$(command -v mdtosendy)
REAL_PATH=$(get_real_path "$MDTOSENDY_PATH")
if [ ! -f "$REAL_PATH" ]; then
error "Could not resolve real path of mdtosendy: $MDTOSENDY_PATH"
exit 1
fi
# Get the directory containing the script
REPO_DIR=$(dirname "$REAL_PATH")
info "Found installation at: $REPO_DIR"
# Check if it's a git repository
if [ ! -d "$REPO_DIR/.git" ]; then
error "Installation directory is not a git repository: $REPO_DIR"
echo "Cannot update. Please reinstall using the bootstrap script."
exit 1
fi
# Change to repository directory and pull
info "Updating repository..."
cd "$REPO_DIR" || exit 1
if ! git pull; then
error "Failed to update repository"
exit 1
fi
success "Repository updated successfully"
# Re-run install.sh to ensure symlink is up to date
if [ -f "$REPO_DIR/install.sh" ]; then
info "Re-running install script to update symlink..."
"$REPO_DIR/install.sh"
else
warning "install.sh not found, skipping symlink update"
fi
echo
success "Update complete!"
}
# Parse arguments
UPDATE_MODE=false
for arg in "$@"; do
case $arg in
--update|-u)
UPDATE_MODE=true
shift
;;
--help|-h)
show_help
exit 0
;;
*)
warning "Unknown option: $arg"
echo "Use --help for usage information"
exit 1
;;
esac
done
# If --update flag is set, run update and exit
if [ "$UPDATE_MODE" = true ]; then
update_installation
exit 0
fi
# Normal installation flow
info "Bootstrap installer for mdtosendy"
echo
# Check for required tools
if ! command -v git >/dev/null 2>&1; then
error "git is not installed"
echo "Please install git first: https://git-scm.com/downloads"
exit 1
fi
# Prompt for install directory
DEFAULT_INSTALL_DIR="$HOME/mdtosendy"
info "Installation directory (default: $DEFAULT_INSTALL_DIR)"
read -p "Enter directory path or press Enter for default: " INSTALL_DIR
INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}" # Expand ~
# Check if directory exists
if [ -d "$INSTALL_DIR" ]; then
if [ -d "$INSTALL_DIR/.git" ]; then
# It's a git repository, offer to update
warning "Directory already exists and is a git repository: $INSTALL_DIR"
read -p "Update existing installation? (Y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
info "Updating existing installation..."
cd "$INSTALL_DIR" || exit 1
if ! git pull; then
error "Failed to update repository"
exit 1
fi
success "Repository updated"
else
info "Installation cancelled"
exit 0
fi
else
# Directory exists but is not a git repository
warning "Directory already exists but is not a git repository: $INSTALL_DIR"
read -p "Remove and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
info "Removing existing directory..."
rm -rf "$INSTALL_DIR"
else
read -p "Enter a different directory path: " INSTALL_DIR
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}" # Expand ~
if [ -d "$INSTALL_DIR" ]; then
error "Directory still exists: $INSTALL_DIR"
exit 1
fi
fi
fi
fi
# Clone repository
info "Cloning repository to $INSTALL_DIR..."
if ! git clone https://github.com/ttscoff/mdtosendy.git "$INSTALL_DIR"; then
error "Failed to clone repository"
exit 1
fi
success "Repository cloned successfully"
# Run install.sh from cloned directory
if [ -f "$INSTALL_DIR/install.sh" ]; then
info "Running install script..."
cd "$INSTALL_DIR" || exit 1
chmod +x install.sh
./install.sh
else
error "install.sh not found in cloned repository"
exit 1
fi
echo
success "Bootstrap installation complete!"
echo
info "The repository is installed at: $INSTALL_DIR"
info "You can update it later by running: cd $INSTALL_DIR && git pull"
info "Or use: ./bootstrap.sh --update (if you saved this script locally)"