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:
co-authored by
Claude Sonnet 5
parent
8e82370b64
commit
7089078b4c
@@ -31,7 +31,7 @@ fallbacks should update it.
|
||||
| `/website/sliders`, `/website/category-groups` | **empty** |
|
||||
| `/website/static-images` | **filled** — `slider` (2), `categories` (8 of 15), `one/two/three-banner-bg` all have real images. Verified live and rendering correctly; if a future check finds otherwise, suspect a stale build/deploy before suspecting the wiring. |
|
||||
| `/blogs` | **empty** (module enabled) |
|
||||
| `/website/business-info` | phones/addresses/socialMedia still empty; `logoUrl`/`faviconUrl` are now set but point to a **different business's** mark (see "Brand") |
|
||||
| `/website/business-info` | phones/addresses/socialMedia still empty; `logoUrl`/`faviconUrl` point to a **different business's** mark and are deliberately unused (see "Brand") |
|
||||
| `/products/by-id/{id}/technical-info` | `{ form: null, values: [] }` — empty on every product checked so far |
|
||||
| `/comments` | **empty**, but `POST` works (no bearer required by the API itself) |
|
||||
|
||||
@@ -42,7 +42,6 @@ fallbacks should update it.
|
||||
| No prices | `src/lib/demoPricing.ts`, gated by `NEXT_PUBLIC_DEMO_PRICING=1` — deterministic price per product id | store-items exist |
|
||||
| No blog posts | `src/data/fallbackBlogs.ts` — the real posts from moalem.shop/articles, shown on the homepage and `/blog` as square-thumbnail cards that link to a real local `/blog/{id}/{slug}` page (`getFallbackBlogById()`); that page shows title/category/image plus a "coming soon" note since only those three fields are real, no scraped body content | `/blogs` returns items |
|
||||
| No category art past slot position 8 | placeholder SVGs in `public/images/categories/{id}.svg`, generated by `scripts/gen-category-placeholders.mjs` for every category at every level, not just level-2 | the `categories` slot has an image for that position |
|
||||
| Wrong logo/favicon in API | `getFavicon()` is wired live (see "Brand") but the uploaded asset itself is wrong; no code fallback needed once the dashboard upload is fixed | the correct asset is uploaded |
|
||||
|
||||
## Brand
|
||||
|
||||
@@ -50,21 +49,15 @@ Colors taken from the live moalem.shop theme, not invented:
|
||||
primary `#E51E22`, dark `#B10123`, ink `#2E2E2E`, body `#7C7C7C`.
|
||||
Fonts: IRANYekan (fa, local `public/fonts`) + Montserrat (latin, next/font).
|
||||
|
||||
- Logo/favicon: `getFavicon()` in `src/lib/api.ts` reads
|
||||
`GET /tenants/moalem.shop/website/favicon` — the documented single source —
|
||||
and `Header.tsx`/`Footer.tsx`/`generateMetadata()` in `layout.tsx` all use
|
||||
it, falling back to the bundled `/images/logo.png` / `/favicon.png` only
|
||||
when the API field is null. **As of 2026-09-06 the dashboard has a logo/
|
||||
favicon uploaded, but it's the wrong business's** (a home-appliances store's
|
||||
mark, not موبایل معلم) — the wiring is correct and will pick up the right
|
||||
asset automatically the moment it's replaced in the dashboard; don't
|
||||
"fix" this by hardcoding the local fallback again.
|
||||
- Header (desktop) logo renders at `h-[70px]`; footer logo stays at
|
||||
`h-[108px]` (3x the original `h-9`/36px, on request) — the two were sized
|
||||
independently, on request, so don't assume they should match. If the wrong
|
||||
logo above still looks small at either size, it's because that specific
|
||||
PNG has a lot of baked-in transparent padding around the mark —
|
||||
`object-contain` can't fix that, only the replacement asset can.
|
||||
- Logo: **bundled, not from the API** — `public/images/logo.png` (200x48
|
||||
wordmark, supplied by the business), referenced via `LOGO_SRC`/`LOGO_WIDTH`/
|
||||
`LOGO_HEIGHT` in `src/lib/config.ts` by `Header.tsx`, `Footer.tsx` and the
|
||||
og:image in `layout.tsx`. This was an explicit decision (2026-09-20): the
|
||||
API's `/website/favicon` returned another business's mark, so
|
||||
`getFavicon()` was removed entirely — don't re-add it. Tab icon is the
|
||||
bundled `public/favicon.png`. To change the logo, replace the file (keep
|
||||
the same name; update `LOGO_WIDTH`/`LOGO_HEIGHT` if its shape changes).
|
||||
- Header (desktop) and footer logos both render at `h-[70px] w-auto`.
|
||||
- Contact data (phone, mobile, branch addresses, Instagram/Telegram) in
|
||||
`src/data/site.ts` is real, pulled from the live theme's footer — `GET
|
||||
/website/business-info` still returns empty `phoneNumbers`/`addresses`/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.3 KiB |
+10
-23
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user