From e7c17b865159fde1afc9a7e09c293e5ebec9feb2 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Sun, 24 May 2026 17:06:07 -0600 Subject: [PATCH] Fix cross-browser favicons --- .gitignore | 7 +++- app/layout.tsx | 22 ++++++++++- docs/guides/static-assets.md | 7 +++- package-lock.json | 47 ++++++++++++++++++++++ package.json | 6 ++- public/apple-touch-icon.png | Bin 0 -> 2178 bytes public/favicon-16x16.png | Bin 0 -> 211 bytes public/favicon-32x32.png | Bin 0 -> 339 bytes public/favicon.ico | Bin 0 -> 5430 bytes scripts/generate-favicons.mjs | 71 ++++++++++++++++++++++++++++++++++ tests/unit/Layout.test.jsx | 32 ++++++++++++++- 11 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/favicon.ico create mode 100644 scripts/generate-favicons.mjs diff --git a/.gitignore b/.gitignore index fc752b1..bf17aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ npm-cache/ /lhci-results/ /.lighthouseci/ -# Ignore other image files (but not visual regression snapshots) +# Ignore other image files (but not visual regression snapshots or favicons) *.png *.jpg *.jpeg @@ -39,6 +39,11 @@ npm-cache/ *.avi *.mkv +# Root favicons (generated via `npm run generate:favicons`) +!public/favicon-16x16.png +!public/favicon-32x32.png +!public/apple-touch-icon.png + # Visual regression snapshots (allow these) !tests/e2e/visual-regression.spec.ts-snapshots/ !tests/e2e/visual-regression.spec.ts-snapshots/*.png diff --git a/app/layout.tsx b/app/layout.tsx index dacd659..9fa8c7f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -60,7 +60,27 @@ export const metadata: Metadata = { }, metadataBase: new URL("https://communityrule.com"), icons: { - icon: [{ url: getAssetPath(ASSETS.LOGO), type: "image/svg+xml" }], + icon: [ + { url: getAssetPath(ASSETS.LOGO), type: "image/svg+xml" }, + { url: "/favicon.ico", sizes: "any" }, + { + url: "/favicon-32x32.png", + sizes: "32x32", + type: "image/png", + }, + { + url: "/favicon-16x16.png", + sizes: "16x16", + type: "image/png", + }, + ], + apple: [ + { + url: "/apple-touch-icon.png", + sizes: "180x180", + type: "image/png", + }, + ], }, alternates: { canonical: "/", diff --git a/docs/guides/static-assets.md b/docs/guides/static-assets.md index a3d611c..0fcea2d 100644 --- a/docs/guides/static-assets.md +++ b/docs/guides/static-assets.md @@ -41,8 +41,11 @@ Do not duplicate the same glyph in both places unless migrating between systems. - **Blog art** stays under `public/content/blog/` with `{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`. - **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in - `app/layout.tsx` metadata). Do not place `favicon.ico` or other static - binaries under `app/` — keep `app/` for routes, layouts, and styles only + `app/layout.tsx` metadata) plus generated root binaries for Safari/iOS: + `public/favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, and + `apple-touch-icon.png`. Regenerate after logo changes with + `npm run generate:favicons`. Do not place other static binaries under + `app/` — keep `app/` for routes, layouts, and styles only (`globals.css`, `tailwind.css`). ## PNG files and `.gitignore` diff --git a/package-lock.json b/package-lock.json index aec4d47..4089b45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "community-rule", "version": "0.1.0", "hasInstallScript": true, + "license": "GPL-3.0-or-later", "dependencies": { "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", @@ -53,6 +54,7 @@ "knip": "^5.50.0", "msw": "^2.10.5", "playwright": "^1.55.0", + "png-to-ico": "^3.0.1", "postcss": "^8.5.6", "prettier": "^3.7.4", "prisma": "^6.19.0", @@ -19175,6 +19177,51 @@ "node": ">=18" } }, + "node_modules/png-to-ico": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/png-to-ico/-/png-to-ico-3.0.1.tgz", + "integrity": "sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.10.3", + "minimist": "^1.2.8", + "pngjs": "^7.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/png-to-ico/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/png-to-ico/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/po-parser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", diff --git a/package.json b/package.json index 4a843b2..a31f15f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "bundle:analyze": "node scripts/bundle-analyzer.js", "db:deploy": "prisma migrate deploy", "migrate:smoke": "./scripts/migrate-smoke-local.sh", - "docker:release": "./scripts/docker-release.sh" + "docker:release": "./scripts/docker-release.sh", + "generate:favicons": "node scripts/generate-favicons.mjs" }, "dependencies": { "@mdx-js/loader": "^3.1.1", @@ -89,9 +90,10 @@ "globals": "^17.1.0", "jest-axe": "^10.0.0", "jsdom": "^26.1.0", - "msw": "^2.10.5", "knip": "^5.50.0", + "msw": "^2.10.5", "playwright": "^1.55.0", + "png-to-ico": "^3.0.1", "postcss": "^8.5.6", "prettier": "^3.7.4", "prisma": "^6.19.0", diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7d50f633b069174a6bd3e0c882dc2d76f450e69d GIT binary patch literal 2178 zcmcgu`#;m|8=vpW$YJ$-#nYTtiN{wCdF0TTEvHIKNeELYW-CHX$yxEFq*grA+T`?2 zIop=Q9P)~UnR$f?-^grX&W9GZ@4e^wAD$oX&-K3U>$=|8`?_DR&;7dpa6jduvIDUL z0)eO;KjwHA*jm|AkOyD89(i+M6eEuLL_r`>p=`;~W6u&G5Cx0lj@WZCFGQn+^z)u- z?Tbb3ct@f8sbI(BPI_VPN57?eApgxjkbHm6N#V=|PKUy)?sTE6+9+Lfn?JJt`P74( zmkxG*dR48YfOS_p<8$u{V?tR$e~;zz%dJ_~nzU_M{MKhf@GW;4*WTT&h^n+kWx+8+ zNl?T#wWK}{vU1@c_d})oMo0a}?3pfGDZji4gyBtA+Q04ykgk!NE{E~Le=vByln9bwxa3iC!S9CLAB*78*~%n5}F&$&Wn=p#%R>I(fbDe$oq zT3$d572op=cVP0kEtX>5dBu8BwmhS=_uYRuuR%=TUJ!;UA>uFQZ8o;0Tr+>Ce^pi* zqJyX{ElRq&R0CJMFe!%UatjxsDNRXDrAxXunPK!GMx zE4BbvQ@;uHU59H|fwWc0*gN0=VjS1HtZH^q#5<|vub|Xz9)_8)S zYdMQka@u~(!0ov(sQtkn_N6qpg{CHt&0v%ux4G!ho-*Ek!<#BY!#A=AH>WVnfQMF!nl@Iu(oum3-()6Q9$f!Jk@MG z>rAg*P`jc;WR()h!C4Z;5GQ7Iln|yJjp?{TwG>%P7dM!bf3p0BmgFvUmOc|PR|b90 z-y)H`Izj@>!ZWq1a+G}0m@4Bqc5=UOp0sMWD?gES5@g>eepi{HBI1=)Co-P& z=!_Fm)3}yk=RQ#~c{fv=b3lVaAwB^*@i8@Q3>TxHH#IvXgxaQN8BOgjm+JsrNSO#!=Z9MwnQX47!lVHU< z>su0Ae8wx&hdQXVFL5kN@y{o6e$yd|I9pQ8a$e`n$9?`Q+ zCGyopm9IJwEJ_m5kR@z&z8Zqed!M00tOYrSJ;FX`JlGg<9iX}X1y1au!3J&jb?gWx zqZH)o#rO6e_D1mjx}&Nnb#$}3jD1+a02}l7CB>D2yAxOe7|XIQ#fjlOku13jeZzZIS8xPNrYugy{`#*)JVYx+aP+a`L(}i@x2=p zC;!LBWba>0eiDv?`ee2#os0)!Y<8Ghi)9JF+G18@{=eW)qcviKBDQC(!sc4ZQ%6Hq zdI}$}ripgz`J(5PC1qzy_o!ex)3Zw literal 0 HcmV?d00001 diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b1dcb03f9e627e4223c6aba317944a2202161dcd GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP_Wh0 z#WAE}&eF?{Tug>M3eA>6SSS*<gQFW|Yv#zL^r z!bTJ6%gY9_#23LT=*ME9stlNhyc3^5RHK36#;4okq8%R2gw2o9f2+Ibj)9YdF)#w zo)d8b#wG0#*myvrK{CgJGmEwlJbpM<8AL2uxC3p6ei@jNU=XnYd;->T=vQ?EWR3-< z7VW?j#;7e2PY5r-q@*1cr=ivmW&u{7fkPAN7wwBE*SrBm`!UM33McU)F>y0 l!bvnHMyoI>G^eWV`5S3FM*ZDhT44YH002ovPDHLkV1gltdGr7P literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..08f364081786ce045e4ea8186c50fd680f433b27 GIT binary patch literal 5430 zcmc&&O=}cE5Uq90c|#zGIqgLa@#qhjy$IgSe~5wnjD-Y42n6*axq1?R#oon>2f;(a zCKpK{5K%P5n8|zW6cj^uZ_n&}G(5VtyQ<#n>Ymw|5$VZ2SzHwK1KGMS@>oO$gX!;& z7(c++D#l>KV>*t`*M^vQ9R@StKTstPHT=nQRL!Fr7_TU! zr>H@5l+j}~ko!IG2Kbw6xZ)5unHt!-)TN_J z4eHjh5wQp7@Hq|m{lwf3Q04z6bS+#U=Lh_8f-piSl*&t zR)^~x@Qn2guQk8S;8#7dI$ZpGp2wSvvwg;>a)kO=eqa699qfGe{x^d^mY3;6=kE;j z&*_x6r|7={zBQNO)ttVyTaF)a=DGKsyDGfockR37on_B^AMEn}TV?I@u9r2}R%Y#W z@{&Jv=Ix%>&8utQgkgD#c2m5w*4`vo0WZ5OYCS2mH!j&X|WcW~&-t { + console.error(error); + process.exit(1); +}); diff --git a/tests/unit/Layout.test.jsx b/tests/unit/Layout.test.jsx index 84b18c8..8a5f4a5 100644 --- a/tests/unit/Layout.test.jsx +++ b/tests/unit/Layout.test.jsx @@ -1,5 +1,5 @@ import { describe, test, expect, vi } from "vitest"; -import RootLayout from "../../app/layout"; +import RootLayout, { metadata } from "../../app/layout"; import MarketingLayout from "../../app/(marketing)/layout"; import AppLayout from "../../app/(app)/layout"; @@ -84,6 +84,36 @@ describe("RootLayout", () => { ); expect(childText).toBeTruthy(); }); + + test("declares svg, ico, png, and apple-touch icons for cross-browser support", () => { + const icons = metadata.icons; + expect(icons?.icon).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + url: "/assets/logos/community-rule.svg", + type: "image/svg+xml", + }), + expect.objectContaining({ url: "/favicon.ico", sizes: "any" }), + expect.objectContaining({ + url: "/favicon-32x32.png", + sizes: "32x32", + type: "image/png", + }), + expect.objectContaining({ + url: "/favicon-16x16.png", + sizes: "16x16", + type: "image/png", + }), + ]), + ); + expect(icons?.apple).toEqual([ + { + url: "/apple-touch-icon.png", + sizes: "180x180", + type: "image/png", + }, + ]); + }); }); describe("Group layouts (chrome composition)", () => {