Efficiently Serving Custom Web Fonts

Note 2009-10-07: Paul Irish published a Bulletproof @font-face syntax, which is a much better way to specify cross-browser downloadable fonts. His technique deprecates what I outlined below. Good work, Paul!

Update 2008-09: Removed superfluous “+” from last RewriteRule.

Update: Acknowledged that Ben was pointing to a solution by John Allsopp.

Ben Galbraith at Ajaxian recently posted again about using custom fonts. He points to a cross-browser solution by John Allsopp which serves EOT fonts and TTF fonts to both IE and non-IE browsers respectively. The idea is to include two @font-face rules with different font-family names (such as "Foo" as the name for the EOT, and "Bar" as the name for the TTF), and then in order to apply these fonts to text in the page you provide both names for the font-family property, such as "Foo, Bar, sans-serif". While this does work, it is also problematic: the client potentially downloads both font files and then throws away the one it doesn’t understand; this problem of wasted HTTP request is intensified by the fact that font files are often quite big (note that Safari appears to skip downloading font files that end in “.eot”, but MSIE doesn’t skip files that end in “.ttf”). There is a more efficient way to serve these font.

In projects I’ve worked on at Shepherd Interactive what I have done is use conditional comments to serve one @font-face rule for IE and another for everyone else. It is also very helpful to include the official name of the file in the CSS3 Web Fonts local() value in the case that the font is already locally installed (also include an additional src:url(...) declaration first in case local() isn’t understood, but if it is then the following src:local(...) declaration will override the previous one).

For example, given a font named “FooBarBaz”, the following is very best way I can conceive to serve web fonts:

<!--[if IE]>
<style type="text/css">
/* Font rule for MSIE referencing EOT font */
@font-face {
    font-family: "FooBarBaz";
    src: url("/fonts/foobarbaz.eot");
    /* above included in case local() isn't *
     * understood, which it isn't by IE7)   */
    src: local("FooBarBaz"), url("/fonts/foobarbaz.eot");
}
</style>
<![endif]-->

<!--[if !IE]>-->
<style type="text/css">
/* Font rule for other browsers referencing TTF font */
@font-face {
    font-family: "FooBarBaz";
    src: url("/fonts/foobarbaz.ttf");
    /* above included in case local() isn't *
     * understood, but Safari 3.1 does      */
    src: local("FooBarBaz"), url("/fonts/foobarbaz.ttf");
}
</style>
<!--<![endif]-->

Of course this assumes that only IE will ever support EOT fonts.

Another way which would remove the need for conditional comments is to use some form of content negotiation (perhaps with mod_rewrite) to serve the appropriate font file based on the user agent (IE or not IE). For example in a CSS file:

@font-face {
    font-family: "FooBarBaz";
    src: url("/fonts/foobarbaz.font");
    src: local("FooBarBaz"), url("/fonts/foobarbaz.font");
}

And then in an .htaccess file:

#Make sure that font files are cached
ExpiresActive On
<FilesMatch "\.(eot|ttf|otf)$">
    ExpiresDefault "access plus 10 years"
</filesMatch>

#Serve "proper" MIME types (they aren't standardized yet)
AddType application/vnd.ms-fontobject .eot
AddType font/ttf .ttf
AddType font/otf .otf

RewriteEngine On

#Serve EOT font if MSIE
RewriteCond %{HTTP_USER_AGENT}   MSIE
RewriteCond %{HTTP_USER_AGENT}   !Opera
RewriteRule (.+)\.font$          $1.eot       [L]

#Serve OTF file if it exists
RewriteCond %{REQUEST_FILENAME}  (.+?)([^/]+)\.font$
RewriteCond %1%2.otf             -f
RewriteRule .+                   %2.otf       [L]

#Serve TTF file otherwise
RewriteRule (.+)\.font$          $1.ttf       [L]

This, however, depends on the client sending a truthful User Agent string.

Any suggestions for further improvements?

7 thoughts on “Efficiently Serving Custom Web Fonts

  1. @Brad:
    Note the additional “-->” after the opening of the non-IE conditional comment and the “<!--” before the closing: these ensure that non-IE browsers do see the contents of the conditional comment while at the same time it remains invisible to IE.

  2. src: url(“/fonts/foobarbaz.font”);
    src: local(“FooBarBaz”), url(“/fonts/foobarbaz.font”);

    is that really necessary? surely the 2nd line alone is sufficient? what am i missing?

  3. @anonymous:
    Some browsers (IE, if I remember right) don’t recognize the local() value for the src property. Those browsers would ignore that entire property declaration, and this is why there is a second property defined directly before: unsupporting browsers ignore the second property, and supporting browsers override the first with the second.

  4. In terms of loading, does the site have to request to EOT or TTF file each time from the server, or can it be cached on the client?

Leave a Reply