Backend / staging cleanup, performance substrate, and create-flow polish #60
+6
-1
@@ -28,7 +28,7 @@ npm-cache/
|
|||||||
/lhci-results/
|
/lhci-results/
|
||||||
/.lighthouseci/
|
/.lighthouseci/
|
||||||
|
|
||||||
# Ignore other image files (but not visual regression snapshots)
|
# Ignore other image files (but not visual regression snapshots or favicons)
|
||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.jpeg
|
||||||
@@ -39,6 +39,11 @@ npm-cache/
|
|||||||
*.avi
|
*.avi
|
||||||
*.mkv
|
*.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)
|
# Visual regression snapshots (allow these)
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/
|
!tests/e2e/visual-regression.spec.ts-snapshots/
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
||||||
|
|||||||
+21
-1
@@ -60,7 +60,27 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
metadataBase: new URL("https://communityrule.com"),
|
metadataBase: new URL("https://communityrule.com"),
|
||||||
icons: {
|
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: {
|
alternates: {
|
||||||
canonical: "/",
|
canonical: "/",
|
||||||
|
|||||||
@@ -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
|
- **Blog art** stays under `public/content/blog/` with
|
||||||
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`.
|
`{slug}-vertical.svg`, `-horizontal.svg`, `-section.svg`, `-tag.svg`.
|
||||||
- **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in
|
- **Favicon** reuses `assets/logos/community-rule.svg` (`ASSETS.LOGO` in
|
||||||
`app/layout.tsx` metadata). Do not place `favicon.ico` or other static
|
`app/layout.tsx` metadata) plus generated root binaries for Safari/iOS:
|
||||||
binaries under `app/` — keep `app/` for routes, layouts, and styles only
|
`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`).
|
(`globals.css`, `tailwind.css`).
|
||||||
|
|
||||||
## PNG files and `.gitignore`
|
## PNG files and `.gitignore`
|
||||||
|
|||||||
Generated
+47
@@ -8,6 +8,7 @@
|
|||||||
"name": "community-rule",
|
"name": "community-rule",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"knip": "^5.50.0",
|
"knip": "^5.50.0",
|
||||||
"msw": "^2.10.5",
|
"msw": "^2.10.5",
|
||||||
"playwright": "^1.55.0",
|
"playwright": "^1.55.0",
|
||||||
|
"png-to-ico": "^3.0.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prisma": "^6.19.0",
|
"prisma": "^6.19.0",
|
||||||
@@ -19175,6 +19177,51 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/po-parser": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
||||||
|
|||||||
+4
-2
@@ -45,7 +45,8 @@
|
|||||||
"bundle:analyze": "node scripts/bundle-analyzer.js",
|
"bundle:analyze": "node scripts/bundle-analyzer.js",
|
||||||
"db:deploy": "prisma migrate deploy",
|
"db:deploy": "prisma migrate deploy",
|
||||||
"migrate:smoke": "./scripts/migrate-smoke-local.sh",
|
"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": {
|
"dependencies": {
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
@@ -89,9 +90,10 @@
|
|||||||
"globals": "^17.1.0",
|
"globals": "^17.1.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"msw": "^2.10.5",
|
|
||||||
"knip": "^5.50.0",
|
"knip": "^5.50.0",
|
||||||
|
"msw": "^2.10.5",
|
||||||
"playwright": "^1.55.0",
|
"playwright": "^1.55.0",
|
||||||
|
"png-to-ico": "^3.0.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prisma": "^6.19.0",
|
"prisma": "^6.19.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 211 B |
Binary file not shown.
|
After Width: | Height: | Size: 339 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Regenerate root favicon binaries from `public/assets/logos/community-rule.svg`.
|
||||||
|
* Safari and iOS need PNG/ICO fallbacks; SVG alone shows a letter fallback in Safari.
|
||||||
|
*
|
||||||
|
* Run: npm run generate:favicons
|
||||||
|
*/
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import pngToIco from "png-to-ico";
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const PUBLIC = path.join(ROOT, "public");
|
||||||
|
const SVG_PATH = path.join(PUBLIC, "assets/logos/community-rule.svg");
|
||||||
|
const LOGO_FILL = "#FFFDD2";
|
||||||
|
const MARK_ON_LIGHT = "#000000";
|
||||||
|
|
||||||
|
async function readLogoSvg() {
|
||||||
|
return fs.readFile(SVG_PATH, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markPng(svg, size, fill) {
|
||||||
|
const tinted = svg.replaceAll(LOGO_FILL, fill);
|
||||||
|
return sharp(Buffer.from(tinted))
|
||||||
|
.resize(size, size, { fit: "contain" })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function creamMarkOnBlack(svg, size) {
|
||||||
|
const logoSize = Math.round(size * 0.75);
|
||||||
|
const logo = await markPng(svg, logoSize, LOGO_FILL);
|
||||||
|
return sharp({
|
||||||
|
create: {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([{ input: logo, gravity: "center" }])
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const svg = await readLogoSvg();
|
||||||
|
const png16 = await markPng(svg, 16, MARK_ON_LIGHT);
|
||||||
|
const png32 = await markPng(svg, 32, MARK_ON_LIGHT);
|
||||||
|
const appleTouch = await creamMarkOnBlack(svg, 180);
|
||||||
|
const faviconIco = await pngToIco([png16, png32]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fs.writeFile(path.join(PUBLIC, "favicon-16x16.png"), png16),
|
||||||
|
fs.writeFile(path.join(PUBLIC, "favicon-32x32.png"), png32),
|
||||||
|
fs.writeFile(path.join(PUBLIC, "apple-touch-icon.png"), appleTouch),
|
||||||
|
fs.writeFile(path.join(PUBLIC, "favicon.ico"), faviconIco),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Wrote public/favicon.ico");
|
||||||
|
console.log("Wrote public/favicon-16x16.png");
|
||||||
|
console.log("Wrote public/favicon-32x32.png");
|
||||||
|
console.log("Wrote public/apple-touch-icon.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect, vi } from "vitest";
|
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 MarketingLayout from "../../app/(marketing)/layout";
|
||||||
import AppLayout from "../../app/(app)/layout";
|
import AppLayout from "../../app/(app)/layout";
|
||||||
|
|
||||||
@@ -84,6 +84,36 @@ describe("RootLayout", () => {
|
|||||||
);
|
);
|
||||||
expect(childText).toBeTruthy();
|
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)", () => {
|
describe("Group layouts (chrome composition)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user