Skip to content

Fix - Kirki downloading Google Fonts on every front-end request - #116

Merged
iamprazol merged 5 commits into
developfrom
fix/88-kirki-font-download-blocking
Sep 30, 2026
Merged

iamprazol merged 5 commits into
developfrom
fix/88-kirki-font-download-blocking

Conversation

@rajatgautam755421

Copy link
Copy Markdown

Closes themegrill/flash-pro#88

Summary

Kirki's bundled font module downloads Google Fonts locally so visitors never contact Google directly, but it does that download inline while a real visitor's page is rendering, and never remembers a failed attempt. Traced the exact mechanism in the bundled Kirki source (Webfonts/Downloader.php, Webfonts/Embed.php):

  • Normal case: the first visitor after a font change waits ~2-4s while Kirki fetches the files. Fine once, since get_option( 'kirki_downloaded_font_files' ) then marks them done.
  • Broken case: if wp-content/fonts isn't writable, that option key is never set (the write fails), so every single subsequent request re-attempts the identical failed download — 6+ seconds and a batch of PHP warnings, per visitor, forever, on affected hosts.

Fix

New inc/kirki-local-fonts.php, three small hooks, nothing in the vendored Kirki code touched:

  • kirki_enqueue_google_fonts filter empties the font list (skip download attempt entirely, fall back to system fonts) whenever the fonts directory — or its parent, if not created yet — isn't writable. This is a local filesystem check, not a network call, so it's cheap on every request and directly kills the "retry forever" failure mode.
  • customize_save_after schedules a one-off WP-Cron tick a few seconds later that pings the homepage (wp_remote_get), so the first-ever download after a real font change happens in the background instead of on a real visitor's request. Reuses Kirki's own existing front-end code path rather than reimplementing font generation.
  • Dropped the block editor's own direct request to fonts.googleapis.com (flash_block_editor_styles()) — the one place in this flow that was hitting Google directly regardless, and removing it is one line.

Testing

Isolated test of flash_kirki_skip_fonts_when_unwritable() and the prewarm-scheduling logic (WP functions stubbed, no real WP needed): fonts-dir-not-yet-created+writable, not-yet-created+unwritable, exists+unwritable, and writable-with-a-duplicate-save (schedules exactly once) all behave as intended.

🤖 Generated with Claude Code

Kirki's bundled webfont module tries to save Google Fonts locally so
visitors never contact Google directly, but it does that download
inline while a real visitor's page is rendering, and doesn't remember
a failed attempt: if wp-content/fonts isn't writable, every single
request re-attempts the same failed download (6+ seconds and a batch
of PHP warnings, per visitor, forever).

- kirki_enqueue_google_fonts is filtered to skip local generation
  entirely when the fonts directory (or its parent, if not created
  yet) isn't writable, so those installs get a fast page with a
  system-font fallback instead of a slow one with repeated failed
  downloads.
- On a writable install, the first-ever download after a Customizer
  save no longer happens on a real visitor's request: a background
  WP-Cron tick (scheduled a few seconds after save) warms the local
  files instead, via Kirki's own normal front-end code path.
- Removed the block editor's direct request to fonts.googleapis.com;
  it's the one request in this whole flow the front end already
  avoids by design, and dropping it is a one-line fix.

Verified: an isolated test of the writability filter and the
prewarm-scheduling logic covers "not yet created", "exists but
unwritable", "writable" and "already scheduled" cases.

See #88.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

QA suite — refused, no regression spec

This PR changes product source but adds no spec, so the suite was
refused before booting WordPress — running it just to report the same
thing at the end costs runner minutes for nothing. Run
/claudegrill:verify-fix locally and let write-spec add the guard
to this branch, then push again.

Source files changed with no matching spec
functions.php
inc/kirki-local-fonts.php

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Five moderate issues remain unresolved.

Review effort: Lite
Findings: 3 Medium severity

Open (3)
What changed in this PR

This PR defers Kirki Google Font downloads, handles unwritable font storage, and removes direct block-editor Google Fonts requests.

Changes:

  • Adds filesystem checks and background WP-Cron prewarming.
  • Removes direct editor font loading.
  • Loads the new font-handling helper.

Review findings:

  • Moderate (4 votes): Prewarming runs after every Customizer save instead of only font changes.
  • Moderate (4 votes): Loopback requests disable TLS verification.
  • Moderate (3 votes): Existing files at the fonts path are treated as valid directories.
  • Moderate (1 vote): Failed loopbacks are consumed without retry.
  • Moderate (1 vote): Programmatic font migrations do not trigger prewarming.
File Description
inc/​kirki-local-fonts.php Adds font download safeguards and background prewarming.
functions.php Removes direct editor font loading and includes the helper.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread inc/kirki-local-fonts.php Outdated
function flash_kirki_local_fonts_writable() {
$dir = WP_CONTENT_DIR . '/fonts';

return file_exists( $dir ) ? wp_is_writable( $dir ) : wp_is_writable( WP_CONTENT_DIR );
Comment thread inc/kirki-local-fonts.php
wp_schedule_single_event( time() + 5, 'flash_kirki_prewarm_fonts' );
}
}
add_action( 'customize_save_after', 'flash_kirki_schedule_font_prewarm' );
Comment thread inc/kirki-local-fonts.php
add_action( 'customize_save_after', 'flash_kirki_schedule_font_prewarm' );

function flash_kirki_prewarm_fonts() {
wp_remote_get( home_url( '/' ), array( 'timeout' => 15, 'sslverify' => false ) );
rajatgautam755421 and others added 3 commits September 28, 2026 15:20
Same corrections as themegrill/flash-pro's matching commit:

1. The writability check discarded fonts that were already successfully
   downloaded before the directory became read-only (e.g. "build with
   write access, deploy read-only" hosting). Read access and write
   access are different things; a site with existing local files should
   keep serving them regardless of current writability. The gate now
   only skips generation when nothing has ever been cached AND the
   directory is unwritable -- the actual severe case from #88 -- not
   whenever it's merely unwritable right now.

2. Reverted removing the block editor's Google Fonts request. Checked
   whether Kirki's own async webfont loader covers the block editor's
   preview area (which would have made the removal safe) and it
   doesn't: Async.php only enqueues its loader when $wp_customize or
   is_customize_preview() is true, neither of which is true on the
   block editor screen. Removing that line would have silently broken
   the heading-font preview in the editor with nothing replacing it --
   a real regression for something the issue itself rates as a
   non-functional consistency nicety, not worth that trade-off.

Verified with an expanded isolated test: the "something was already
cached before it became unwritable" case now correctly keeps serving
fonts instead of discarding them, on top of the existing four cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wp_schedule_single_event() only queues an event; WordPress's default
pseudo-cron only checks for due events on a subsequent front-end
request, so on a low-traffic site the prewarm could sit unfired for
an unbounded amount of time. Schedule the event for "now" instead of
5 seconds out, and call WordPress's own spawn_cron() right after so
the loopback dispatch is triggered immediately rather than waited
for. spawn_cron() is non-blocking (fire-and-forget, ~0.01s to open
the socket), so this adds no measurable delay to the Customizer save
request itself. If spawn_cron() is rate-limited by WP_CRON_LOCK_TIMEOUT,
the already-persisted event still fires on the next normal cron check,
same as before - so this is a strict improvement with no new failure
mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…iles

Live Playwright/WP-CLI reproduction of issue #88's unwritable-directory
case (chmod-equivalent via icacls on Windows) exposed a real regression:
flash_kirki_local_fonts_ready() treated a non-empty kirki_downloaded_font_files
option as proof that cached fonts exist, but that option lives in the
database and can outlive the actual files - e.g. a container redeploy
resets the filesystem while the DB persists. With a stale option and an
empty unwritable directory, the gate said "ready", so Kirki still hit the
same synchronous download-and-fail loop on every request that issue #88
reports (confirmed live: 500-file every request, identical PHP warnings
in the log, until this fix).

Replaced the option check with a direct glob() for physical files under
the fonts directory - the only source of truth that can't go stale
independently of the filesystem it's describing. Re-verified live: same
unwritable+stale-option scenario now resolves in ~1-2s with zero
warnings, and the original regression guard (unwritable dir that still
has genuinely-cached files on disk) still returns ready, confirmed with
an actual cached file on a write-denied directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@deepench

Copy link
Copy Markdown
Contributor

kirki_enqueue_google_fonts is also used by Async.php, which loads fonts directly from Google's CDN using webfontloader.js in the WordPress Editor and Customizer. This path doesn't touch the server/filesystem, so it isn't affected by the original issue.

This fix also blocks those fonts when the directory is unwritable, though. Verified on wp-admin/post-new.php: baseline loads webfontloader.js and Montserrat, but with this filter returning [], the script never enqueues and the editor preview breaks.

Could this be scoped to Embed.php's download path instead of the shared filter?

Same cached-font gap as the flash-pro version too: flash_kirki_local_fonts_ready() treats the directory as ready if it finds any cached font file, so a site with one cached font but an unwritable dir will still retry-and-fail forever for any newly selected font.

kirki_enqueue_google_fonts is used by both Embed.php (the real, front-
end-only download path this fix targets) and Async.php (webfontloader.js,
loaded on admin/customize-preview pages - never touches the filesystem).
Emptying the font list whenever the fonts dir was unwritable blocked the
Async path too, breaking font previews in the block editor and Customizer
on exactly the hosts this fix was meant to help. Now a no-op whenever
is_admin() or is_customize_preview() is true, mirroring Kirki's own
Webfonts::get_method() gate between the two paths.

Also scoped the readiness check to the specific font(s) being requested
instead of "any font is cached anywhere" - a host with one font already
cached but an unwritable directory would otherwise still attempt (and
retry-loop on) every newly selected font.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rajatgautam755421

Copy link
Copy Markdown
Author

@deepench Same fix applied here, same commit content (cae2085) — filter no-ops on admin/customize-preview, and the readiness check is now scoped to the specific requested font rather than any cached font. Verified against both original and fixed code.

@deepench deepench left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@iamprazol
iamprazol merged commit cd89ee6 into develop Sep 30, 2026
1 check failed
@iamprazol
iamprazol deleted the fix/88-kirki-font-download-blocking branch September 30, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants