A custom landing page
src/pages/LandingPage.tsx is presentation and yours to replace. The only things
worth keeping are the hero fallbacks, because a merchant edits those fields under
Settings → Landing page and expects them to show up, and the two catalogue
helpers, because they already know which collection is the featured one and which
collections are visible.
This version drops the collection tiles, adds a “new this week” row of the most recently added live products, and a short story block under the hero.
src/pages/LandingPage.tsx
Section titled “src/pages/LandingPage.tsx”import { Link } from "react-router-dom";import { Button } from "antd";import { getFeaturedProducts, getVisibleCollections } from "@shared/catalog";import { PageWrapper } from "@/components/layout/PageWrapper";import { ProductList } from "@/components/product/ProductList";import { useStore } from "@/lib/useStore";import { assetUrl } from "@/lib/store-source";import styles from "./LandingPage.module.css";
export function LandingPage() { const store = useStore(); const { hero } = store; const featured = getFeaturedProducts(store); const collections = getVisibleCollections(store);
// The snapshot lists live products in display order; the merchant's order is // the best available "newest" without adding a column for it. const latest = store.products.slice(0, 4);
const href = hero.buttonHref ?? "/shop"; const cta = ( <Button type="primary" size="large"> {hero.buttonLabel ?? "Shop everything"} </Button> );
return ( <> <section className={styles.hero} style={hero.image ? { backgroundImage: `url(${assetUrl(hero.image.path)})` } : undefined} > <div className={styles.heroInner}> <h1>{hero.heading ?? store.name}</h1> {hero.text && <p>{hero.text}</p>} {href.startsWith("/") ? ( <Link to={href}>{cta}</Link> ) : ( <a href={href} target="_blank" rel="noopener noreferrer">{cta}</a> )} </div> </section>
<PageWrapper width="wide"> <section className={styles.story}> <h2>Made in small batches</h2> <p> Everything here is made to order in the studio. Most things ship within a week; the <Link to="/shipping">shipping page</Link> has the details. </p> </section>
{latest.length > 0 && ( <section className={styles.section}> <header className={styles.sectionHead}> <h2>New this week</h2> <Link to="/shop?sort=newest">View all →</Link> </header> <ProductList products={latest} currency={store.currency} locale={store.locale} /> </section> )}
{featured.length > 0 && ( <section className={styles.section}> <header className={styles.sectionHead}> <h2>Featured</h2> </header> <ProductList products={featured} collection="Featured" currency={store.currency} locale={store.locale} /> </section> )}
{collections.length > 0 && ( <nav className={styles.collections} aria-label="Collections"> {collections.map((c) => ( <Link key={c.id} to={`/collection/${c.slug}`}>{c.name}</Link> ))} </nav> )} </PageWrapper> </> );}What this is careful about
Section titled “What this is careful about”- The hero fallbacks are preserved. Heading to the store name, no paragraph
when there is none, “Shop everything” to
/shop. A merchant who sets nothing gets what they had. - The button branches on path versus URL. Handing
https://…to a routerLinkmakes react-router try to resolve it as an in-app route and lands on the 404 page.heroHrefSchemahas already refused anything that is neither, so this is a two-way branch, not validation. - Prices are never touched here.
ProductListformats them from the catalogue. - The shipping link points at a page, which the merchant writes under
Pages, so the copy can change without a deploy. If no page has that slug the
link 404s, which is a reason to prefer
store.pageswhen you know the slug at render time. assetUrlbuilds the image URL, so it resolves under either image driver.