Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ node_modules
.idea
.vscode
.iterm-workspace

*.preview.html
2 changes: 2 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"jsdom": "^22.0.0",
"json2csv": "^5.0.6",
"jsonwebtoken": "^9.0.0",
"liquidjs": "^10.24.0",
"lodash": "^4.17.23",
"mime-types": "^2.1.35",
"mixin-deep": "^2.0.1",
Expand Down Expand Up @@ -144,6 +145,7 @@
"lint": "eslint --max-warnings=0 --ext .ts",
"lint:all": "eslint --no-error-on-unmatched-pattern --max-warnings=0 --ext .ts server",
"emails:build": "ts-node server/mails/build.ts",
"emails:preview": "ts-node server/mails/preview.ts",
"test:activity-summary": "ts-node test/mails/testActivitySummary.ts"
}
}
9 changes: 9 additions & 0 deletions packages/api/server/mails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ Les autres variables utilisés plus spécifiques:
- formationUrl => https://app.evalandgo.com/s/index.php?a=JTk2cCU5N2slOUElQjA=&id=JTk4ayU5QW4lOTYlQUY=
- adminUrl => https://resorption-bidonvilles.dihal.gouv.fr/liste-des-utilisateurs

## Développement

### Prévisualiser un email
```bash
yarn emails:preview \
--template activity_summary \
--variables server/mails/preview/activity_summary.variables.example.json
```

## Tracking

Tracking des activités proposées dans les mèls
Expand Down
89 changes: 89 additions & 0 deletions packages/api/server/mails/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from 'node:fs';
import path from 'node:path';
import mjml2html from 'mjml';
import { Liquid } from 'liquidjs';

const args = process.argv.slice(2);

const getArg = (names: string[]): string | undefined => {
const index = args.findIndex(arg => names.includes(arg));
if (index === -1) {
return undefined;
}

return args[index + 1];
};

const templateName = getArg(['--template', '-t']);
const variablesPath = getArg(['--variables', '-v']);
const outputPathArg = getArg(['--output', '-o']);
const strictMode = args.includes('--strict');

if (!templateName || !variablesPath) {
// eslint-disable-next-line no-console
console.error('Usage: yarn emails:preview --template <name> --variables <path/to/variables.json> [--output <path/to/output.html>]');
process.exit(1);
}

const normalizeMailjetSyntax = (template: string): string => template
.replaceAll(/\bvar:([A-Za-z0-9_.]+)/g, 'var.$1')
.replaceAll(/{%\s*elseif\b/g, '{% elsif');

const run = async (): Promise<void> => {
const templatePath = path.resolve(__dirname, 'src', `${templateName}.mjml`);

if (!fs.existsSync(templatePath)) {
throw new Error(`Template introuvable: ${templatePath}`);
}

const absoluteVariablesPath = path.resolve(process.cwd(), variablesPath);
if (!fs.existsSync(absoluteVariablesPath)) {
throw new Error(`Fichier de variables introuvable: ${absoluteVariablesPath}`);
}

const variables = JSON.parse(fs.readFileSync(absoluteVariablesPath, 'utf-8'));
const mjmlContent = fs.readFileSync(templatePath, 'utf-8');
const compiled = mjml2html(mjmlContent, { filePath: templatePath });

if (compiled.errors.length > 0) {
compiled.errors.forEach((error) => {
// eslint-disable-next-line no-console
console.warn(`[MJML] ${error.formattedMessage}`);
});
if (strictMode) {
process.exit(1);
}
}

const engine = new Liquid({
cache: false,
strictVariables: false,
});
try {
const html = await engine.parseAndRender(normalizeMailjetSyntax(compiled.html), {
var: variables,
...variables,
});

const outputDir = path.resolve(__dirname, 'preview');
fs.mkdirSync(outputDir, { recursive: true });

const outputPath = outputPathArg
? path.resolve(process.cwd(), outputPathArg)
: path.join(outputDir, `${templateName}.preview.html`);

fs.writeFileSync(outputPath, html, 'utf-8');
/* eslint-disable no-console */
console.log(`\n✅ Preview générée avec succès: ${outputPath}`);
console.log(`Fichier: ${outputPath}`);
/* eslint-disable no-console */
} catch (err) {
/* eslint-disable no-console */
console.error('❌ Erreur lors du rendu Liquid :');
console.error(err);
/* eslint-disable no-console */
process.exit(1);
}
};

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"from": "01",
"to": "07 janvier 2026",
"recipientName": "Utilisateur Test",
"showDetails": false,
"show_question_summary": false,
"questions_with_answers_length": 0,
"questions_with_answers": [],
"questions_without_answers_length": 0,
"questions_without_answers": [],
"wwwUrl": "https://www.resorption-bidonvilles.gouv.fr",
"webappUrl": "https://app.resorption-bidonvilles.gouv.fr",
"backUrl": "https://api.resorption-bidonvilles.gouv.fr",
"utm": "utm_source=test&utm_medium=email&utm_campaign=preview",
"summaries": [
{
"code": "75",
"name": "Paris",
"has_activity": false,
"new_shantytowns": [],
"new_shantytowns_length": 0,
"closed_shantytowns": [],
"closed_shantytowns_length": 0,
"updated_shantytowns": [],
"updated_shantytowns_length": 0,
"new_comments": [],
"new_comments_length": 0,
"new_users": [],
"new_users_length": 0,
"shantytowns_total": 10,
"population_total": 120,
"updated_shantytowns_6_months": 9
}
]
}
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5949,6 +5949,7 @@ __metadata:
json2csv: ^5.0.6
jsonwebtoken: ^9.0.0
lint-staged: ^13.2.2
liquidjs: ^10.24.0
lodash: ^4.17.23
mime-types: ^2.1.35
mixin-deep: ^2.0.1
Expand Down Expand Up @@ -21523,6 +21524,18 @@ __metadata:
languageName: node
linkType: hard

"liquidjs@npm:^10.24.0":
version: 10.24.0
resolution: "liquidjs@npm:10.24.0"
dependencies:
commander: ^10.0.0
bin:
liquid: bin/liquid.js
liquidjs: bin/liquid.js
checksum: a25638d1a52e34d64ad0ff8deb04d42dd2276913b277742b389b9f5e1a8ec8fc3b923228eb701d28f48b36e199c15466eb5bfee4d38464806a05050ff2c507ce
languageName: node
linkType: hard

"listenercount@npm:~1.0.1":
version: 1.0.1
resolution: "listenercount@npm:1.0.1"
Expand Down
Loading