Skip to content

Replace the wordmark favicon with Milo - #9

Merged
Vilos92 merged 3 commits into
mainfrom
milo-favicon
Jul 27, 2026
Merged

Vilos92 merged 3 commits into
mainfrom
milo-favicon

Conversation

@Vilos92

@Vilos92 Vilos92 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Replaces the green "Greg" wordmark favicon with Milo, and fills in the icon and social metadata that was missing around it.

The art

public/favicon.svg is Milo's rest pose. The live component in src/components/milo/markup.ts already renders exactly this pose before its cursor tracker starts, so the art is traced from there rather than redrawn. The standalone copy drops three things the live rig needs and a still image does not: the data-milo* hooks (valueless attributes, which are legal in HTML but invalid in an SVG file, since that is parsed as XML), the per-instance id namespacing, and the attribute-less <g> wrappers that existed only for the tracker to grab. The cleanup was verified render-identical to the raw component output at 16, 32, 180 and 512px.

The viewBox is cropped from the component's 220 units to 144. Milo's ears top out at y=-84 and his chin at y=56, so a 144-unit square centered on the head is the tightest crop that keeps him whole, and it happens to still contain all but ~5 units of whisker tip on each side. The component's own viewBox would have spent most of a 16px favicon on empty margin.

Files

File Purpose
favicon.svg What every current browser except Safari uses
favicon.ico 16/32/48, replaces the wordmark. Safari has no SVG favicon support, so this is a live path, not a legacy fallback
apple-touch-icon.png 180x180, opaque #f7f4ef plate with a 10% inset so iOS's corner mask does not clip the ears
icon-192.png, icon-512.png Referenced by the manifest, purpose: any
icon-maskable-512.png Padded and opaque, purpose: maskable, so Android's launcher mask crops background instead of Milo
site.webmanifest Android home screen, plus long-press shortcuts to Blog, Tools and Milo
og/default.png 1200x630 social card

Metadata

BaseHead.astro previously emitted a single <link rel="icon">. It now emits the modern set, plus light and dark theme-color sourced from the existing pageBg / pageBgDark tokens rather than hardcoded.

Two details worth flagging, both from research rather than habit:

  • The ICO carries sizes="32x32" and the SVG carries none. The widely-copied sizes="any" advice is superseded and has the opposite effect, making Chrome download both files.
  • The two unpadded icons deliberately declare no purpose, and the padded one is a separate file. Declaring maskable on unpadded art gets it clipped by Android's adaptive mask, and the spec warns against one file serving both purposes because it ends up wrong on one of them.

The maskable icon sizes Milo to 66% of the tile. That number is derived, not eyeballed: his ear tips sit at radius 0.604 x scale from the center, and the W3C safe zone is a circle of radius 40% of the icon, so 0.66 is the largest scale where nothing of him falls outside the guarantee on any launcher mask shape.

Social cards

BaseHead had an explicit "no fallback image" rule, so only blog posts (their hero) and the 3D-to-SVG tool shared with a picture. Everything else, including /milo itself, shared as a bare text blob. There is now a fallback to the Milo card. Since an image is always present, twitter:card is unconditionally summary_large_image and the six conditional tag expressions collapse to plain ones.

Two cases are worth calling out, both surfaced by the review rather than by me:

  • The alt has to follow whichever image won. blog/[...slug].astro always passes imageAlt={post.data.title} but heroImage is optional in the schema. My first cut keyed the alt off imageAlt alone, so a post with no hero would have shipped the Milo card labelled with the article title. The alt is now derived from the image that actually won.
  • An SVG hero is not a usable card. copyparty-tunnel.md uses /blog/copyparty.svg, and no major social platform renders SVG og:image. That post was sharing imageless before this PR and would have kept doing so, which is the exact thing this change exists to fix, so an SVG hero now falls through to the Milo card.

Verified against the built output, including a throwaway post with no hero:

Page og:image og:image:alt
/blog/vilos92-com/ (jpg hero) its own hero the post title
/tools/3d-to-svg/ its own card its own alt
/blog/copyparty-tunnel/ (svg hero) Milo Milo
post with no hero Milo Milo
/, /milo/, the rest Milo Milo

Notes

  • The manifest still omits id, which is inert without an install flow, and uses display: minimal-ui rather than standalone so an installed copy keeps its back button on what is a multi-page content site. It also omits screenshots, which would unlock Chrome's richer install dialog but needs real captures and only pays off at install time.
  • The fallback card declares og:image:width and og:image:height. Those numbers are hand-written and would drift if the card were ever regenerated at a different size. The tradeoff is that Slack and Facebook can lay the card out on the first crawl rather than after fetching the image.
  • The manifest duplicates two hex values from tokens.ts. A static JSON file cannot import them, and generating the manifest from an endpoint seemed like more machinery than two colors justify.
  • The OG card is Milo on the page background with no text. Adding the site name would have meant getting Fraunces into the rasterizer, which was out of scope.

Checks

vp check (format, lint, typecheck), tsc --noEmit, 19 tests, and fallow all pass. Fallow reports 0 dead files and 0 dead exports.

Summary by CodeRabbit

  • New Features

    • Added web app manifest support for improved installation and launch behavior.
    • Added shortcuts to quickly access the Blog, Tools, and Milo sections.
    • Expanded favicon and Apple device icon support.
    • Added light/dark browser theme color metadata.
  • Bug Fixes

    • Improved social sharing previews with reliable fallback images, alt text, and consistent large-image cards.
    • Prevented unsupported hero images from being used in social previews.

The old icon was a green "Greg" wordmark and the site's only icon. Milo is
the mark everywhere else on the site, so he takes the tab too.

public/favicon.svg is his rest pose, traced from src/components/milo/markup.ts,
which renders exactly this before its cursor tracker starts. The standalone
copy drops the data-milo hooks, the per-instance id namespacing, and the empty
groups that existed only for the tracker to grab. A still image needs none of
them and an XML parser rejects the first. Verified render-identical to the
component output at 16, 32, 180 and 512px.

Adds the icon files the site never had: a 16/32/48 ICO for Safari, which still
does not read SVG favicons, a 180px apple-touch-icon on an opaque plate, 192
and 512 PNGs, and a manifest.

BaseHead also gains a social card fallback. It previously emitted og:image only
when a page passed one, so every page except blog posts and the 3D tool shared
as bare text, including /milo. Pages that bring a usable image of their own are
unchanged. A post with no hero, or with an SVG hero that no social platform can
render, now falls through to the Milo card.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6973608-5f3f-4443-aaab-0cd0911da5f9

📥 Commits

Reviewing files that changed from the base of the PR and between 31aafdb and 553c1d0.

⛔ Files ignored due to path filters (7)
  • public/apple-touch-icon.png is excluded by !**/*.png
  • public/favicon.ico is excluded by !**/*.ico
  • public/favicon.svg is excluded by !**/*.svg
  • public/icon-192.png is excluded by !**/*.png
  • public/icon-512.png is excluded by !**/*.png
  • public/icon-maskable-512.png is excluded by !**/*.png
  • public/og/default.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • public/site.webmanifest
  • src/components/BaseHead.astro

📝 Walkthrough

Walkthrough

The site now provides fallback social-card metadata, expanded favicon and theme-color declarations, and a web app manifest describing site identity, icons, navigation, display mode, and colors.

Changes

Site metadata

Layer / File(s) Summary
Social preview fallback resolution
src/components/BaseHead.astro
Image inputs are normalized, SVGs are excluded from social previews, and fallback OG/Twitter images and alt text are emitted consistently.
Browser and app metadata
src/components/BaseHead.astro, public/site.webmanifest
Favicon variants, touch icon, manifest link, light/dark theme colors, and manifest app settings are declared.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

A rabbit peeked at cards so bright,
With Milo smiling day and night.
New icons hop from leaf to leaf,
While themes bring browsers sweet relief.
“Manifest magic!” thumps the hare.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches a real part of the PR and captures the main favicon branding change, even though the PR also adds broader icon and metadata updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch milo-favicon

Comment @coderabbitai help to get the list of available commands.

@Vilos92

Vilos92 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Context for the review, so you can skip the things that are deliberate and spend effort where it helps.

What this is. The site's favicon was a green "Greg" wordmark. It is now Milo, the cat mascot already used in the header and on /milo. The PR also fills in the icon and social-card metadata the site never had.

public/favicon.svg is generated art, not hand-written. It was derived mechanically from src/components/milo/markup.ts by stripping the data-milo* animation hooks, the per-instance id namespacing, and the attribute-less <g> wrappers, then re-serialized through an XML parser. The path data is the same artwork the live component draws, and the result was verified render-identical to the component output at 16, 32, 180 and 512px. Please do review it for standalone-SVG correctness (dangling url(#id) references, missing xmlns, XML validity), but there is no value in commenting on the path data itself or on the file's length.

Known duplication, accepted on purpose:

  1. public/favicon.svg duplicates artwork that also lives in src/components/milo/markup.ts. There is no build step wiring them together. This was a deliberate call: the alternative was an Astro endpoint or a checked-in generator script plus a devDependency, for art that changes approximately never. The file header explains the provenance. Please do not suggest deduplicating it.
  2. public/site.webmanifest hardcodes #f7f4ef in two fields, which duplicates palette.pageBg from src/styles/tokens.ts. A static JSON file cannot import a TS constant. The HTML theme-color tags do import from tokens. Flagging the drift risk is fair; suggesting an endpoint to generate the manifest is a tradeoff I already considered and rejected as more machinery than two colors justify.

Decisions that look wrong but are researched. Please do not "correct" these without a source that postdates the ones cited:

  • The ICO link carries sizes="32x32" and the SVG link carries none. The widely-copied sizes="any" advice is superseded and has the opposite of the intended effect, causing Chrome to download both icon files. favicon.ico does contain 16/32/48 frames; the narrower declaration is about HTML link selection, and OS-level consumers still read all three frames out of the file.
  • The manifest icons deliberately declare no purpose. Declaring maskable on unpadded artwork gets it clipped by Android's adaptive mask, which per the W3C spec requires content to sit inside a centered circle of radius 40% of the icon. These icons are not padded, so any (the default) is correct.
  • The manifest uses display: "minimal-ui" rather than standalone, so an installed copy keeps its back button on what is a multi-page content site. It also omits id, which is inert without an install flow.

Where scrutiny is genuinely welcome. src/components/BaseHead.astro is the only real logic. It previously emitted og:image / twitter:image only when a page passed an image, and switched twitter:card between summary and summary_large_image accordingly. It now always emits them, falling back to /og/default.png. The interesting part is readCardImage / readText and the alt-precedence rule: the alt must follow whichever image actually won, because blog/[...slug].astro always passes imageAlt={post.data.title} while heroImage is optional in blogSchema. An earlier revision of this PR got that wrong and would have shipped the Milo card labelled with an article title. Please check the resolution logic for any remaining case that emits a mismatched, empty, or unrenderable value.

Verified against built output for: jpg hero (keeps its own), svg hero (falls back, since no platform renders SVG cards), no hero (falls back), and pages that pass nothing.

Repo conventions live in AGENTS.md (prefer type over interface, prefer undefined over null, ?? vs || rules, empty-string-as-absent through a named helper, boolean predicates named as questions). Please flag violations only in code this PR adds. BaseHead's pre-existing interface Props is out of scope here.

Local gates all pass: vp check (format, lint, typecheck), tsc --noEmit, 19 tests, and fallow at 0 dead files and 0 dead exports.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

@Vilos92 Understood. I’ll perform a full review of #9, focusing on the metadata resolution paths and standalone SVG validity while honoring the documented intentional tradeoffs.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/BaseHead.astro`:
- Around line 33-35: Update readCardImage so SVG detection removes or ignores
URL query strings and fragments before checking the .svg extension. Preserve the
existing fallback behavior for undefined values and SVG paths, while allowing
non-SVG image URLs to continue through unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: da05925e-0d23-41ec-8cc8-9b77cbaaead2

📥 Commits

Reviewing files that changed from the base of the PR and between 31aafdb and 8295f5a.

⛔ Files ignored due to path filters (6)
  • public/apple-touch-icon.png is excluded by !**/*.png
  • public/favicon.ico is excluded by !**/*.ico
  • public/favicon.svg is excluded by !**/*.svg
  • public/icon-192.png is excluded by !**/*.png
  • public/icon-512.png is excluded by !**/*.png
  • public/og/default.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • public/site.webmanifest
  • src/components/BaseHead.astro

Comment thread src/components/BaseHead.astro Outdated
Vilos92 and others added 2 commits July 26, 2026 23:35
A hero carrying a cache-busting query, `/blog/x.svg?v=2`, sailed past the
endsWith check and shipped an SVG card that no social platform renders.

Co-Authored-By: Claude <noreply@anthropic.com>
Without a maskable variant Android shrinks Milo onto a white plate and
letterboxes him. The unpadded icons stay purpose any, since the spec warns
against one file serving both, and the new padded file sizes him to 66% of
the tile. His ear tips sit at radius 0.604 x scale from centre, and the safe
zone is a circle of radius 40%, so that is the largest scale that survives
every launcher mask shape.

Shortcuts put Blog, Tools and Milo on a long press of the installed icon.

Also declares og:image:width and height on the fallback card, which lets
Slack and Facebook lay it out before they have fetched the image. Only the
fallback can claim a size, since page heroes are arbitrary files.

Co-Authored-By: Claude <noreply@anthropic.com>
@Vilos92

Vilos92 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Two commits landed after your review at 06:30, so they have not been looked at. Automatic reviews are off on this repo, hence the explicit re-request. The earlier context comment still applies, in particular the list of deliberate decisions not to "correct". Delta since your pass:

cf5d36c — addresses your inline finding. You flagged that readCardImage checked .endsWith('.svg') against the raw string, so a hero carrying a query or fragment would slip through. It now strips [?#].*$ before reading the extension. Verified both directions against built output: /blog/copyparty.svg?v=2 falls back to the Milo card, and /blog/hello-world.jpg?v=2 keeps its own hero with the query string intact. Worth confirming I did not break the non-SVG path.

553c1d0 — new work, please review properly.

  • public/icon-maskable-512.png, a padded opaque variant declared purpose: "maskable". The two unpadded icons keep purpose: any and stay separate files, per the spec's warning against one file serving both. Milo is sized to 66% of the tile: his ear tips sit at radius 0.604 x scale from center, and the W3C safe zone is a circle of radius 40% of the icon, so 0.66 is the largest scale where no part of him falls outside the guarantee. If you think that derivation is wrong, say so with the arithmetic.
  • shortcuts for /blog/, /tools/ and /milo/. This site sets trailingSlash: 'always' in astro.config.mjs, so the trailing slashes are deliberate. All three routes are confirmed present in the build output.
  • og:image:width / og:image:height, emitted only when the Milo fallback is the image that won, since page heroes are arbitrary files of unknown size. The values are hand-written and match the generated 1200x630 card. The drift risk is known and accepted, so please do not file it as a bug, though a suggestion for cheaply asserting it would be welcome.

Also worth a look: the manifest grew a nested icons entry and a shortcuts array, so please sanity check it against the Web App Manifest spec and confirm every src resolves to a file that exists in public/.

All CI green: fmt, lint, typecheck, test, fallow.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

@Vilos92 I’ll re-review the current PR state, including cf5d36c and 553c1d0. I’ll specifically verify the query/fragment-aware SVG fallback without regressing usable hero URLs, the fallback-only OG dimensions and aligned alt text, standalone SVG correctness, and manifest icon/shortcut validity and asset presence—while respecting the documented intentional decisions.

✅ Action performed

Full review finished.

@Vilos92
Vilos92 merged commit 11c8f3e into main Jul 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant