Sitelet https://github.com/solinter/font-proxy-cache
Skip to content

About

Font Proxy Cache Wordpress Plugin

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Font Proxy Cache

A WordPress plugin (WordPress 6.2+, PHP 7.4+) that serves external font dependencies from the site itself. The browser no longer contacts fonts.googleapis.com, fonts.gstatic.com or use.fontawesome.com. WordPress fetches those files once, keeps them as content-addressed objects in the persistent object cache (Redis) and serves them from /font-proxy/.... Nothing is written to disk.

<link href="/sitelet?url=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DMontserrat%3Aregular%26amp%3Bver%3D7.1">
        ↓
<link href="/sitelet?url=https%3A%2F%2Fexample.com%2Ffont-proxy%2F%26lt%3Bsig%26gt%3B%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DMontserrat%3Aregular">

Installation

  1. Copy the font-proxy-cache folder to wp-content/plugins/.
  2. Make sure a persistent object cache is enabled (see Requirements).
  3. Activate Font Proxy Cache under Plugins, or run wp plugin activate font-proxy-cache.

No configuration is needed. Status and cache management are under Tools → Font Proxy Cache.

Requirements

  • A connected persistent object cache. In practice that means the Redis Object Cache plugin with its object-cache.php drop-in enabled (Settings → Redis → Enable Object Cache, or wp redis enable). Other persistent object cache drop-ins should also work, but only Redis Object Cache has been tested.
  • Activation is refused without it. If the object cache later becomes unavailable, the plugin turns itself off and shows an admin notice. "Unavailable" covers three cases: the drop-in is removed, Redis is unreachable, or the group is listed in WP_REDIS_IGNORED_GROUPS.
  • If Redis goes down, the whole site is down, unless graceful mode is on. With WP_REDIS_GRACEFUL unset, the Redis Object Cache drop-in stops the whole site with "Error establishing a Redis connection" whenever Redis is unreachable, before any plugin loads. Define WP_REDIS_GRACEFUL as true in wp-config.php (or via WORDPRESS_CONFIG_EXTRA in the official WordPress Docker image) to keep the site running without Redis. This plugin then switches itself off.
  • While off:
    • pages keep their original CDN URLs
    • /font-proxy/ URLs still referenced by cached pages answer with a 302 to the CDN, with X-Font-Proxy-Cache-Error: font_proxy_cache_unavailable
    • nothing is cached anywhere

How it works

  1. Rewriting page output

    • style_loader_src rewrites enqueued stylesheets.
    • An output buffer rewrites hard-coded <link> tags and <style> blocks, including @import and @font-face src:url(/sitelet?url=https%3A%2F%2Fgithub.com%2Fsolinter%2F...). Scripts and post content are left untouched.
    • wp_preload_resources rewrites preloads.
    • integrity attributes are removed from rewritten tags. The proxied CSS has its dependency URLs rewritten, so the upstream SRI hash would no longer match.
    • dns-prefetch and preconnect hints for the proxied hosts are removed, both WP-generated and hard-coded ones.
  2. Proxy endpoint (/font-proxy/<signature>/<host>/<path>?<query>). It runs on plugins_loaded, before the theme loads.

    • The signature is an HMAC of the canonical upstream URL. Only URLs generated by this site can be fetched, so the endpoint is not an open proxy.
    • On a miss, the upstream file is fetched with a modern browser UA, so Google returns woff2 with unicode-range subsets. Its content type is validated: CSS, woff2, woff, ttf, otf, eot and svg are accepted, and HTML error pages are rejected.
    • CSS is rewritten before caching. Every url() and @import it contains is resolved against the upstream URL. That covers absolute gstatic URLs and Font Awesome's relative ../webfonts/.... Resolved references are pointed at the proxy.
    • If upstream is down, a stale copy is served. If nothing is cached yet, the browser gets a 302 to the CDN, so the page keeps working.
  3. Storage (object cache group font_proxy_cache)

    Key Content
    <gen>:obj:<sha256> File body, named by the hash of its content
    <gen>:ref:<sha256(url)> One record per canonical URL, read on every proxy request
    <gen>:index Objects (registered when written), refs and upstream failures, for the Tools page, garbage collection and purge. Updated under <gen>:lock

    <gen> is the purge generation. It is stored in the autoloaded font_proxy_cache_generation option, next to the signing secret font_proxy_cache_secret.

    • Purge switches to a new generation, so every server stops using the old keys at once. The indexed old keys are deleted right away to free memory.
    • Lock expiry is kept in the lock's value. With WP_REDIS_MAXTTL=0, Redis Object Cache makes every TTL permanent. A lock left behind by a crashed request is taken over once its deadline passes.
    • Replaced objects are freed at once. When a ref stops pointing to an object, the object is deleted immediately. An object no ref points to yet, for example from an interrupted store, is deleted after 5 minutes. A purge deletes all of them.
    • A hit costs two Redis GETs (ref and object).
    • Evicted keys are fetched again automatically. If Redis evicts a ref or an object (e.g. maxmemory-policy allkeys-lru), the file is fetched again on its next request.
    • Size: a typical setup (one Google Fonts family and Font Awesome 6) uses a few hundred KB to about 1.5 MB. Font Awesome's TTF fallbacks are cached only if a browser requests them.

No duplicates

The same file is never cached under two identifiers:

  • Canonical URL identity. Every spelling of a URL maps to one ref and one proxy URL:

    • //, http: and https:
    • host case and default ports
    • &#038; and &amp;
    • dot segments (css/../webfonts)
    • percent-encoding (%3A vs :, + vs %20)
    • query order
    • the WordPress ver argument, which is dropped
    • fragments such as ?#iefix

    So a font referenced from two stylesheets resolves to the same URL, and the browser caches it once too.

  • Content addressing. Different URLs that return identical bytes share a single object.

Cache lifetime

Type Upstream refresh Browser Cache-Control
Fonts never (versioned URLs) public, max-age=31536000, immutable
CSS weekly, conditional (ETag / Last-Modified) public, max-age=86400, stale-while-revalidate=604800

The ETag is the object's content hash, and If-None-Match gets a 304 response. The X-Font-Proxy-Cache header shows the cache status: HIT, MISS, REVALIDATED, REFRESHED, REBUILT, STALE or BYPASS.

Management

  • Tools → Font Proxy Cache shows the object-cache status, stats, cached URLs and upstream failures. It has Purge cache and Remove orphaned objects actions. It also shows when the page was rendered and by which server. If it shows an old time, it came from a cache in front of WordPress, not from the server.

  • WP-CLI:

    wp font-proxy-cache status | list | purge | gc
    wp font-proxy-cache warm <url>...
    wp font-proxy-cache url <url>
    

Troubleshooting

When a file cannot be served locally, the proxy answers with a 302 to the original CDN. The page keeps working, but that file and its dependencies are loaded from the CDN.

  • Response headers. Look for X-Font-Proxy-Cache: BYPASS and X-Font-Proxy-Cache-Error: <code> [<http status>]:
    • font_proxy_cache_unavailable: no connected object cache
    • font_proxy_cache_http 403: upstream refused the request
    • font_proxy_cache_storage: writing to Redis failed
    • http_request_failed: network error
  • Tools page. Tools → Font Proxy Cache lists the upstream failures with the full message. A Cloudflare bot block is reported as cf-mitigated: challenge.
  • Retries. Failed fetches are retried every 5 minutes. Purge cache retries on the next request.

Filters

Register these from a plugin or mu-plugin, not from the theme. The endpoint answers before the theme is loaded.

Filter Default
font_proxy_cache_hosts fonts.googleapis.com, fonts.gstatic.com, use.fontawesome.com
font_proxy_cache_ignored_query_args ['ver']
font_proxy_cache_is_proxiable CSS/font extensions or extensionless paths
font_proxy_cache_base_url home_url('/sitelet?url=https%3A%2F%2Fgithub.com%2Fsolinter%2Ffont-proxy%2F') (e.g. a CDN in front of the site)
font_proxy_cache_ttl CSS 1 week, fonts 0 (never)
font_proxy_cache_cache_control see table above
font_proxy_cache_user_agent, font_proxy_cache_timeout modern Chrome UA, 10 s
font_proxy_cache_enabled true (turns off rewriting only; the endpoint keeps serving)

Define FONT_PROXY_CACHE_SECRET in wp-config.php to pin the signing secret. Otherwise a secret is generated once and stored in the font_proxy_cache_secret option.

Server notes

/font-proxy/... must reach WordPress's index.php. The standard WordPress .htaccess does this on Apache. On nginx, make sure no location ~* \.(css|woff2)$ block answers these requests with a 404 before try_files ... /index.php.

About

Font Proxy Cache Wordpress Plugin

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages