Automatiser la génération de lettres de motivation avec une extension Raycast (export en PDF)

Utilisez un script Bash et Node.js (docxtemplater) pour remplir un modèle Word avec l'entreprise, le poste et la date, puis exportez en PDF via Pages, le tout déclenché depuis Raycast.

Votre MacBook doit travailler pour vous, et non l’inverse. Il est donc essentiel d’utiliser les capacités dont vous disposez localement en automatisant au plus tôt vos tâches et flux de travail répétitifs.

Ici, nous allons automatiser la génération de lettres de motivation personnalisées que vous envoyez aux entreprises avec votre CV. Comme la lettre est adaptée à une entreprise, un poste et une date précis, copier une lettre, modifier ces champs puis enregistrer en PDF devient long lorsque vous postulez à des dizaines d’offres.

Vue d’ensemble

Nous allons créer un script Bash et un script JavaScript pour personnaliser et générer la lettre de motivation en PDF. Ensuite, nous ajouterons ce script à Raycast pour l’appeler depuis son interface.

Note : Nous sommes obligés d’utiliser le script JS car, en mettant à jour le fichier .docx avec uniquement des outils intégrés, le PDF généré échouait ou supprimait tous les styles du document d’origine.

Prérequis

  • Raycast installé.
  • NodeJS installé.
  • Une lettre de motivation en Word ou Google Docs. Si Google Docs, téléchargez-la en Microsoft Word (.docx) en local.

Installation

Pour tout garder organisé et prêt pour la suite, créons un dossier dans le répertoire utilisateur nommé raycast-scripts/cover-letter, puis allons-y.

cd ~
mkdir raycast-scripts/cover-letter
cd raycast-scripts/cover-letter

Ensuite, installons les paquets nécessaires pour manipuler le fichier docx à l’étape suivante.

npm i pizzip docxtemplater

Création des scripts

Créez deux fichiers : cover-letter-local.sh et generate-cover-letter.mjs. Rendez le script shell exécutable :

chmod +x cover-letter-local.sh

1. cover-letter-local.sh

Ce script est le point d’entrée appelé par Raycast. Il accepte Company (Entreprise) et Role (Poste) en arguments, localise votre modèle DOCX, lance le script Node pour remplir les placeholders, puis utilise Pages (via AppleScript) pour exporter le document rempli en PDF.

#!/bin/bash

# @raycast.schemaVersion 1
# @raycast.title Generate Cover Letter
# @raycast.mode compact
# @raycast.packageName cover-letter
# @raycast.icon 📄
# @raycast.argument1 { "type": "text", "placeholder": "Company (e.g. Acme)" }
# @raycast.argument2 { "type": "text", "placeholder": "Role (e.g. Backend Engineer)" }

set -euo pipefail

COMPANY_NAME="$1"
ROLE_NAME="$2"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE_DOCX="$SCRIPT_DIR/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx"
OUTDIR="$HOME/Downloads/Cover Letters"
mkdir -p "$OUTDIR"

NEW_DATE="$(LC_TIME=en_US_POSIX date '+%b %e, %Y' | sed 's/  / /g')"

sanitize() {
  echo "$1" \
    | tr '\n' ' ' \
    | sed -E 's/[\/:*?"<>|]/-/g; s/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//'
}

SAFE_COMPANY="$(sanitize "$COMPANY_NAME")"
BASENAME="YOUR_NAME_${SAFE_COMPANY}_Cover Letter"

TMPDOCX="/tmp/${BASENAME}.docx"
PDFPATH="${OUTDIR}/${BASENAME}.pdf"

if [[ ! -f "$TEMPLATE_DOCX" ]]; then
  echo "DOCX template not found: $TEMPLATE_DOCX"
  exit 1
fi

if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then
  echo "Run once in this folder: npm install"
  exit 1
fi

NODE=""
if command -v node &>/dev/null; then
  NODE="node"
elif [[ -x "$HOME/.nvm/current/bin/node" ]]; then
  NODE="$HOME/.nvm/current/bin/node"
elif [[ -x /opt/homebrew/bin/node ]]; then
  NODE="/opt/homebrew/bin/node"
elif [[ -x /usr/local/bin/node ]]; then
  NODE="/usr/local/bin/node"
fi
if [[ -z "$NODE" ]]; then
  echo "node not found. Install Node.js or add it to PATH."
  exit 1
fi

"$NODE" "$SCRIPT_DIR/generate-cover-letter.mjs" \
  "$TEMPLATE_DOCX" "$TMPDOCX" "$COMPANY_NAME" "$ROLE_NAME" "$NEW_DATE"

osascript - "$TMPDOCX" "$PDFPATH" <<'APPLESCRIPT'
on run argv
  set docxPosix to item 1 of argv
  set pdfPosix to item 2 of argv
  set docxAlias to (POSIX file docxPosix) as alias
  set pdfFile to POSIX file pdfPosix
  tell application "Pages"
    activate
    open docxAlias
    delay 0.8
    tell front document
      export it to pdfFile as PDF
      close saving no
    end tell
    quit
  end tell
end run
APPLESCRIPT

rm -f "$TMPDOCX"
echo "Saved: $PDFPATH"

Ce que fait le script :

  • Les métadonnées Raycast (commentaires @raycast.*) définissent le nom de la commande, l’icône et deux arguments texte pour afficher les champs « Company » et « Role » dans Raycast.
  • Chemins : TEMPLATE_DOCX pointe vers YOUR_COVER_LETTER_FILE_NAME.docx dans un sous-dossier JobSearch/. Remplacez YOUR_COVER_LETTER_FILE_NAME par le nom réel de votre fichier DOCX (sans le chemin). Les PDF sont écrits dans ~/Downloads/Cover Letters.
  • Date : utilise la date du jour au format type Feb 11, 2025.
  • Sanitize : nettoie le nom de l’entreprise pour l’utiliser dans le nom de fichier (supprime ou remplace les caractères interdits).
  • Nommage : BASENAME utilise YOUR_NAME_ pour obtenir des noms du type YOUR_NAME_Acme_Cover Letter.pdf. Remplacez YOUR_NAME_ par votre nom ou vos initiales.
  • Recherche de Node : essaie node dans le PATH, puis les emplacements courants (nvm, Homebrew), pour que le script fonctionne quand il est lancé depuis Raycast (où le PATH peut être minimal).
  • AppleScript : ouvre le DOCX rempli dans Pages, l’exporte en PDF, ferme le document sans enregistrer puis quitte Pages. Le DOCX temporaire est supprimé.

2. generate-cover-letter.mjs

Ce script Node remplit le modèle DOCX en remplaçant les placeholders texte dans le XML du document. Il n’utilise pas la syntaxe {COMPANY_NAME}, seulement les mots COMPANY_NAME, ROLE_NAME et NEW_DATE dans votre fichier Word.

#!/usr/bin/env node
/**
 * Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>
 */

import PizZip from "pizzip";
import fs from "fs";
import path from "path";

const [templatePath, outputPath, companyName, roleName, newDate] =
  process.argv.slice(2);

const TEMPLATE = templatePath || process.env.TEMPLATE_PATH;
const OUTPUT = outputPath || process.env.OUTPUT_PATH;
const COMPANY = companyName || process.env.COMPANY_NAME || "";
const ROLE = roleName || process.env.ROLE_NAME || "";
const DATE = newDate || process.env.NEW_DATE || "";

if (!TEMPLATE || !OUTPUT) {
  console.error(
    "Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>",
  );
  process.exit(1);
}

const templateAbs = path.resolve(TEMPLATE);
const outputAbs = path.resolve(OUTPUT);

if (!fs.existsSync(templateAbs)) {
  console.error("Template not found:", templateAbs);
  process.exit(1);
}

const content = fs.readFileSync(templateAbs, "binary");
const zip = new PizZip(content);

const docPath = "word/document.xml";
const docFile = zip.files[docPath];
if (!docFile) {
  console.error("DOCX invalid: missing word/document.xml");
  process.exit(1);
}

let xml = docFile.asText();
const replacements = [
  ["COMPANY_NAME", COMPANY],
  ["ROLE_NAME", ROLE],
  ["NEW_DATE", DATE],
];

for (const [placeholder, value] of replacements) {
  const escaped = String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
  xml = xml.split(placeholder).join(escaped);
}

zip.file(docPath, xml);

const buf = zip.generate({ type: "nodebuffer" });
fs.writeFileSync(outputAbs, buf);

console.log(outputAbs);

Ce qu’il fait :

  • Lit le DOCX comme une archive ZIP, modifie word/document.xml et écrit un nouveau DOCX. Toute la mise en forme (styles, polices, mise en page) est conservée car seuls les nœuds texte sont modifiés.
  • Remplace COMPANY_NAME, ROLE_NAME et NEW_DATE par les valeurs passées (ou via les variables d’environnement). Les valeurs sont échappées pour le XML pour que des caractères comme & ou < ne corrompent pas le fichier.

3. Préparer votre modèle DOCX

  1. Rédigez votre lettre de motivation dans Word (ou Google Docs, puis Fichier → Télécharger → Microsoft Word (.docx)).
  2. Là où vous voulez le nom de l’entreprise, tapez COMPANY_NAME (sans accolades).
  3. Là où vous voulez le poste, tapez ROLE_NAME.
  4. Là où vous voulez la date, tapez NEW_DATE.
  5. Enregistrez le fichier et placez-le où cover-letter-local.sh l’attend, par exemple raycast-scripts/cover-letter/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx. Dans le script, remplacez YOUR_COVER_LETTER_FILE_NAME par le nom réel de votre fichier DOCX et YOUR_NAME_ par votre nom (ou initiales) pour que les fichiers exportés aient le nom souhaité.

4. Ajouter le script à Raycast

  1. Ouvrez Raycast → Settings → Extensions.
  2. Cliquez sur Add Script Directory et choisissez ~/raycast-scripts/cover-letter (ou le dossier contenant cover-letter-local.sh et generate-cover-letter.mjs).
  3. Raycast détectera le script grâce aux commentaires @raycast.*. Vous devriez voir Generate Cover Letter sous le package cover-letter.
  4. Lancez-le : saisissez l’entreprise et le poste, puis Entrée. Le script génère le DOCX rempli, l’exporte en PDF via Pages et l’enregistre dans ~/Downloads/Cover Letters.

Vous avez maintenant un flux en une étape : Entreprise + Poste dans Raycast → lettre de motivation PDF personnalisée sur votre Mac, sans recherche-remplacement ni export manuel.

Bonne chance pour décrocher le poste !