Persian typography on the web
Persian typography does not begin with choosing a beautiful typeface; it begins with a technical condition: the font must carry the letter-shaping tables, or the letters will not join and the text becomes unreadable. After that, four decisions remain that have no counterpart in Latin typography: the weight of the font file, where the half-space goes, the form of the numerals, and the Arabic letters that are wrong in Persian.
- Lesson 5 of 12
- Intermediate
- Free, no signup
A Persian type audit sheet, before you hand the work over
Must be there
- The font carries init, medi and fina, and the letters genuinely join
- Persian numerals live inside the font itself and are not borrowed from another
- At least two real weights exist, so the browser synthesises no bold
- Line spacing is opened up for Persian, beyond what suffices for Latin
- font-display is optional, so a font switch never shifts the layout
Must not be there
- An Arabic letter in place of a Persian one, even once in a heading
- Fused words, that is a half-space position left empty rather than filled
- The tatweel character used to fill a width or match two lines
- Justification to both margins, which a browser does by opening word spaces
- Latin and Persian digits mixed inside one table or one sentence
This list is about the Persian script and does not replace a general typography audit: scale, line length and hierarchy still need checking separately.
Last checked: Facts and tool names in this lesson are re-checked against their sources on this date.
What does Persian have that Latin does not?
The core difference is that in Persian the shape of a letter depends on its neighbours. One letter can have up to four forms: isolated, initial, medial and final. The letter beh takes three different shapes on screen in barf, abr and ketab, and in the computer memory it is one code point.
That single sentence is the source of most of the technical trouble in Persian typography. A Latin font has a simple mapping from code point to glyph; a Persian font needs a layer of rules that decides which glyph gets drawn in each position. The next section is about that layer.
Three other differences bear directly on design. First, Persian has no capital letters, so one of the most common hierarchy levers in Latin does not exist here at all, and weight, size and spacing take its place. Second, the dots carry meaning and four letters differ by dots alone, which makes a typeface that draws them small or fused dangerous at small sizes. Third, a large share of the letters drop below the baseline, which is why Persian text looks crowded at the line spacing that suffices for Latin.
And one difference that gets mentioned less and shows up in layout: Persian letter widths vary far more than Latin ones. In the typeface this site uses, the isolated forms of the alphabet range from 209 to 1223 units of a 1000-unit em. Two Persian words with the same number of letters can therefore have completely different widths, and any estimate based on a character count is less precise in Persian than in Latin.
What must a Persian web font carry?
The short answer: the shaping tables. In the OpenType format these are called features and each carries a four-letter tag. The three that are vital for Persian are init, medi and fina: the initial, medial and final forms. Beside them sits rlig, which builds the required ligatures such as lam-alef, and ccmp, which performs the necessary composition and decomposition.
When those tables are missing the browser raises no error. It sets the text, but it does not join the letters, and the result is something any Persian reader recognises as wrong at a glance without being able to say why. That is exactly what happens when you put a Latin typeface on Persian text, or when you take a subset font whose features have been stripped.
The typeface this page is set in carries these features inside its own file: ccmp, init, medi, fina, rlig, calt, locl, plus a few optional ones. We read that from the file itself with the fontTools library rather than from the foundry description. If you are evaluating a typeface, that check is repeatable and takes five minutes.
Three further conditions to check before choosing. First, real weights; a family with only one weight means the browser has to synthesise a bold for emphasis, and in Persian the result is poor. Second, Persian numerals inside the font itself, which is section five of this lesson. Third, a web licence, which on a client project is a legal matter and not a technical detail.
How one Persian letter finds its final shape
Each step is a table inside the font file. Remove one and the steps after it do not run either.
-
1
One code point is stored
In memory the letter is one number, regardless of whether it starts or ends the word.
-
2
ccmp: composition and decomposition
The letter and its marks are put into a form the later rules can work on.
-
3
init, medi and fina
The neighbours decide which of the four shapes is drawn. Without this table the letters stay separate.
-
4
rlig: the required ligatures
Lam and alef become one glyph. This ligature is not optional, and without it the text looks wrong.
-
5
The glyph on screen
What the reader sees. One code point at the start of the path, several possible shapes at its end.
This order is for Persian and Arabic. Other languages in the same script have their own rules, and a font can separate them with the locl feature.
Making the font lighter without breaking it
A Persian typeface is larger than its Latin equivalent, because it holds several glyphs per letter rather than one. The standard way to lighten it is subsetting: removing the parts of the font this site will never need.
The real numbers from this site, read from the files themselves: the full font at regular weight is 42,708 bytes, about 41.7 KB, with 726 glyphs and 652 code points. The subset version is 25,272 bytes, about 24.7 KB, with 599 glyphs and 527 code points. The three weights this site actually loads come to 76,364 bytes together, about 74.6 KB.
Now what was removed, which is where subsetting parts company with guessing. 125 code points went, and 107 of them are accented Latin letters, from A with macron to C with caron: the alphabets of languages this site does not write in. Fifteen more are combining marks. Not one Persian, Arabic or Turkish letter was dropped, and the Turkish-specific letters are all still there, because this site is written in four languages and all of them share one font.
And the trap that wrecks this job more than anything else: subsetting on the basis of "the characters used on the page". The day a writer types a word containing a letter outside that list, that letter renders in a different font. The second trap is worse and specific to the Persian script: if you configure the subsetter so that it does not keep layout features, the init, medi and fina tables go with them and the letters fall apart. In its default mode pyftsubset keeps a set of these features; the danger arrives when someone rewrites that list by hand around Latin needs.
One more setting that matters as much as the size: font-display. With swap the browser shows a fallback first and switches mid-page, and the layout shifts at that moment. With optional, if the font does not arrive in time the fallback stays for that whole visit and no switch happens at all. For Persian, whose letter widths differ sharply from the fallback, that difference in layout shift is large.
Where exactly does the half-space belong?
The half-space is an invisible character, U+200C, that keeps two parts of a word apart without inserting a full space. In standard Persian typography it is correct, and where it goes follows a clear rule.
The rule is this: a half-space is needed only where the last letter of the stem joins to the next one. If the stem ends in one of alef, dal, zal, re, ze, zhe or vav, that letter does not join forward at all and already takes its separate shape, so the solid form is correct from the start and needs no half-space. That is why کارها, بارگذاری and بالاتر are right and must not be split, whereas the standard present-tense form, میشود, carries a half-space between the prefix and the rest.
And the mistake that is worse than either: deleting the half-space instead of replacing it. Remove that character and put nothing in its place, and the two words fuse into میشود, which is wrong in standard Persian and hard to read at small sizes as well. If for any reason you do not want an invisible character in your text, put an ordinary space where it was, not nothing.
Let us state this site own position plainly, because the reader sees it on this very page: in our published text we use an ordinary space instead of the half-space, so می شود. That is an editorial choice and not a claim about linguistic correctness; the reason is that an invisible character in text behaves unpredictably in search and in text tooling, and we prefer our text to contain nothing that cannot be seen. If you decide otherwise and keep the half-space, that is entirely defensible. What is defensible under no circumstances is the fused form.
A practical note for typing: on the standard Persian keyboard layout the half-space is shift plus space. If your content team does not know that, your text will be a mixture of all three forms, and that is the worst possible outcome, because your own site search will stop finding the words.
Does the last letter of the stem join to the next one?
This is where the half-space belongs
- mi + shavad
- khaneh + ha
- rotbeh + bandi
- neveshteh + am
The solid form is correct from the start
- karha
- bargozari
- balatar
- keshvarha
Persian numerals, from a typographic angle
There are three families of digit that resemble one another and are not the same: Latin 0 to 9, Persian numerals, and the Arabic-Indic numerals used in Arabic text. Persian and Arabic-Indic differ in four of the ten shapes and share the rest, and that resemblance is what makes confusing them easy.
Typographically two things matter. First, your font must genuinely contain the Persian numerals; if it does not, the browser borrows them from another font and, in the middle of a sentence, the numbers suddenly acquire a different height and weight. You can see this in thirty seconds: write a Persian number beside your text and look closely.
Second, digit width. In tables and anywhere numbers stack vertically, the digits must share one width or the column falls apart. Most Persian typefaces respect this, though not all, and the way to check is to write several numbers with different digits under one another and see whether their right edge stays a straight line.
This site typeface contains all three families, Latin, Persian and Arabic-Indic, and that is what lets the Persian and the Arabic versions of the site run on one font file while each keeps its own correct numerals.
And one boundary this lesson deliberately does not cross: Persian numerals in forms are a separate and technical problem, not a typographic one. Standard validation patterns do not recognise Persian numerals as digits, and if you make no provision for it, a user who typed their number on a Persian keyboard gets an "invalid number" message. That argument, with its solution, sits in UI design principles.
The Arabic letters that are wrong in Persian
Persian and Arabic share one script and differ in a few letters, and that is why Persian text is full of Arabic letters without anyone noticing. Three matter most: Arabic ي in place of the Persian yeh, Arabic ك in place of the Persian kaf, and ة, which has no place in Persian at all and should be a heh.
Why does it matter, when on screen they look almost identical? Because to a computer they are not identical. Your own site search will not treat a word written with a Persian kaf and the same word written with an Arabic kaf as one, alphabetical sorting breaks, and a search engine sees two different strings. A page whose title is written with an Arabic yeh is not the same page for a query the user typed with a Persian yeh.
It has a typographic effect too: the final form of the Persian yeh and the Arabic yeh differ in most typefaces, because one carries two dots and the other does not. Mixed text looks inconsistent at the ends of words and nobody can say why.
The second family is the hamza forms. In standard Persian, words such as tasir, mabda and jozv are written without a hamza. And adding a hamza over the heh for the ezafe, that is writing خانۀ, is a form that has better replacements in present-day Persian: a space and a yeh, or nothing at all.
The origin of these letters is usually one specific place: the keyboard. Arabic layouts and some default mobile layouts produce Arabic letters. If you have a content team, settle this once in a meeting rather than page by page; the fast path in this lesson carries a command that finds all three families in a single run.
Stretching and fake bold: two things not to do
The first is stretching letters with the tatweel character, U+0640, which turns a word into something like کـــتاب. In calligraphy, elongation is a real and delicate tool with a defined place. In web text it is almost always used to fill a width or make two lines match, and that is exactly where it becomes a problem: the word is no longer the same string, search will not find it, a screen reader reads it differently, and when the text reaches a phone an elongation tuned for desktop becomes meaningless.
The second is fake bold. When a typeface has no bold weight and you ask for weight 700, the browser can thicken the letters itself. For Latin the result is tolerable; for Persian it is not, because machine thickening fills in the counters inside letters and fuses the dots into the body. The right answer is to have a real weight, and if you do not, build emphasis with size or colour rather than with a weight that does not exist.
And a third that is less often recognised as a mistake: justifying text to both margins. In Persian the temptation is stronger, because the Arabic script allows elongation and it looks as though you could justify without ugly gaps. But browsers do not do it by stretching letters, they do it by opening the spaces between words, and the result is the same white rivers. WCAG criterion 1.4.8 says plainly that text should not be justified.
The practical summary of all three: each is an attempt to solve a layout problem with a typographic tool. When a block of text looks wrong, the answer is usually to change the column width or the line spacing, not to manipulate the letters themselves.
The fast path, with AI
This job has two halves and the first needs no AI at all. Arabic letters, tatweel and Arabic-Indic digits are all specific characters, so one grep command finds every instance in seconds and misses none. The second half is the empty half-space positions, and there a text pattern alone is not enough, because knowing that karha is right while a fused rotbeh-bandi is wrong requires knowing the word itself. That is where a model genuinely does something a tool cannot.
- Put the text of your pages into one text file. On a WordPress site the Export tool, or even copying the text of a few important pages, is enough; you do not need to pull the whole database out.
- Run the grep command below on that file. Every line it prints contains at least one wrong character, and its line number comes with it. If nothing prints, this half of the job is done.
- Now hand the same text to a model with the second recipe. A stronger model works better here than a fast one, because the decision on each word is a linguistic judgement; our current pick is kept in the <a class="text-link" href="/ai/">AI section</a>.
- Confirm the model output word by word and do not run a global replace over it. This is the one step that must not be automated, because a wrong replace is exactly what turns karha into two words and breaks text that was already correct.
Copy-ready recipe
Step one, no AI. This command finds every Arabic letter, every tatweel, every hamza form and every Arabic-Indic digit in your file, with its line number:
grep -nP '[\x{064A}\x{0643}\x{0629}\x{0640}\x{0621}\x{0623}-\x{0625}\x{0654}\x{06C0}\x{0660}-\x{0669}]' FILE
Silence means clean. Any output is one case to be fixed by hand.
Step two, for the model:
This text is in Persian.
{text}
Find only the words where a half-space position has been left empty and two parts have fused together.
The rule you must apply: a half-space is needed only where the last letter of the stem joins to the next one. If the stem ends in alef, dal, zal, re, ze, zhe or vav, the solid form is correct and you must not split it. So karha, bargozari, balatar and keshvarha are all correct.
Give the answer as a list: the word you found, its correct form with an ordinary space, and the line or the sentence it appeared in.
If there is a word you are not sure about, put it in a separate section headed "not sure" and write why. Do not mix guesses with confident findings. Do not change the text and do not hand back a corrected text; give only the list.
Before you trust the output: Three boundaries this path does not cross. First, grep finds characters and not meaning: a word written with the right letters but the wrong spelling passes straight through the sieve. Second, models make mistakes in Persian too, especially with proper nouns and loanwords; that is why the recipe asks it to separate the uncertain cases, and that is the section you should read most carefully. And third, more important than either: never run the output of this job as a global replace over your site database. One wrong replace across pages that already rank costs more than any spelling error; apply the changes page by page, with a human reading each one.
AI in this kind of work
Our position here is blunter than in the other lessons on this path: for Persian type, put models in the role of text inspector and not image producer. Finding Arabic letters, spotting empty half-space positions and making numerals consistent are three things they genuinely do well. Generating an image with Persian text written in it is still something none of them does reliably, and the risk section says why.
Tools that actually help
- Claude The best fit for the second recipe, because it holds a long text in one piece and, when you ask it to separate the uncertain cases, it actually separates them rather than writing everything in one confident tone. Iran is on neither of Anthropic two supported-countries lists, which we read on Anthropic own page.
- Gemini Better when you have no text and only an image of the design: it reads the picture and tells you where letters have failed to join or where the numerals do not match the rest of the sentence. Google own page says the Gemini web app is available in more than two hundred and thirty countries and territories, and Iran is not on that list.
- fontTools Not AI, and the very tool the numbers in this lesson were measured with. It reads a font feature list and its code point list out of the file itself, and it is also what performs the subsetting. It is free, it runs on your own computer, and no file leaves your machine.
Where it backfires
The specific risk in this area is images. Image generation models have got somewhere with Latin text, but for Persian they give no reliable answer, and the evidence comes from the vendors themselves: none of them publishes a number for text inside an image, and each writes an adjective instead; one says "advanced", one says it "may still have issues", one calls its model "typography specialised". Three adjectives and zero measurements, and not one of them mentions Persian at all. We wrote that in the best AI for image generation table, and it is also why it never became a column there.
The practical consequence: do not generate a banner, a logo or any image with Persian text in it. Make the image without text and put the text over it in a design tool or in the HTML itself. That is the only route where the letters join correctly and the text stays searchable and selectable.
The second risk is more technical and concerns fake bold: if a model writes CSS for you and asks for a weight your font does not have, the browser synthesises a bold and in Persian the result is poor. The fix is to turn font-synthesis off explicitly in your own CSS, so that instead of a bad result you see the difference and add the real weight. To see how each of these tools can be paid for from Iran, see the buying guide.
Sources: Microsoft: OpenType layout feature tag registry fontTools: the subsetter Anthropic: supported countries Google: where the Gemini web app is available
Where this advice stops
This lesson is about Persian on the web and it has boundaries in several places. First, calligraphy: everything here concerns letters produced by a layout engine, and nastaliq and shekasteh are another world with their own rules that cannot be reconstructed in HTML. Second, mixing Persian and Latin in one sentence, which is a bidirectional problem whose answer lies in page layout rather than in the font; that argument sits in the UI design principles lesson. Third, print, where the unit, the colour space and the ability to adjust lines by hand are all different. And one boundary worth stating plainly: the font file numbers in this lesson are true only for this site typeface, and copying them into another project proves nothing. The measuring method is repeatable; the numbers themselves are not.
From our own work
We opened this site typeface with the fontTools library and put the two files side by side. The full regular weight holds 726 glyphs and 652 code points at 42,708 bytes; the subset holds 599 glyphs and 527 code points at 25,272 bytes. The difference is 125 code points, and when we printed the removed list, 107 of them were accented Latin letters and 15 were combining marks. Both files carry exactly the same feature set: aalt calt ccmp dlig fina init locl medi rlig salt ss01. That is, the subset left the joining tables untouched, and that is precisely what separates a correct subset from a broken one.
And something the same measurement revealed that we had not known beforehand: all three weights the site loads have identical coverage, 599 glyphs and 527 code points, and Turkish is fully inside them; the letters ı, İ, ş, ğ, ç, ö and ü are all still present. So the Turkish version of the site runs on the same file as the Persian and the Arabic, and for one font to serve four languages the only thing genuinely removable was the alphabets of languages this site never writes in.
Real follow-up questions
Is one font enough for all four languages or does each need its own?
If the font has the coverage, one is enough and is also better. A site that loads a separate typeface per language sends more files and gives its pages different visual characters. What you should do is check the coverage for all four languages before choosing; the method is in this lesson and takes no more than five minutes.
How do I know whether the font I have contains Persian numerals?
The simplest method is visual: write a Persian number in the middle of a Persian sentence and look closely. If the height or weight of the digits differs from the rest of the sentence, the font does not have them and the browser borrowed them from elsewhere. The more definitive route is reading the code point list out of the file with fontTools, the same tool the numbers in this lesson were measured with.
Should I keep the half-space on my site or replace it with an ordinary space?
Both choices are defensible and we picked the ordinary space ourselves, but what genuinely matters is consistency. A site where all three forms appear is the worst case, because internal search and user queries no longer line up. Make one decision, write it into your team style guide, and fix old pages only when you are visiting them for some other reason, never in one sweeping run.