Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Handlebars: serverdagi klassik shablon
- 2.1 Birinchi shablon
- 2.2 strict — yo'q o'zgaruvchi xato bersin
- 2.3 Partial, helper va each
- 3. Ekranlash va havolalar: o'zingiz yozing
- 3.1 html() funksiyasi
- 3.2 Xavfsiz havola
- 4. Inline CSS: juice
- 5. MJML va React Email: serverda
- 5.1 MJML 5
- 5.2 React Email 6
- 5.3 Preheader va hajm
- 5.4 Qaysi birini tanlash
- 6. Matn versiyasi
- 7. Hujumchi nigohi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): HTML'dan matn versiya
- 3-mashq (qiyin): mini Handlebars
- 10. Loyiha qadami: Pochta moduli — shablonlar
- 11. Real ishda
- Xulosa
- Manbalar
Email shablonlari serverda: Handlebars, MJML va React Email
Qisqacha: Email xati — har foydalanuvchi uchun ma'lumot bilan to'ldiriladigan HTML shablon. Serverda uni Handlebars (
{{ism}}— avtomatik ekranlanadi,strict: true— yo'q o'zgaruvchi xato beradi), MJML (responsive jadval HTML yasaydi) yoki React Email (komponentlar,render()vaplainText) bilan yig'ish mumkin. Har xatda HTML va matn versiyasi bo'lsin, foydalanuvchi ma'lumoti doim ekranlansin, havola esa faqat server yasaganhttps://manzil bo'lsin — ekranlashjavascript:sxemasini to'xtatmaydi.
Bu darsda
- Nega email shablonini serverda render qilish kerakligini va qaysi vositani qachon tanlashni bilasiz.
- Handlebars bilan layout, partial va helper yozasiz,
{{ }}va{{{ }}}farqini ko'rasiz. - Kanon o'lchovlari bilan juice, MJML 5 va React Email 6 ni solishtirasiz.
- HTML ekranlash va xavfsiz havola funksiyalarini o'zingiz yozasiz.
Pochtamodulining shablon qismini tugatib, modulni commit qilasiz.
Oldin bilishingiz kerak: HTML email'larni stillash (juice, MJML, React Email — dizayn tomoni), Template engine'lar va server-side HTML, XSS va DOM xavfsizligi, Email yuborish: SMTP va Nodemailer.
Oldingi darsda Pochta.yubor() tayyor { mavzu, html, matn } ni yubordi. Bu matnlar qayerdan keladi? Bugun ularni shablondan yig'amiz.
1. Nega bu kerak?
HTML email'larni stillash darsida siz bitta xatni qo'lda yozdingiz: 600 pikselli jadval, inline stil, Outlook uchun shartli izoh. Endi o'sha xat har kuni 500 foydalanuvchiga ketadi va har birida boshqa ism, boshqa vazifalar, boshqa havola.
Satrlarni + bilan yopishtirish birinchi kunda ishlaydi. Keyin uch muammo chiqadi:
- Xavfsizlik. Foydalanuvchi ismini
<a href="https://yomon.uz">Hisobingiz bloklandi</a>deb yozdi. Ekranlanmagan ism xatga havola bo'lib tushadi — va xat sizning domeningizdan ketadi. Bu HTML injection, fishing uchun ideal. - Ikki versiya. Har xatga matn versiyasi kerak — uni HTML'dan qo'lda takrorlash xato manbai.
- Dizayn va kod aralashadi. Matnni o'zgartirish uchun JavaScript satrlarini titkilash kerak.
Shablon (template) — bo'sh joylari belgilangan tayyor matn. O'xshatish: bankdagi blanka. Blanka bir marta chop etiladi, har mijoz faqat ism va summa kataklarini to'ldiradi. Katakka nima yozilsa ham, blankaning qolgan qismi buzilmaydi.
2. Handlebars: serverdagi klassik shablon
2.1 Birinchi shablon
Handlebars — {{ }} belgili shablon tili (Template engine'lar darsidagi g'oya). Shablon bir marta kompilyatsiya qilinadi — funksiyaga aylanadi. Keyin u har ma'lumot bilan chaqiriladi. Kanon handlebars 4.7.10 ni ishlatdi:
npm i handlebars@4.7.10import Handlebars from "handlebars";
const t = Handlebars.compile("<p>Salom, {{ism}}!</p>");
console.log(t({ ism: "Aziz" }));
console.log(t({ ism: '<b>Aziz</b> & "do\'st" =' }));
const uch = Handlebars.compile("<p>Salom, {{{ism}}}!</p>");
console.log(uch({ ism: "<b>Aziz</b>" }));Konsolda (biz ishga tushirdik: Node 24.21.0, handlebars 4.7.10):
<p>Salom, Aziz!</p>
<p>Salom, <b>Aziz</b> & "do'st" =!</p>
<p>Salom, <b>Aziz</b>!</p>Qoida bitta:
{{ism}}— ekranlanadi:<→<,&→&, qo'shtirnoq va apostrof ham. Hatto=ham=bo'ldi — atribut ichida qo'shtirnoqsiz qiymat buzilmasin uchun.{{{ism}}}— uchta qavs — ekranlanmaydi. HTML xatga aynan tushadi.
Uchta qavsni foydalanuvchi ma'lumoti uchun ishlatmang. U faqat siz o'zingiz yig'gan va ishongan HTML uchun (masalan, layout ichiga tananing tayyor HTML'i).
2.2 strict — yo'q o'zgaruvchi xato bersin
const qattiq = Handlebars.compile("Salom, {{ism}}! {{havola}}", {
strict: true,
});
try {
qattiq({ ism: "Aziz" });
} catch (e) {
console.log(e.message.split("\n")[0]);
}
const yumshoq = Handlebars.compile("Salom, {{ism}}! [{{havola}}]");
console.log(yumshoq({ ism: "Aziz" }));Konsolda (biz ishga tushirdik):
"havola" not defined in [object Object] - 1:18
Salom, Aziz! []Xato: "havola aniqlanmagan, 1-qator, 18-ustun". Sukutda esa Handlebars yo'q o'zgaruvchini jim bo'sh satrga aylantiradi: foydalanuvchi "Tasdiqlash" tugmasi bor, lekin havolasi bo'sh xat oladi. Bunday xatoni test emas, foydalanuvchi topadi. strict: true bilan esa birinchi testda yiqiladi.
2.3 Partial, helper va each
Katta xatlar bo'laklardan yig'iladi:
- Partial — qayta ishlatiladigan bo'lak (tugma, futer):
{{> tugma url=havola matn="Ochish"}}. - Helper — shablon ichidan chaqiriladigan JS funksiya:
{{son vazifalar}}. {{#each}}— ro'yxat bo'ylab takrorlash.
Handlebars.registerPartial("tugma", '<a href="{{url}}">{{matn}}</a>');
Handlebars.registerHelper("son", (arr) => arr.length);
const r = Handlebars.compile(
"Bugun {{son vazifalar}} ta:" +
"{{#each vazifalar}} {{this}};{{/each}} " +
'{{> tugma url=havola matn="Ochish"}}',
);
console.log(r({
vazifalar: ["Non olish", "Hisobot"],
havola: "https://v.example/t/1",
}));Konsolda (biz ishga tushirdik):
Bugun 2 ta: Non olish; Hisobot; <a href="https://v.example/t/1">Ochish</a>Mantiqni shablonga ko'p kiritmang. Hisob-kitob (sana formati, summa) — JavaScript'da, shablonga tayyor satr beriladi. Handlebars ataylab "mantiqsiz" (logic-less) qilib yaratilgan.
Tekshirib ko'ring: Admin panelda foydalanuvchilarga "o'z shablonini yozish" imkoniyatini bermoqchisiz: matn
Handlebars.compile(foydalanuvchiMatni)ga beriladi. Bu xavfsizmi?
Javob
Yo'q. Shablon — kod. Begona shablon helper'larni chaqiradi va kontekstdagi har maydonni o'qiy oladi (masalan, xatga uzatilgan butun foydalanuvchi obyekti). Bu shablon injection (server-side template injection — begona shablonni serverda bajarish). Handlebars 4.7.10 da prototip xususiyatlari sukutda berilmaydi — biz {{constructor.name}} ni sinadik, natija bo'sh. Lekin bu yagona himoya qatlami bo'lmasin: foydalanuvchiga faqat oldindan belgilangan o'zgaruvchilar ro'yxatini bering va shablonni ishonchli rol (admin) tahrirlasin.
3. Ekranlash va havolalar: o'zingiz yozing
3.1 html() funksiyasi
Handlebars ichida nima bo'layotganini tushunish uchun ekranlashni o'zimiz yozamiz:
const BELGILAR = {
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'", "`": "`", "=": "=",
};
const html = (s) =>
String(s).replace(/[&<>"'`=]/g, (b) => BELGILAR[b]);
console.log(html('<img src=x onerror=alert(1)> "Aziz"'));
console.log(html("Gʻayrat & Co"));
console.log(`<a href="${html("javascript:alert(1)")}">Ochish</a>`);Konsolda:
<img src=x onerror=alert(1)> "Aziz"
Gʻayrat & Co
<a href="javascript:alert(1)">Ochish</a>Birinchi ikki qator — kutilganidek: teglar matnga aylandi, o'zbek harflari tegilmadi.
Uchinchi qator — darsning eng muhim tuzog'i. javascript:alert(1) da ekranlanadigan belgi yo'q. Ekranlash atributdan "chiqib ketishni" to'xtatadi, lekin atribut ichidagi qiymatning ma'nosini tekshirmaydi. Kanon modul testi aynan shuni qayd etgan: href="{{havola}}" ga javascript: sxemasi o'tadi.
Pochta ilovalarining ko'pchiligi javascript: havolasini bloklaydi. Lekin https://yomon.uz/kirish kabi oddiy fishing havolasini bloklamaydi. Xulosa: havola foydalanuvchidan kelmasin — uni server o'zi yasaydi.
3.2 Xavfsiz havola
Ba'zan havola baribir tashqaridan keladi (masalan, vazifa ichidagi manzil). U holda uni ruxsat ro'yxati bilan tekshiring:
function xavfsizHavola(matn, ruxsat = ["vazifalar.example.uz"]) {
let url;
try {
url = new URL(matn);
} catch {
return null;
}
if (url.protocol !== "https:") return null;
if (!ruxsat.includes(url.hostname)) return null;
return url.href;
}
console.log(xavfsizHavola("https://vazifalar.example.uz/t/abc"));
console.log(xavfsizHavola("javascript:alert(1)"));
console.log(xavfsizHavola("JavaScript:alert(1)"));
console.log(xavfsizHavola("https://vazifalar.example.uz.yomon.uz/t"));
console.log(xavfsizHavola("http://vazifalar.example.uz/t"));
console.log(xavfsizHavola("/t/abc"));Konsolda:
https://vazifalar.example.uz/t/abc
null
null
null
null
nullMatnni new URL tahlil qiladi (URL va URLSearchParams) — startsWith("https") emas. Shuning uchun katta-kichik harfli JavaScript: va vazifalar.example.uz.yomon.uz kabi "o'xshash" domen ham ushlandi: hostname aniq solishtiriladi. Nisbiy yo'l (/t/abc) ham rad — emailda nisbiy havola ishlamaydi, xatda doim to'liq manzil.
4. Inline CSS: juice
Pochta ilovalari <style> blokini ko'pincha olib tashlaydi, shuning uchun stillar har tegning style atributiga ko'chiriladi (HTML email'larni stillash darsida juice'ni ko'rgansiz). Serverda bu render'dan keyingi qadam. Kanon Handlebars layout + partial + helper'ni juice 12.2.0 dan o'tkazdi:
Handlebars HTML 566 belgi → juice 608 belgi
tugma (juice'dan keyin): <a class="tugma" href="https://vazifalar.example.uz/t/abc123" style="background: #4f46e5; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px;">
@media qoidasi <style> da qoldi: true (inline qilib bo'lmaydi; Gmail ilovalari qo'llamasligi mumkin — sinalmagan)
render + juice: mediana 1.133 ms.tugmaklassining qoidasi tugmaning o'ziga ko'chdi.@media (prefers-color-scheme: dark)— qorong'i rejim qoidasi —<style>da qoldi: media so'rovni inline qilib bo'lmaydi. Uni qo'llaydigan ilovalarda ishlaydi, qolganlarida yorug' versiya ko'rinadi.- Render + juice — taxminan bir millisekund. Har xat uchun qayta yasash qimmat emas.
5. MJML va React Email: serverda
5.1 MJML 5
MJML — responsive email uchun maxsus belgilash tili: <mj-section>, <mj-column>, <mj-button> yozasiz, u Outlook'ga mos jadval HTML yasaydi. Kanon MJML 5.4.1 bilan bitta xatni render qildi:
MJML: 5742 belgi, 32 ms (birinchi chaqiruv), xatolar: 0, <table> soni: 5, MSO shartli izohlar: 5
MJML keyingi chaqiruvlar: mediana 2.4 ms
noto'g'ri tuzilma (mj-text mj-column'siz): Line 1 of C:\Users\…\Temp\…\node (mj-text) — mj-text cannot be used inside mj-body, only inside: mj-attributes, mj-column, mj-hero(Xatodagi lokal yo'lni qisqartirdik.)
O'n qatorga yaqin MJML 5.7 KB HTML'ga aylandi: beshta jadval va Outlook uchun beshta shartli izoh. Buni qo'lda yozish — soatlar. Noto'g'ri tuzilma esa aniq xabar beradi: "mj-text ni mj-body ichida to'g'ridan ishlatib bo'lmaydi, faqat mj-column ichida".
Kanon bitta muhim eslatma yozdi: MJML ichiga foydalanuvchi matnini JavaScript template literal bilan qo'ysangiz, u ekranlanmaydi. MJML — shablon tili emas, faqat tuzilma. Ma'lumotni qo'yishdan oldin html() dan o'tkazing yoki MJML'ni Handlebars bilan birga ishlating (avval Handlebars, keyin MJML).
5.2 React Email 6
React Email — xatni React komponentlari bilan yozish (<Html>, <Button>, <Preview>). Serverda render() uni HTML satrga aylantiradi. Kanon muhim o'zgarishni topdi: avvalgi @react-email/components va uning bo'lak paketlari npm'da deprecated (eskirgan) deb belgilangan. O'rniga bitta paket — react-email 6.11.3: komponentlar ham, render ham shu yerda.
npm i react-email@6.11.3 react@19.3.1 react-dom@19.3.1Kanon JSX'siz, createElement bilan yozdi (o'quvchi React'ni 17-qismdan biladi). Asosiy qism:
import { createElement as h } from "react";
import {
Html, Head, Body, Container, Heading, Text, Button, Preview,
render,
} from "react-email";
const Xat = ({ ism, havola, soni }) =>
h(Html, { lang: "uz" }, h(Head),
h(Preview, null, "Bugungi vazifalar"),
h(Body, null,
h(Container, null,
h(Heading, { as: "h1" }, `Salom, ${ism}!`),
h(Text, null, `Bugun ${soni} ta vazifa:`),
h(Button, { href: havola }, "Ochish"))));
const html = await render(h(Xat, { ism, havola, soni: 2 }));
const matn = await render(h(Xat, { ism, havola, soni: 2 }), {
plainText: true,
});Kanonning natijasi (to'liq komponentda vazifalar ro'yxati va stillar ham bor). plainText versiya dan keyingi qatorlarga qarang — bu HTML'dan avtomatik olingan matn:
HTML 3078 belgi, birinchi render 474 ms; ism ekranlandi: true
plainText versiya:
| SALOM, GʻAYRAT!
|
| Bugun 2 ta vazifa:
|
| Non olish — 28.10
|
| Hisobot — 29.10
|
| Ochish https://vazifalar.example.uz/t/abc123
React Email render: mediana 4.06 ms- Ism o'rniga
<script>alert(1)</script>berilganda u ekranlandi — React matnni doim ekranlaydi. plainText: true— matn versiyasi bepul: sarlavha katta harf bilan, tugma — "Ochish" va havola.- Birinchi render 474 ms (modullar yuklanishi), keyingilari ~4 ms. Server ishga tushganda bitta "qizdirish" render'i foydali.
5.3 Preheader va hajm
Preheader — pochta qutisida mavzudan keyingi kulrang qator (HTML email'larni stillash). React Email'da u <Preview>, kanon chiqishida:
Preview elementi: <div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" data-skip-in-text="true">
Gmail ~102 KB dan katta HTML ni kesadi ('[Message clipped]' — Gmail xulqi, hujjat/kuzatuv bo'yicha; sinalmagan). Bu yerdagi xatlar: 0.6 KB, 0.6 KB, 5.6 KB, 4.6 KBYashirin div: xat ochilganda ko'rinmaydi, ro'yxatda esa ko'rinadi. data-skip-in-text — matn versiyasiga tushmasin. Hajm: to'rtta xat 0.6–5.6 KB. Gmail ~102 KB dan katta HTML'ni kesadi — kanon buni sinamagan, bu keng tarqalgan kuzatuv. Xat oxiridagi "obunani bekor qilish" havolasi aynan kesilgan qismda qolib ketishi mumkin.
5.4 Qaysi birini tanlash
- Handlebars + juice1,133 ms
- MJML 52,4 ms
- React Email 64,06 ms
Manba: IlmHamroh kanon tajribasi, 2026-10-27: handlebars 4.7.10, juice 12.2.0, mjml 5.4.1, react-email 6.11.3, Node 24.21.0, i5-12500H; birinchi chaqiruvlar: MJML 32 ms, React Email 474 ms
| Vosita | Kuchli tomoni | Qachon |
|---|---|---|
| Handlebars (+ juice) | oddiy, tez, shablon — alohida fayl | kam xat, oddiy dizayn |
| MJML | Outlook'ga mos jadvalni o'zi yasaydi | dizayni murakkab xatlar |
| React Email | komponentlar, TypeScript, plainText |
jamoa React'ni biladi, ko'p xat |
Uchalasi ham bitta xat uchun millisekundlar — tezlik tanlov sababi emas. Kanon Pochta modulida Handlebars'ni tanladi: ikkita kichik xat uchun React va JSX bog'liqligi ortiqcha.
6. Matn versiyasi
HTML'dan matn olishning uch yo'li bor:
- Alohida shablon — kanon tanlovi:
matnmaydoni o'z Handlebars shabloni bilan. Eng aniq natija, lekin ikki joyni yangilash kerak. - Avtomatik — React Email'ning
plainText. - O'zgartirgich — HTML'dan teglarni olib tashlash (2-mashqda o'zingiz yozasiz).
Matn versiyasida HTML ekranlash kerak emas: u brauzerda ko'rsatilmaydi. Kanon shuning uchun matn shablonini noEscape: true bilan kompilyatsiya qiladi. Aks holda Aziz Non <olish> ni ko'radi. Mavzu ham shunday — u HTML emas.
Tekshirib ko'ring: Matn versiyasida ekranlash yo'q. Hujumchi ismiga
<script>yozsa, xavf bormi?
Javob
Yo'q. text/plain qismini pochta ilovasi matn sifatida ko'rsatadi — teg bajarilmaydi. Xavf faqat HTML qismida. Lekin matndagi havolaga o'xshash satr (https://yomon.uz) ilovada bosiladigan havola bo'lib ko'rinishi mumkin. Shuning uchun foydalanuvchi matnini uzunligi bilan cheklash va xatga faqat kerakli maydonlarni qo'yish baribir foydali.
7. Hujumchi nigohi
| Hujum | Natijasi | Himoya |
|---|---|---|
ismga <a href=…>Hisob bloklandi</a> |
sizning domeningizdan fishing xati | {{ }} ekranlash; {{{ }}} — faqat ishonchli HTML |
havolaga javascript: yoki begona domen |
bosilganda zararli sahifa | havolani server yasaydi; tashqi bo'lsa xavfsizHavola |
| foydalanuvchi shablon yozadi | shablon injection | o'zgaruvchilar ro'yxati, faqat admin |
| MJML/template literal'ga xom matn | HTML injection | avval html() yoki Handlebars |
| mavzuga uzun matn | spam filtri, kesilgan mavzu | foydalanuvchi matnini qisqartirish |
Email — XSS uchun g'alati joy: pochta ilovalari skriptni deyarli doim o'chiradi. Lekin email'dagi asosiy xavf skript emas, ishonch. Xat sizning domeningizdan keladi, SPF va DKIM tekshiruvidan o'tadi (Email yetkazib berish). Uning ichidagi har havola foydalanuvchi uchun "rasmiy". Shuning uchun HTML injection emailda veb-sahifadagidan ham xavfliroq.
8. Ko'p uchraydigan xatolar
| Xato | Belgisi | To'g'risi |
|---|---|---|
{{{ism}}} foydalanuvchi ma'lumoti uchun |
HTML injection | {{ism}} |
strict yo'q |
tugma havolasi bo'sh | strict: true + test |
| matn versiyasi ham ekranlangan | & matnda |
noEscape: true faqat matn va mavzu uchun |
| havolani foydalanuvchidan olish | fishing | server yasaydi yoki ruxsat ro'yxati |
@react-email/components o'rnatish |
npm deprecated ogohlantirishi | react-email 6 |
nisbiy havola (/t/abc) |
xatda ishlamaydi | to'liq https:// manzil |
har xatda shablonni qayta compile |
ortiqcha ish | modul yuklanganda bir marta |
9. Mashqlar
Mashqlar kurs/mashqlar/29/14-shablon/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
{{havola}}ekranlashjavascript:havolasini to'xtatadi.strict: truebilan yo'q o'zgaruvchi bo'sh satrga aylanadi.- Matn versiyasini ekranlash shart emas.
- MJML foydalanuvchi matnini o'zi ekranlaydi.
- React Email
render()gaplainText: trueberilsa, matn versiya qaytadi.
Yechim
- Noto'g'ri —
javascript:alert(1)da ekranlanadigan belgi yo'q; kanon testi buni tasdiqladi. - Noto'g'ri —
strictbilan xato tashlanadi:"havola" not defined. Bo'sh satr — sukutdagi xulq. - To'g'ri — u HTML sifatida ko'rsatilmaydi.
- Noto'g'ri — kanon eslatmasi: template literal bilan qo'yilgan matn ekranlanmaydi.
- To'g'ri — sarlavha katta harf bilan, tugma — matn va havola.
2-mashq (o'rta): HTML'dan matn versiya
matnVersiya(html) funksiyasini yozing: <a href="URL">Matn</a> → Matn (URL); </p>, </h1>, </li>, <br> → yangi qator; <li> → - ; qolgan teglar olib tashlansin; &, <, >, " ochilsin; ketma-ket uch va undan ko'p yangi qator ikkitaga qisqarsin; boshi va oxiridagi bo'shliq olinsin. Ishora: replace ning ikkinchi argumenti funksiya yoki $1, $2 bo'lishi mumkin (Almashtirish chuqur).
Yechim
const ENT = { "&": "&", "<": "<", ">": ">", """: '"' };
function matnVersiya(html) {
return html
.replace(/<a [^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g, "$2 ($1)")
.replace(/<br\s*\/?>|<\/p>|<\/h1>|<\/li>/g, "\n")
.replace(/<li>/g, "- ")
.replace(/<[^>]+>/g, "")
.replace(/&(amp|lt|gt|quot);/g, (e) => ENT[e])
.replace(/\n{3,}/g, "\n\n")
.trim();
}
const xat =
"<h1>Salom, Aziz!</h1><p>Bugun 2 ta vazifa:</p>" +
"<ul><li>Non olish</li><li>Hisobot & chek</li></ul>" +
'<p><a href="https://vazifalar.example.uz/t/1">Ochish</a></p>';
console.log(matnVersiya(xat));Konsolda:
Salom, Aziz!
Bugun 2 ta vazifa:
- Non olish
- Hisobot & chek
Ochish (https://vazifalar.example.uz/t/1)Tartib muhim. Havola teglar olib tashlanishidan oldin ochiladi — aks holda URL yo'qoladi. Entity'lar esa eng oxirida ochiladi: avval ochsak, <b> haqiqiy <b> ga aylanib, keyingi qadamda teg sifatida o'chib ketardi. Bu o'quv funksiyasi — murakkab HTML (jadvallar, ichma-ich teglar) uchun tayyor kutubxona yoki React Email plainText ishonchliroq.
3-mashq (qiyin): mini Handlebars
render(shablon, data) yozing: {{ nom }} — html() bilan ekranlangan qiymat, {{{nom}}} — xom qiymat, data da kalit bo'lmasa — Error('"nom" not defined') (strict). Kalit tekshiruvi Object.hasOwn bilan bo'lsin (Obyekt: kalit-qiymat juftliklari). To'rt holatni sinang: ekranlangan va xom qiymat, bo'shliqli {{ son }}, yo'q imzo, {{toString}}.
Yechim
const BELGILAR = {
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'", "`": "`", "=": "=",
};
const html = (s) =>
String(s).replace(/[&<>"'`=]/g, (b) => BELGILAR[b]);
const RE = /\{\{(\{?)\s*(\w+)\s*\}?\}\}/g;
function render(shablon, data) {
return shablon.replace(RE, (_m, xom, nom) => {
if (!Object.hasOwn(data, nom)) {
throw new Error(`"${nom}" not defined`);
}
return xom ? String(data[nom]) : html(data[nom]);
});
}
const s = "<p>Salom, {{ism}}!</p>{{{imzo}}}";
const d = { ism: "<b>Ali</b>", imzo: "<i>vazifalar</i>" };
console.log(render(s, d));
console.log(render("{{ son }} ta", { son: 3 }));
try {
render(s, { ism: "Ali" });
} catch (e) {
console.log(e.message);
}
try {
render("{{toString}}", {});
} catch (e) {
console.log(e.message);
}Konsolda:
<p>Salom, <b>Ali</b>!</p><i>vazifalar</i>
3 ta
"imzo" not defined
"toString" not definedOxirgi holat — nega in emas, Object.hasOwn: "toString" in {} — true, chunki u prototipdan keladi. in bilan shablon [object Object] emas, funksiya matnini xatga yozib yuborardi. Bu Handlebars prototip himoyasining kichik nusxasi. Regex {{{nom}} kabi teng bo'lmagan qavslarni ham qabul qiladi — haqiqiy shablon tili tahlilchisi (parser) buni xato deydi.
10. Loyiha qadami: Pochta moduli — shablonlar
Oldingi darsdagi pochta.ts ga shablon qismini qo'shamiz. Avval butun yo'lni ko'ramiz: bitta ma'lumot obyekti uchta shablondan o'tadi, faqat HTML yo'lida ekranlash bor:
flowchart LR
D["ma'lumot: matn, muddat, obunaBekor"] --> M["mavzu shabloni (noEscape)"]
D --> H["HTML shabloni (ekranlaydi)"]
D --> T["matn shabloni (noEscape)"]
M --> X["{ mavzu, html, matn }"]
H --> X
T --> X
X --> Y["Pochta.yubor() → SMTP"]Kanonda ikkita shablon bor: tasdiqlash (28-qismdagi email tasdiqlash uchun, jadval layout, preheader, qorong'i rejim meta teglari) va eslatma. Telefon uchun uzun qatorlarni bo'ldik; eslatma HTML'ida qator bo'linishi qo'shildi — brauzer uni bo'shliq sifatida ko'rsatadi, ma'nosi kanondagidek:
import Handlebars from "handlebars";
/** Handlebars: `{{x}}` HTML-ekranlanadi, `{{{x}}}` — yo'q
* (foydalanuvchi ma'lumoti uchun ISHLATILMAYDI).
* `strict` — yo'q o'zgaruvchi xato. */
export function shablonKompilyatsiya(
s: Shablon,
): (data: Record<string, unknown>) => Shablon {
const opt = { strict: true, noEscape: false };
// mavzu — matn, HTML emas; CRLF ni nodemailer o'zi tozalaydi
const xom = { ...opt, noEscape: true };
const mavzu = Handlebars.compile(s.mavzu, xom);
const html = Handlebars.compile(s.html, opt);
const matn = Handlebars.compile(s.matn, xom);
return (d) => ({ mavzu: mavzu(d), html: html(d), matn: matn(d) });
}
export const SHABLONLAR = {
tasdiqlash: {
mavzu: "Emailingizni tasdiqlang — vazifalar",
html: `…`, // jadval layout, tugma, "Tugma ishlamasa: {{havola}}"
matn: "…", // havola va "Siz so'ramagan bo'lsangiz …"
},
eslatma: {
mavzu: "Eslatma: {{matn}}",
html: `<!doctype html><html lang="uz"><head>
<meta charset="utf-8"></head>
<body style="font-family:Arial,sans-serif">
<p>Vazifa muddati yaqin: <strong>{{matn}}</strong> — {{muddat}}.</p>
<p style="font-size:12px;color:#777">
<a href="{{obunaBekor}}">Eslatmalarni o'chirish</a></p>
</body></html>`,
matn:
"Vazifa muddati yaqin: {{matn}} — {{muddat}}.\n\n" +
"Eslatmalarni o'chirish: {{obunaBekor}}\n",
},
} as const satisfies Record<string, Shablon>;- Bitta
Shablon— uchta Handlebars funksiya: mavzu, HTML, matn. Faqat HTML ekranlanadi. strict: true— har uchalasida.as const satisfies Record<string, Shablon>— har shablon to'g'ri shaklda ekani tekshiriladi, nomlari esa aniq tur bo'lib qoladi (satisfies).tasdiqlashHTML'ida{{havola}}ikki marta: tugmada va "Tugma ishlamasa" matnida. Ba'zi ilovalar tugmani ko'rsatmaydi — havola matnda ham bo'lishi kerak.
eslatma ni hujumchi uslubidagi vazifa nomi bilan render qildik (formatlangan modul nusxasi, Node 24.21.0):
const eslatma = shablonKompilyatsiya(SHABLONLAR.eslatma);
const x = eslatma({
matn: "Non <olish> & choy",
muddat: "28.10 18:00",
obunaBekor: "https://vazifalar.example.uz/obuna/x1",
});
console.log(x.mavzu);
console.log(x.matn);
console.log(x.html.match(/<p>Vazifa.*<\/p>/)[0]);
eslatma({ matn: "Non olish", muddat: "28.10" }); // obunaBekor yo'qEslatma: Non <olish> & choy
Vazifa muddati yaqin: Non <olish> & choy — 28.10 18:00.
Eslatmalarni o'chirish: https://vazifalar.example.uz/obuna/x1
<p>Vazifa muddati yaqin: <strong>Non <olish> & choy</strong> — 28.10 18:00.</p>
"obunaBekor" not defined in [object Object] - 6:11Mavzu va matnda belgilar o'z holicha, HTML'da — ekranlangan. obunaBekor unutilsa — xato, bo'sh havolali xat emas.
Kanon testi (test/email.test.ts, birinchi test) aynan shu qoidalarni tekshiradi:
test("shablon: {{}} ekranlaydi, matn versiyasi ekranlamaydi, " +
"yo'q o'zgaruvchi — xato", () => {
const t = shablonKompilyatsiya(SHABLONLAR.tasdiqlash);
const x = t({
ism: `<img src=x onerror=alert(1)> "Aziz"`,
havola: "https://v.example/t?a=1&b=2",
});
// Handlebars `=` ni ham ekranlaydi
assert.match(x.html,
/Salom, <img src=x onerror=alert\(1\)> "Aziz"!/);
assert.match(x.html,
/href="https:\/\/v\.example\/t\?a=1&b=2"/);
// ekranlash href ichidagi sxemani TEKSHIRMAYDI —
// havola faqat server yasagan (https) bo'lishi shart
assert.match(t({ ism: "A", havola: "javascript:alert(1)" }).html,
/href="javascript:alert\(1\)"/);
assert.match(x.matn, /Salom, <img src=x onerror=alert\(1\)> "Aziz"!/);
assert.throws(() => t({ ism: "A" }), /"havola" not defined/);
});Ikkinchi assert qiziq: havoladagi & va = ham ekranlandi (a=1&b=2). Brauzer va pochta ilovasi atributni o'qiyotganda ularni qaytadan ochadi — havola buzilmaydi. Formatlangan nusxani biz tsc, eslint va test bilan tekshirdik:
✔ shablon: {{}} ekranlaydi, matn versiyasi ekranlamaydi, yo'q o'zgaruvchi — xato (39.6647ms)
﹣ Mailpit orqali yuborish (0.6322ms) # MAILPIT_API yo'qMailpit testlari — oldingi darsdagidek, Docker'siz o'tkazildi; kanonda ular o'tgan (82/82). Modul tayyor — commit:
git commit -m "feat(email): Pochta — SMTP va Handlebars shablonlari"vazifalar-api ga ulash 28-qism tugagach: tasdiqlash xati ro'yxatdan o'tishda ketadi, havolani esa token bilan server yasaydi.
11. Real ishda
- Transactional provayderlar (Postmark, SendGrid, Brevo) o'z panelida shablon saqlashni taklif qiladi: dizayner kodga tegmasdan tahrirlaydi, ilova faqat shablon ID va o'zgaruvchilarni yuboradi. Narxi — shablon provayderga bog'lanib qoladi.
- React Email Next.js va TypeScript loyihalarida tez tarqalmoqda: xat ham komponent, test ham oddiy.
- Ko'p tilli xat: har til uchun alohida shablon yoki tarjima kalitlari — Backend'da ko'p tillilik darsida.
- Intervyuda: "Email'da XSS bormi?", "Matn versiyasi nega kerak?", "Email shablonlarini qanday test qilasiz?"
Xulosa
- Shablon bir marta kompilyatsiya qilinadi, har xat uchun ma'lumot bilan chaqiriladi.
{{x}}ekranlanadi,{{{x}}}— yo'q;strict: trueyo'q o'zgaruvchini xatoga aylantiradi.- Ekranlash
javascript:ni to'xtatmaydi: havolani server yasaydi yokinew URL+ ruxsat ro'yxati. - juice
<style>ni inline qiladi,@mediaesa<style>da qoladi; MJML Outlook'ga mos jadval yasaydi. - React Email 6 — yagona
react-emailpaketi;plainTextmatn versiyani beradi; render ~4 ms (birinchisi 474 ms). - Matn versiyasi va mavzu ekranlanmaydi; HTML — doim.
Keyingi dars: Email yetkazib berish (deliverability) — xat nega spam'ga tushadi: SPF, DKIM va DMARC'ni haqiqiy imzo va tekshiruv bilan ko'ramiz, List-Unsubscribe va bounce'larni o'rganamiz.
Manbalar
- Handlebars (ekranlash,
strict, partial, helper, prototip himoyasi): https://handlebarsjs.com/guide/ va https://handlebarsjs.com/api-reference/runtime-options.html - juice: https://github.com/Automattic/juice
- MJML 5: https://documentation.mjml.io/
- React Email (
react-emailpaketi,render,plainText): https://react.email/docs - OWASP — Server-Side Template Injection: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
- Kanon tajribasi (IlmHamroh, 2026-10-27): handlebars 4.7.10, mjml 5.4.1, juice 12.2.0, react-email 6.11.3, react 19.3.1, Node 24.21.0. Handlebars misollari,
eslatmarender'i, modul testi va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!