generated from wiz4apps/templates.OnlineShop
Favicon: mirror the source site's /favicon.ico — a 32×32 PNG payload under an `.ico` name — into public/favicon.ico. scripts/fetch-favicon.ts keeps it in place (`pnpm fetch:favicon`, or `pnpm fetch:brand` for wordmark + icon) and the dev server calls it at boot; the file handler sniffs the PNG signature for `.ico` and serves it as image/png, so the default /favicon.ico request — and the new <link rel="icon"> — resolve to the brand rose. Brand lockup: the tagline "Цветы подарки Минск" is now red, un-stretched text under the wordmark in the header and inside the footer's white plate (it used to be letter-spaced 8px caps, which read as squeezed). The wordmark itself is sized on one axis only (height, width:auto, object-fit:contain) — the artwork is tightly cropped, so a width and a height at once would stretch it. BRAND.tagline / favicon* record both facts in src/store/constants.ts.
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
||
import { dirname, join, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { BRAND } from '../src/store/constants';
|
||
|
||
/**
|
||
* Mirror the store icon into public/favicon.ico.
|
||
*
|
||
* Source: the icon the source site publishes at BRAND.faviconSourceUrl. That file is
|
||
* named `.ico` but its payload is a single 32×32 PNG (the source site serves the PNG
|
||
* bytes as `image/x-icon`) — so the bytes are validated against the PNG signature here,
|
||
* and the dev server labels `.ico` files with a PNG payload as `image/png`
|
||
* (scripts/dev-server.ts). Browsers ask for `/favicon.ico` by themselves, so serving that
|
||
* path correctly is all the storefront needs; the <link rel="icon"> in public/index.html
|
||
* just names the size and type up front.
|
||
*
|
||
* Idempotent: an existing non-empty file is kept; pass `--force` to re-download it
|
||
* (the source is a static theme asset, so a plain re-run is a no-op).
|
||
*
|
||
* pnpm fetch:favicon # download only when missing
|
||
* pnpm fetch:favicon -- --force # refresh the local copy
|
||
*
|
||
* The dev server also calls `ensureBrandFavicon()` once at boot (scripts/dev-server.ts),
|
||
* so a fresh checkout gets the icon without an extra manual step.
|
||
*/
|
||
|
||
const USER_AGENT =
|
||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 la-rose-seed/1.0';
|
||
const REQUEST_TIMEOUT_MS = 20_000;
|
||
|
||
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||
|
||
const PUBLIC_DIR = fileURLToPath(new URL('../public', import.meta.url));
|
||
|
||
/** Absolute path of the local file served at BRAND.faviconLocalPath. */
|
||
export const BRAND_FAVICON_FILE = join(PUBLIC_DIR, ...BRAND.faviconLocalPath.split('/').filter(Boolean));
|
||
|
||
export type BrandFaviconStatus = 'kept' | 'downloaded';
|
||
|
||
/** True when `bytes` start with the PNG signature (the source icon is a PNG, not an ICO). */
|
||
export function isPng(bytes: Buffer): boolean {
|
||
return bytes.length >= PNG_SIGNATURE.length && bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE);
|
||
}
|
||
|
||
/** Ensure the local icon copy is on disk; download it when missing (or when forced). */
|
||
export async function ensureBrandFavicon(force = false): Promise<BrandFaviconStatus> {
|
||
if (!force) {
|
||
const info = await stat(BRAND_FAVICON_FILE).catch(() => null);
|
||
if (info && info.size > 0) return 'kept';
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||
try {
|
||
const response = await fetch(BRAND.faviconSourceUrl, {
|
||
headers: {
|
||
'user-agent': USER_AGENT,
|
||
accept: 'image/png,image/x-icon;q=0.9,image/*;q=0.8,*/*;q=0.5',
|
||
},
|
||
redirect: 'follow',
|
||
signal: controller.signal,
|
||
});
|
||
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
||
|
||
const bytes = Buffer.from(await response.arrayBuffer());
|
||
if (!isPng(bytes)) throw new Error('unexpected payload — expected the PNG icon');
|
||
|
||
await mkdir(dirname(BRAND_FAVICON_FILE), { recursive: true });
|
||
await writeFile(BRAND_FAVICON_FILE, bytes);
|
||
return 'downloaded';
|
||
} finally {
|
||
clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
const isDirectRun =
|
||
process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||
|
||
if (isDirectRun) {
|
||
const status = await ensureBrandFavicon(process.argv.includes('--force'));
|
||
const bytes = (await stat(BRAND_FAVICON_FILE)).size;
|
||
console.log(
|
||
status === 'kept'
|
||
? `fetch-favicon: kept ${BRAND.faviconLocalPath} (${bytes} B, ${BRAND.faviconSize}px PNG payload)`
|
||
: `fetch-favicon: downloaded ${BRAND.faviconLocalPath} (${bytes} B, ${BRAND.faviconSize}px PNG payload)`,
|
||
);
|
||
}
|