Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7fb6860
PDP: render custom.reviews stars + count above title
laugharn Apr 29, 2026
1a089f7
Merge main; replace custom.reviews metafield with cacheLife max stub …
laugharn Apr 29, 2026
64f8b56
Populate demo nav + footer menus with placeholder # links
laugharn Apr 29, 2026
b273122
Rebrand to Hausu, add Bricolage Grotesque display font, tweak PDP UI
laugharn Apr 29, 2026
0b56747
Add vibrant blue link token; apply to prose anchors
laugharn Apr 29, 2026
60f9fbb
Apply --link blue to nav, footer, primary CTAs, and banner button
laugharn Apr 29, 2026
364fb6c
Switch link color to navy and apply across nav chrome
laugharn Apr 29, 2026
88f025b
Add navy "Shipping to <postal code>" top bar above the navbar
laugharn Apr 29, 2026
d1c8329
PDP: 50/50 split with thumbnail+featured gallery on desktop
laugharn Apr 29, 2026
e69cb52
PDP gallery: drop rounding, match variant-thumbnail active border
laugharn Apr 29, 2026
53c3f9a
TopBar: add right-justified market selector (UI only)
laugharn Apr 29, 2026
c5e923e
PDP: AI-generated customer reviews section between detail and recs
laugharn Apr 29, 2026
1893359
ProductReviewsSection: drop connection() and review-card top border
laugharn Apr 29, 2026
63da3ee
ProductReviewsSection: build resilience for AI Gateway failures
laugharn Apr 29, 2026
6ff7c8f
PDP: complementary products section between buy buttons and description
laugharn Apr 29, 2026
8d77bbd
PDP: pin gallery, scroll info column
laugharn Apr 29, 2026
02b1aac
ComplementaryProductsSection: card shell matching agent cart summary
laugharn Apr 29, 2026
965ccd8
ComplementaryProductsSection: cart-style stack; full-bleed PDP descri…
laugharn Apr 29, 2026
6cdc7d2
Merge remote-tracking branch 'origin/main' into demos/2026-04-29-ent
laugharn Apr 29, 2026
b686d39
Title-case "Pairs Well With"; unify product-title weight to font-semi…
laugharn Apr 29, 2026
d2e1b02
PDP: include reviews + related-products skeletons in page-level fallback
laugharn Apr 29, 2026
d194990
Add placeholder newsletter intake section above the footer
laugharn Apr 29, 2026
090adcc
Move newsletter into footer; add placeholder social-link icons
laugharn Apr 29, 2026
c6dd6e2
Hero: "The Outdoor Edit", chartreuse overlay, Shopify backdrop
laugharn Apr 29, 2026
e526dd3
Home: rugs banner under featured products + restored hero description
laugharn Apr 29, 2026
e52c5ce
Drop banner subheadlines; recolor chat pill to navy/white
laugharn Apr 29, 2026
9107525
Home: bookcases slider + bedroom sale banner under the rugs section
laugharn Apr 29, 2026
89d2a93
Home: search-driven bookcases slider; swap rug/bedroom banner order
laugharn Apr 29, 2026
38f155b
Scope navy chat-pill text color to the trigger only
laugharn Apr 29, 2026
34f6ea7
ProductReviewSnippets: switch cache to "use cache: remote"
laugharn Apr 29, 2026
69a789d
ProductReviewsSection: re-add connection() guard for testing
laugharn Apr 29, 2026
0a1400d
ProductReviewsSection: swap connection() for unstable_io()
laugharn Apr 30, 2026
5312c6c
ProductReviewSnippets: switch model to openai/gpt-5.4-nano
laugharn Apr 30, 2026
18f126e
Merge remote-tracking branch 'origin/main' into demos/2026-04-29-ent
laugharn Apr 30, 2026
0e7d9f4
Hero banner: optional autoplay video background
laugharn Apr 30, 2026
890ede8
Hero banner: use AutoPlayVideo with poster fallback for the video path
laugharn Apr 30, 2026
2c7f39f
Hero: drop the unrelated poster image, fall back to native video frame
laugharn Apr 30, 2026
9c91906
Hero: add the boomerang's first frame as the poster image
laugharn Apr 30, 2026
ff1026d
ProductCard: optional aspect ratio (landscape/portrait/square)
laugharn Apr 30, 2026
2484b30
Bump dependencies
laugharn Apr 30, 2026
71b176f
Merge branch 'main' into demos/2026-04-29-ent
laugharn Apr 30, 2026
4fb279c
ProductCardSkeleton: reserve two title lines + price
laugharn Apr 30, 2026
108382f
AutoPlayVideo: retry play() on canplay to fix cold-load race
laugharn Apr 30, 2026
d088635
Merge branch 'main' into demos/2026-04-29-ent
laugharn May 1, 2026
7c4540c
Image optimization: trim breakpoints, unify small thumbnails (test on…
laugharn May 1, 2026
f199950
Nav: center the logomark and size it to match slider headings
laugharn May 1, 2026
21424ca
Nav: add Hausu logomark next to the wordmark
laugharn May 1, 2026
1db552c
Revert "Nav: add Hausu logomark next to the wordmark"
laugharn May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/template/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { Page } from "@/components/ui/page";

export const metadata: Metadata = {
title: "About",
description: "Learn about Vercel Shop, a Next.js storefront template for Shopify.",
description: "Learn about Hausu, a Next.js storefront template for Shopify.",
};

export default function AboutPage() {
return (
<Page>
<Container className="max-w-2xl">
<article className="prose prose-neutral prose-headings:font-semibold prose-headings:tracking-tight">
<h1>About Vercel Shop</h1>
<h1>About Hausu</h1>
<p>
Vercel Shop is a Next.js storefront that connects to Shopify. You get product pages,
Hausu is a Next.js storefront that connects to Shopify. You get product pages,
collections, a cart, and search out of the box. Point it at your store and you're
selling.
</p>
Expand Down
1 change: 1 addition & 0 deletions apps/template/app/collections/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const unstable_instant = {
sort: null,
},
cookies: [{ name: "shopify_cartId", value: null }],
headers: [["x-vercel-ip-postal-code", null]],
},
],
};
Expand Down
10 changes: 10 additions & 0 deletions apps/template/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
--breakpoint-4xl: 140rem; /* 2240px */

--color-background: var(--background);
--color-chartreuse: var(--chartreuse);
--color-foreground: var(--foreground);
--color-link: var(--link);
--color-link-foreground: var(--link-foreground);
--color-shop: #5A31F4;
--font-display: var(--font-bricolage);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--text-xxs: 0.625rem;
Expand Down Expand Up @@ -72,7 +76,10 @@
); /* 64px = h-16 single nav row + safe area */
--radius: 0.625rem;
--background: #ffffff;
--chartreuse: #ccff00;
--foreground: #010101;
--link: #0a1f4b;
--link-foreground: #ffffff;
--card: #ffffff;
--card-foreground: #010101;
--popover: #ffffff;
Expand Down Expand Up @@ -112,6 +119,9 @@
letter-spacing: -0.025em; /* tracking-tight */
font-weight: 600; /* font-semibold */
}
.prose :where(a):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
color: var(--link);
}
}

@layer base {
Expand Down
13 changes: 10 additions & 3 deletions apps/template/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "./globals.css";
import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations } from "next-intl/server";
import { Geist, Geist_Mono } from "next/font/google";
import { Bricolage_Grotesque, Geist, Geist_Mono } from "next/font/google";
import { Suspense } from "react";

import { ActionBar } from "@/components/action-bar";
Expand All @@ -11,6 +11,7 @@ import { CartProvider } from "@/components/cart/context";
import { CartOverlayWithAddress } from "@/components/cart/overlay-with-address";
import { Footer } from "@/components/footer";
import { Nav } from "@/components/nav";
import { TopBar } from "@/components/nav/top-bar";
import { SiteSchema } from "@/components/schema/site-schema";
import { siteConfig } from "@/lib/config";
import { getLocale } from "@/lib/params";
Expand All @@ -26,6 +27,11 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});

const bricolage = Bricolage_Grotesque({
variable: "--font-bricolage",
subsets: ["latin"],
});

export default async function RootLayout({ children }: LayoutProps<"/">) {
const [locale, messages, t] = await Promise.all([
getLocale(),
Expand All @@ -37,7 +43,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
<html lang={locale}>
<head />
<body
className={`${geistSans.variable} ${geistMono.variable} flex min-h-dvh flex-col font-sans antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${bricolage.variable} flex min-h-dvh flex-col font-sans antialiased`}
>
<a
href="#main-content"
Expand All @@ -48,6 +54,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
<SiteSchema locale={locale} />
<NextIntlClientProvider locale={locale} messages={messages}>
<CartProvider initialCart={null}>
<TopBar />
<Nav locale={locale} />
<main id="main-content" className="flex flex-1 flex-col min-w-0">
{children}
Expand All @@ -72,7 +79,7 @@ export const generateMetadata = async (): Promise<Metadata> => {
return {
alternates: buildAlternates({ pathname: "/" }),
description: t("defaultDescription"),
generator: "Vercel Shop",
generator: siteConfig.name,
metadataBase: new URL(siteConfig.url),
openGraph: {
images: [{ url: "/og-default.png", width: 1200, height: 630 }],
Expand Down
4 changes: 2 additions & 2 deletions apps/template/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export default async function NotFoundError() {
<Page className="flex flex-1 flex-col">
<Container className="flex flex-1 flex-col items-center justify-center text-center">
<div className="flex flex-col items-center text-center gap-2.5">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
{t("notFound")}
</h1>
<p className="text-sm md:text-base text-muted-foreground max-w-xl">{t("notFoundDesc")}</p>
<Link
href="/search"
className="inline-flex items-center justify-center h-12 px-8 rounded-lg text-sm font-medium bg-foreground text-background hover:bg-foreground/90 transition-colors"
className="inline-flex items-center justify-center h-12 px-8 rounded-lg text-sm font-medium bg-link text-link-foreground hover:bg-link/90 transition-colors"
>
{t("continueShopping")}
</Link>
Expand Down
75 changes: 73 additions & 2 deletions apps/template/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Suspense } from "react";

import { FeaturedProducts } from "@/components/product/products-grid";
import { ProductsSlider } from "@/components/product/products-slider";
import { RelatedProductsSectionSkeleton } from "@/components/product/related-products-section";
import { BannerSection } from "@/components/sections/banner-section";
import { Container } from "@/components/ui/container";
import { Page } from "@/components/ui/page";
import { Sections } from "@/components/ui/sections";
import { siteConfig } from "@/lib/config";
import type { Locale } from "@/lib/i18n";
import { getLocale } from "@/lib/params";
import { buildAlternates, buildOpenGraph } from "@/lib/seo";
import { getCatalogProducts } from "@/lib/shopify/operations/products";

export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("seo");
Expand Down Expand Up @@ -40,8 +45,17 @@ export default async function HomePage() {
<BannerSection
hero={{
id: "homepage-hero",
headline: "Agentic Infrastructure for Commerce",
subheadline: "A production-ready, agent-friendly Shopify storefront built on Next.js.",
headline: "The Outdoor Edit",
subheadline: null,
backgroundImage: {
url: "https://cdn.shopify.com/s/files/1/0968/7236/6467/files/boomerang-poster.jpg?v=1777564966",
alt: "The Outdoor Edit",
width: 1280,
height: 720,
},
backgroundVideo: {
url: "https://cdn.shopify.com/videos/c/o/v/db1572ad04ae4ecbad692430c6269fcf.mp4",
},
ctaText: "Browse the Catalog",
ctaLink: "/search",
}}
Expand All @@ -55,7 +69,64 @@ export default async function HomePage() {
collectionUrl="/search"
/>
</Container>

<BannerSection
headingLevel="h2"
hero={{
id: "homepage-bedroom-sale",
headline: "The Semi-Annual Bedroom Sale",
subheadline: null,
backgroundImage: {
url: "https://cdn.shopify.com/s/files/1/0968/7236/6467/files/ba52372f-eace-4eca-a867-d92aed5bab5b-color-smoked-ash-oxWB0cyHNJnTlCbm8GGURF572Y8LgF.png?v=1776523451",
alt: "The Semi-Annual Bedroom Sale",
width: 1920,
height: 640,
},
ctaText: "Shop the Sale",
ctaLink: "/collections/bedroom",
}}
/>

<Container>
<Suspense
fallback={
<RelatedProductsSectionSkeleton title="Shop Bookcases" aspectRatio="portrait" />
}
>
<BookcasesSlider locale={locale} />
</Suspense>
</Container>

<BannerSection
headingLevel="h2"
hero={{
id: "homepage-rugs",
headline: "Eye-Catching Interior Rugs",
subheadline: null,
backgroundImage: {
url: "https://cdn.shopify.com/s/files/1/0968/7236/6467/files/06d5df4c-347b-4b6b-91d1-c6bbade245ba-2-GOxxEWNRZj0NgqxVk1TPPyAXvdRi0c.png?v=1776523437",
alt: "Eye-catching interior rugs",
width: 1920,
height: 640,
},
ctaText: "Shop Rugs",
ctaLink: "/collections/rugs",
}}
/>
</Sections>
</Page>
);
}

async function BookcasesSlider({ locale }: { locale: Locale }) {
const { products } = await getCatalogProducts({ query: "bookcase", limit: 8, locale });
if (products.length === 0) return null;
return (
<ProductsSlider
title="Shop Bookcases"
products={products}
locale={locale}
aspectRatio="portrait"
/>
);
}
5 changes: 5 additions & 0 deletions apps/template/app/products/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { notFound } from "next/navigation";

import { ProductDetailPage } from "@/components/product-detail/product-detail-page";
import { getLocale } from "@/lib/params";
import { getProductReviews } from "@/lib/reviews/server";
import { buildAlternates, buildOpenGraph } from "@/lib/seo";
import { getCatalogProducts, getProduct } from "@/lib/shopify/operations/products";

Expand Down Expand Up @@ -69,6 +70,7 @@ export const unstable_instant = {
params: { handle: "__placeholder__" },
searchParams: { variantId: "1" },
cookies: [{ name: "shopify_cartId", value: null }],
headers: [["x-vercel-ip-postal-code", null]],
},
],
};
Expand All @@ -89,11 +91,14 @@ export default async function ProductPage({
getProduct(handle, locale).catch(() => notFound()),
);

const reviewsPromise = handlePromise.then((handle) => getProductReviews(handle));

const variantIdPromise = searchParams.then((sp) => sp?.variantId as string | undefined);

return (
<ProductDetailPage
productPromise={productPromise}
reviewsPromise={reviewsPromise}
locale={locale}
variantIdPromise={variantIdPromise}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/template/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const unstable_instant = {
sort: null,
},
cookies: [{ name: "shopify_cartId", value: null }],
headers: [["x-vercel-ip-postal-code", null]],
},
],
};
Expand Down Expand Up @@ -104,7 +105,7 @@ export default async function SearchPage({ searchParams }: PageProps<"/search">)
<FilterTransitionProvider>
<Sections className="gap-5">
<div>
<h1 className="text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
<Link href="/search">{t("title")}</Link>
<Suspense fallback={null}>
<SearchQueryLabel searchParamsPromise={searchParams} />
Expand Down
4 changes: 3 additions & 1 deletion apps/template/components/account/page-header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export function AccountPageHeader({ title, description }: { title: string; description?: string }) {
return (
<div>
<h1 className="text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">{title}</h1>
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
{title}
</h1>
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/template/components/action-bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function ActionBar({ children }: ActionBarProps) {
if (!children) return null;

return (
<div className="fixed bottom-5 right-5 z-40 flex items-center bg-input/80 backdrop-blur-md h-12 rounded-full shadow-[0px_2px_4px_0px_rgba(90,90,90,0.30)] outline -outline-offset-1 outline-border/35 px-2">
<div className="fixed bottom-5 right-5 z-40 flex items-center bg-link backdrop-blur-md h-12 rounded-full shadow-[0px_2px_4px_0px_rgba(90,90,90,0.30)] px-2">
{children}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/template/components/agent/agent-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export function AgentButton() {
<button
ref={triggerRef}
type="button"
className="flex items-center gap-1.5 px-2 py-1"
className="flex items-center gap-1.5 px-2 py-1 text-link-foreground"
onClick={() => setOpen((prev) => !prev)}
aria-expanded={open}
>
<MessageCircle className="size-4 text-primary" />
<MessageCircle className="size-4" />
<span className="sr-only">Agent</span>
</button>
{open && <AgentPanel open={open} onOpenChange={setOpen} triggerRef={triggerRef} />}
Expand Down
6 changes: 4 additions & 2 deletions apps/template/components/cart-page/empty-cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export async function Empty() {

return (
<div className="flex flex-col items-center justify-center gap-5 py-10 px-5">
<h2 className="text-2xl sm:text-3xl font-semibold tracking-tighter">{t("empty")}</h2>
<h2 className="font-display text-2xl sm:text-3xl font-semibold tracking-tighter">
{t("empty")}
</h2>
<Link
href="/"
className="inline-flex items-center justify-center h-12 px-8 rounded-lg text-sm font-medium bg-foreground text-background hover:bg-foreground/90 transition-colors"
className="inline-flex items-center justify-center h-12 px-8 rounded-lg text-sm font-medium bg-link text-link-foreground hover:bg-link/90 transition-colors"
>
{t("continueShopping")}
</Link>
Expand Down
2 changes: 1 addition & 1 deletion apps/template/components/cart-page/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function Header() {

return (
<div className="flex items-center gap-2.5">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight">
{t("shoppingCart")}
</h1>
{count > 0 && (
Expand Down
4 changes: 2 additions & 2 deletions apps/template/components/cart-page/summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function CheckoutLink({
}, []);

const baseClassName =
"flex items-center justify-center w-full h-12 rounded-lg text-sm font-medium bg-foreground text-background transition-colors";
"flex items-center justify-center w-full h-12 rounded-lg text-sm font-medium bg-link text-link-foreground transition-colors";

if (isUpdatingCart || isCheckingOut) {
return (
Expand All @@ -48,7 +48,7 @@ function CheckoutLink({
return (
<button
type="button"
className={cn(baseClassName, "hover:bg-foreground/90 cursor-pointer")}
className={cn(baseClassName, "hover:bg-link/90 cursor-pointer")}
onClick={async () => {
setIsCheckingOut(true);
const { checkoutUrl: url } = await prepareCheckoutAction();
Expand Down
6 changes: 3 additions & 3 deletions apps/template/components/cart/overlay-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ export function OverlayContent({ locale }: OverlayContentProps) {
if (!displayCart || displayCart.lines.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full px-5 text-center">
<h3 className="text-2xl font-semibold tracking-tighter mb-6">{t("empty")}</h3>
<h3 className="font-display text-2xl font-semibold tracking-tighter mb-6">{t("empty")}</h3>
<Button
onClick={() => {
setOverlayOpen(false);
router.push("/");
}}
className="h-12 px-8 bg-foreground text-background hover:bg-foreground/90"
className="h-12 px-8 bg-link text-link-foreground hover:bg-link/90"
>
{t("continueShopping")}
</Button>
Expand All @@ -104,7 +104,7 @@ export function OverlayContent({ locale }: OverlayContentProps) {
{/* Checkout Button */}
<Button
onClick={handleCheckout}
className="w-full h-12 justify-center bg-foreground text-background hover:bg-foreground/90"
className="w-full h-12 justify-center bg-link text-link-foreground hover:bg-link/90"
disabled={isCheckingOut || isUpdatingCart}
aria-label={t("proceedToCheckout")}
>
Expand Down
Loading