diff --git a/modules/identity/plugins/Favicon.php b/modules/identity/plugins/Favicon.php index fb34747..9475439 100644 --- a/modules/identity/plugins/Favicon.php +++ b/modules/identity/plugins/Favicon.php @@ -6,11 +6,16 @@ * site identity (config `tiger.site.favicon`, a media id), not SEO, but like the SEO head tags it * rides TigerZF's headLink registry so the layout renders it with no theme edit. A single high-res * square source is emitted as both `rel="icon"` (browsers downscale it for every tab size) and - * `rel="apple-touch-icon"` (iOS) — the modern, derivative-free approach. Fail-open: a missing or - * unresolvable favicon simply emits nothing (the browser falls back to /favicon.ico if present). + * `rel="apple-touch-icon"` (iOS) — the modern, derivative-free approach. When Site Identity sets no + * favicon, a baked-in Tiger paw default (DEFAULT_FAVICON — a puma base-theme asset whose `/_theme` + * symlink is always present for both admin and public) is emitted, so EVERY page has a favicon out of + * the box; a configured Site Identity favicon overrides it. Fail-open: any error emits nothing. */ class Identity_Plugin_Favicon extends Zend_Controller_Plugin_Abstract { + /** The stock Tiger paw, shipped in tiger-core (themes/puma/assets/img) + served at the always-present /_theme base. */ + const DEFAULT_FAVICON = '/_theme/img/tiger-favicon.png'; + /** Emit-once latch — the favicon is the same on every dispatch (incl. forwards). */ private static $_done = false; @@ -26,13 +31,12 @@ public function preDispatch(Zend_Controller_Request_Abstract $request) self::$_done = true; try { - $id = self::_config('site.favicon'); - if ($id === '') { - return; - } - $url = self::_mediaUrl($id, $request); + // A configured Site Identity favicon wins; otherwise fall back to the baked-in Tiger paw so + // every page (public / admin / auth) has a favicon by default. + $id = self::_config('site.favicon'); + $url = ($id !== '') ? self::_mediaUrl($id, $request) : ''; if ($url === '') { - return; + $url = self::DEFAULT_FAVICON; } $view = self::_view(); $view->headLink(['rel' => 'icon', 'href' => $url]); diff --git a/tests/Integration/Identity/FaviconPluginTest.php b/tests/Integration/Identity/FaviconPluginTest.php index 782b6f0..ac18211 100644 --- a/tests/Integration/Identity/FaviconPluginTest.php +++ b/tests/Integration/Identity/FaviconPluginTest.php @@ -17,9 +17,10 @@ /** * Identity_Plugin_Favicon — contributes the site favicon (config `tiger.site.favicon`, a media id) to - * the head via TigerZF's `headLink` registry, as both `rel=icon` and `rel=apple-touch-icon`. Fail-open: - * an unset or unresolvable favicon emits nothing. Wave-4 coverage: no config → silent, an unresolvable - * id → silent, and a real media id → two head links pointing at the resolved media URL. + * the head via TigerZF's `headLink` registry, as both `rel=icon` and `rel=apple-touch-icon`. When no + * Site Identity favicon is set (or it's unresolvable), a baked-in Tiger paw default is emitted so every + * page has one; a configured favicon overrides it. Coverage: no config → the default paw, an + * unresolvable id → the default paw, and a real media id → two links pointing at the resolved media URL. * * The plugin has a process-wide emit-once latch (`$_done`); each test resets it via reflection. */ @@ -66,19 +67,21 @@ private function dispatch(): void } #[Test] - public function emits_nothing_when_no_favicon_is_configured(): void + public function emits_the_default_paw_when_no_favicon_is_configured(): void { $this->faviconConfig(''); $this->dispatch(); - $this->assertSame('', trim($this->headLinks()), 'no config → no head links'); + $out = $this->headLinks(); + $this->assertStringContainsString('rel="icon"', $out, 'no config → the baked-in default favicon'); + $this->assertStringContainsString(Identity_Plugin_Favicon::DEFAULT_FAVICON, $out, 'points at the stock Tiger paw'); } #[Test] - public function emits_nothing_for_an_unresolvable_media_id(): void + public function falls_back_to_the_default_paw_for_an_unresolvable_media_id(): void { $this->faviconConfig('deadbeef-0000-7000-8000-000000000000'); // no such media row $this->dispatch(); - $this->assertSame('', trim($this->headLinks()), 'unresolvable id → fail-open, nothing emitted'); + $this->assertStringContainsString(Identity_Plugin_Favicon::DEFAULT_FAVICON, $this->headLinks(), 'unresolvable id → fail-safe to the default paw'); } #[Test] diff --git a/themes/puma/assets/img/tiger-favicon.png b/themes/puma/assets/img/tiger-favicon.png new file mode 100644 index 0000000..0d50564 Binary files /dev/null and b/themes/puma/assets/img/tiger-favicon.png differ diff --git a/themes/puma/layouts/scripts/admin.phtml b/themes/puma/layouts/scripts/admin.phtml index 0de083d..4d0cbf5 100644 --- a/themes/puma/layouts/scripts/admin.phtml +++ b/themes/puma/layouts/scripts/admin.phtml @@ -67,6 +67,7 @@ $_hasAgent = class_exists('Tiger_Agent') && Tiger_Agent::isAvailable(); codeInject('head') ?> +headLink() ?>s — chiefly the site favicon (Identity_Plugin_Favicon) */ ?> pageHead ?? '' ?>/styles a view registered via $this->pageStyle() */ ?> diff --git a/themes/puma/layouts/scripts/auth.phtml b/themes/puma/layouts/scripts/auth.phtml index c71fb9e..8061213 100644 --- a/themes/puma/layouts/scripts/auth.phtml +++ b/themes/puma/layouts/scripts/auth.phtml @@ -33,6 +33,7 @@ $_lang = defined('LANG') ? LANG : 'en'; +headLink() ?>s — chiefly the site favicon (Identity_Plugin_Favicon) */ ?> pageHead ?? '' ?>/styles a view registered via $this->pageStyle() */ ?>