Use bundled logo from public/ instead of the API

Header, footer and og:image now read public/images/logo.png via
LOGO_SRC in config.ts. The API's /website/favicon returned another
business's mark, so getFavicon()/BrandAssets are removed and the tab
icon is the bundled favicon.png. Footer logo is now 70px like the
header (108px was sized for the old square mark; the new 200x48
wordmark would be 450px wide).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-20 00:13:53 +03:30
co-authored by Claude Sonnet 5
parent 8e82370b64
commit 7089078b4c
8 changed files with 36 additions and 78 deletions
+10 -23
View File
@@ -6,16 +6,14 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
import TopBar from "@/components/TopBar";
import MobileBottomNav from "@/components/MobileBottomNav";
import { getCategories, getFavicon, safe } from "@/lib/api";
import { getCategories, safe } from "@/lib/api";
import { buildCategoryTree, carouselCategories } from "@/lib/categoryTree";
import { getSession } from "@/lib/auth";
import { SITE_NAME } from "@/data/site";
import { SITE_URL } from "@/lib/config";
import { LOGO_HEIGHT, LOGO_SRC, LOGO_WIDTH, SITE_URL } from "@/lib/config";
import { ToastProvider } from "@/components/providers/ToastProvider";
import { CartProvider } from "@/components/providers/CartProvider";
const NO_BRAND_ASSETS = { faviconUrl: null, logoUrl: null, logoDarkUrl: null, hasDedicatedFavicon: false };
// Montserrat is the Latin face the live moalem.shop theme pairs with IRANYekan.
const montserrat = Montserrat({
variable: "--font-en",
@@ -27,13 +25,9 @@ const montserrat = Montserrat({
const BASE_DESCRIPTION =
"خرید اینترنتی گوشی موبایل، لپ‌تاپ، تبلت، کنسول بازی و لوازم جانبی با ضمانت اصالت کالا از فروشگاه موبایل معلم.";
// generateMetadata (not a static export) because the tab icon comes from
// GET /tenants/{domain}/website/favicon — once the business uploads a real
// one in the dashboard this picks it up with no code change. Falls back to
// the bundled /favicon.png only while that API field is empty.
export async function generateMetadata(): Promise<Metadata> {
const brand = await safe(getFavicon(), NO_BRAND_ASSETS);
// Tab icon and og:image use the bundled brand files in /public — on purpose
// not GET /website/favicon (that endpoint served another business's mark).
export function generateMetadata(): Metadata {
return {
metadataBase: new URL(SITE_URL),
title: {
@@ -41,7 +35,7 @@ export async function generateMetadata(): Promise<Metadata> {
template: `%s | ${SITE_NAME}`,
},
description: BASE_DESCRIPTION,
icons: [{ url: brand.faviconUrl || "/favicon.png", type: "image/png" }],
icons: [{ url: "/favicon.png", type: "image/png" }],
openGraph: {
type: "website",
locale: "fa_IR",
@@ -49,24 +43,17 @@ export async function generateMetadata(): Promise<Metadata> {
title: `${SITE_NAME} | خرید موبایل، لپ‌تاپ و لوازم جانبی`,
description: BASE_DESCRIPTION,
url: SITE_URL,
// The dashboard logo is a square mark; the bundled fallback is a wide
// wordmark — the two aren't the same shape, so their og:image sizes
// can't share one hardcoded pair.
images: brand.logoUrl
? [{ url: brand.logoUrl, width: 512, height: 512, alt: SITE_NAME }]
: [{ url: "/images/logo.png", width: 130, height: 36, alt: SITE_NAME }],
images: [{ url: LOGO_SRC, width: LOGO_WIDTH, height: LOGO_HEIGHT, alt: SITE_NAME }],
},
};
}
export default async function RootLayout({ children }: LayoutProps<"/">) {
const [categories, user, brand] = await Promise.all([
const [categories, user] = await Promise.all([
safe(getCategories().then(buildCategoryTree), []),
getSession(),
safe(getFavicon(), NO_BRAND_ASSETS),
]);
const menu = carouselCategories(categories);
const logoUrl = brand.logoUrl || "/images/logo.png";
return (
<html lang="fa" dir="rtl" className={`${iranYekan.variable} ${montserrat.variable} h-full antialiased`}>
@@ -74,9 +61,9 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
<ToastProvider>
<CartProvider>
<TopBar />
<Header menu={menu} user={user} logoUrl={logoUrl} />
<Header menu={menu} user={user} />
<main className="flex-1 pb-6">{children}</main>
<Footer categories={menu} logoUrl={logoUrl} />
<Footer categories={menu} />
{/* Spacer so the fixed mobile bar never covers the end of the footer. */}
<div className="h-[68px] lg:hidden" />
<MobileBottomNav categories={menu} user={user} />
+6 -13
View File
@@ -1,30 +1,23 @@
import Link from "next/link";
import Image from "next/image";
import { MAIN_NAV, PHONE, PHONE_HREF, SITE_NAME, SITE_TAGLINE, SOCIAL } from "@/data/site";
import { LOGO_HEIGHT, LOGO_SRC, LOGO_WIDTH } from "@/lib/config";
import { categoryHref } from "@/lib/slug";
import type { CategoryNode } from "@/lib/types";
import { InstagramIcon, PhoneIcon, TelegramIcon } from "./icons";
import { toFaDigits } from "@/lib/format";
export default function Footer({
categories,
logoUrl,
}: {
categories: CategoryNode[];
logoUrl: string;
}) {
export default function Footer({ categories }: { categories: CategoryNode[] }) {
return (
<footer className="mt-14 border-t border-line bg-surface">
<div className="boxed grid gap-8 py-10 md:grid-cols-4">
<div className="md:col-span-2">
{/* h-[108px] w-auto so a square dashboard logo and the wide
fallback wordmark both render undistorted — see Header.tsx. */}
<Image
src={logoUrl}
src={LOGO_SRC}
alt={SITE_NAME}
width={280}
height={108}
className="h-[108px] w-auto object-contain"
width={LOGO_WIDTH}
height={LOGO_HEIGHT}
className="h-[70px] w-auto object-contain"
/>
<p className="mt-3 max-w-md text-[13px] leading-7 text-body">{SITE_TAGLINE}</p>
<a
+4 -7
View File
@@ -8,6 +8,7 @@ import AccountMenu from "./AccountMenu";
import CartButton from "./CartButton";
import { ChevronDown, GridIcon, PhoneIcon } from "./icons";
import { MAIN_NAV, PHONE, PHONE_HREF, SITE_NAME } from "@/data/site";
import { LOGO_HEIGHT, LOGO_SRC, LOGO_WIDTH } from "@/lib/config";
import { categoryHref } from "@/lib/slug";
import type { AuthUser, CategoryNode } from "@/lib/types";
@@ -16,11 +17,9 @@ import type { AuthUser, CategoryNode } from "@/lib/types";
export default function Header({
menu,
user,
logoUrl,
}: {
menu: CategoryNode[];
user: AuthUser | null;
logoUrl: string;
}) {
const [megaOpen, setMegaOpen] = useState(false);
@@ -33,13 +32,11 @@ export default function Header({
the search box/buttons beside it. */}
<div className="flex h-[70px] items-center gap-4 lg:h-25">
<Link href="/" className="hidden shrink-0 lg:block" aria-label={SITE_NAME}>
{/* h-[70px] w-auto (not a fixed box) so a square dashboard logo
mark and the wide fallback wordmark both render undistorted. */}
<Image
src={logoUrl}
src={LOGO_SRC}
alt={SITE_NAME}
width={180}
height={70}
width={LOGO_WIDTH}
height={LOGO_HEIGHT}
className="h-[70px] w-auto object-contain"
priority
/>
-8
View File
@@ -4,7 +4,6 @@ import type {
ApiCategory,
ApiProduct,
AuthUser,
BrandAssets,
BusinessInfo,
ContactFormInput,
EntityComment,
@@ -88,13 +87,6 @@ export function getBusinessInfo(): Promise<BusinessInfo> {
return apiGet<BusinessInfo>(t("/website/business-info"));
}
// Single documented source for header/footer logos and the browser-tab
// icon — see AGENTS.md "Branding". Prefer this over reading logoUrl off
// getTenant()/getBusinessInfo() directly.
export function getFavicon(): Promise<BrandAssets> {
return apiGet<BrandAssets>(t("/website/favicon"));
}
export function getSliders(): Promise<SliderItem[]> {
return apiGet<{ items: SliderItem[] }>(t("/website/sliders")).then((r) => r.items);
}
+6
View File
@@ -7,6 +7,12 @@ export const CUSTOMER_PORTAL_URL = `https://customer.${SITE_DOMAIN}`;
// "مدیریت وبسایت" account-menu item shown to admins.
export const BUSINESS_PORTAL_URL = `https://business.${SITE_DOMAIN}`;
// Bundled brand mark (public/images/logo.png, 200x48 wordmark) — deliberately
// not read from the API's /website/favicon; see AGENTS.md "Brand".
export const LOGO_SRC = "/images/logo.png";
export const LOGO_WIDTH = 200;
export const LOGO_HEIGHT = 48;
// Written by the customer dashboard on login, on the shared parent domain
// (".{SITE_DOMAIN}", not HttpOnly) — this storefront only ever reads it, per
// the official AI_PROMPT.md "Shared login" contract. May be chunked into
-10
View File
@@ -23,16 +23,6 @@ export interface Tenant {
faviconUrl: string | null;
}
// GET /tenants/{domain}/website/favicon — the documented single source for
// header/footer logos and the browser-tab icon (falls back to the light
// logo when hasDedicatedFavicon is false).
export interface BrandAssets {
faviconUrl: string | null;
logoUrl: string | null;
logoDarkUrl: string | null;
hasDedicatedFavicon: boolean;
}
// This tenant has no emails/phones/addresses filled in yet, so the loose
// shapes are read defensively at render time.
export interface BusinessInfo {