Skip to main content

Store Websites

The BookWish stores website is a Next.js 14+ application that provides multi-tenant storefronts for independent bookstores. Each store gets a branded website showcasing their inventory, book clubs, and reading programs.

Architecture

Deployment

  • Domain: bookwish.shop
  • Platform: Vercel
  • Framework: Next.js 14+ with App Router
  • Backend API: Railway (bookwishmonorepo-production.up.railway.app)
  • Main App: bookwish.io (Flutter app)

Directory Structure

stores/
├── app/
│ ├── [storeSlug]/ # Dynamic store routes
│ │ ├── books/
│ │ │ ├── [bookId]/page.tsx # Individual book detail
│ │ │ └── page.tsx # Browse all books
│ │ ├── programs/page.tsx # Book clubs & challenges
│ │ ├── subscription/page.tsx # Store subscription management
│ │ ├── layout.tsx # Store-level layout
│ │ └── page.tsx # Store homepage
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Directory/landing page
│ └── not-found.tsx # 404 page
├── components/
│ ├── BookCard.tsx # Book display card
│ ├── StoreHeader.tsx # Store navigation header
│ ├── StoreFooter.tsx # Store footer
│ ├── add-to-wishlist-button.tsx
│ ├── wishlist-picker.tsx
│ └── subscription-buttons.tsx
├── lib/
│ ├── api.ts # API client
│ ├── config.ts # Configuration
│ └── auth-context.tsx # Client-side auth
└── next.config.ts

Multi-Tenant Implementation

Dynamic Routing

The application uses Next.js dynamic routes with [storeSlug] to serve multiple stores from a single deployment:

// Route pattern: /{storeSlug}/...
// Example: /mycoolbookstore/books

interface StorePageProps {
params: Promise<{ storeSlug: string }>;
}

export default async function StorePage({ params }: StorePageProps) {
const { storeSlug } = await params;
const store = await api.getStoreBySlug(storeSlug);

if (!store || !store.websiteEnabled) {
notFound();
}
// ... render store
}

Store Verification

Each store page verifies:

  1. Store exists in database
  2. websiteEnabled flag is true
  3. Returns 404 if conditions not met

Theming System

Dynamic Branding

Each store can customize:

  • Primary color: Applied via CSS custom properties
  • Logo: Store logo image URL
  • Banner: Hero banner image
  • Description: Store description text

CSS Custom Properties

<div
className="store-branded"
style={{
"--dynamic-primary": store.primaryColor || "#1a1a1a",
} as React.CSSProperties}
>

Components reference the custom property:

color: var(--store-primary);
background-color: var(--dynamic-primary);

Pages & Features

1. Store Homepage (/[storeSlug])

  • Hero section with store banner/gradient
  • Store info (location, phone, email)
  • Featured books grid (8 books)
  • Dynamic metadata generation

2. Browse Books (/[storeSlug]/books)

  • Paginated book grid (24 per page)
  • Search by title/author
  • Category filtering support
  • Server-side rendering for SEO
  • Responsive grid (2 cols mobile → 6 cols desktop)

3. Book Detail (/[storeSlug]/books/[bookId])

  • Full book information
  • Cover image display
  • Price and condition (new/like_new/good/fair)
  • Stock availability
  • "Buy with BookWish" CTA linking to main app
  • Add to wishlist functionality
  • Breadcrumb navigation

4. Read With Us (/[storeSlug]/programs)

  • Book clubs showcase
  • Reading challenges display
  • Community engagement features
  • CTA to download BookWish app

5. Subscription Management (/[storeSlug]/subscription)

  • BookWish Bookstore tier subscription
  • $49.99/month pricing
  • Subscription status display
  • Stripe integration for payments
  • Feature list display

API Integration

API Client (lib/api.ts)

Centralized API client with type-safe methods:

interface Store {
id: string;
name: string;
slug: string;
description?: string;
logoUrl?: string;
bannerUrl?: string;
primaryColor?: string;
address?: Address;
phone?: string;
email?: string;
hours?: Record<string, string>;
websiteEnabled: boolean;
}

class ApiClient {
async getStoreBySlug(slug: string): Promise<Store | null>
async getStoreInventory(storeSlug: string, options?): Promise<PaginatedInventory>
async getInventoryItem(storeSlug: string, itemId: string): Promise<InventoryItem | null>
}

Data Fetching Patterns

  • Server Components: Direct API calls with fetch() and caching
  • Revalidation: 5-minute cache (next: { revalidate: 300 })
  • No-store: For user-specific data like subscriptions

Authentication Integration

Cross-Domain Auth

The stores site integrates with the main BookWish app authentication:

// Checks for token in:
// 1. localStorage (bookwish_token)
// 2. Cookies (bookwish_token)

function getToken(): string | null {
const localToken = localStorage.getItem('bookwish_token');
if (localToken) return localToken;

// Check cookies for cross-domain auth
const cookies = document.cookie.split(';');
// ...
}

Auth Context (lib/auth-context.tsx)

Client-side context provider:

  • User session management
  • Wishlist data fetching
  • Token validation
  • Unauthenticated users can browse but can't add to wishlist

Components

BookCard

Reusable book display component:

  • Cover image with fallback
  • Title and authors
  • Price formatting
  • Condition badge for used books
  • Hover effects
  • Links to book detail page

StoreHeader

Navigation header with:

  • Store logo/name
  • Navigation links (Browse Books, Read With Us)
  • "Powered by BookWish" link
  • Sticky positioning with backdrop blur

StoreFooter

Footer with store information

AddToWishlistButton

Client component for wishlist functionality:

  • Shows "Sign in" for unauthenticated users
  • Opens wishlist picker for authenticated users
  • Deep links to main app login if needed

Metadata & SEO

Dynamic Metadata

Each page generates metadata based on content:

export async function generateMetadata({ params }: Props) {
const { storeSlug } = await params;
const store = await api.getStoreBySlug(storeSlug);

if (!store) {
return { title: "Store Not Found" };
}

return {
title: store.name,
description: store.description || `Shop books at ${store.name}`,
};
}

Configuration (lib/config.ts)

export const config = {
appName: "BookWish Stores",
apiUrl: process.env.NEXT_PUBLIC_API_URL,
mainAppUrl: process.env.NEXT_PUBLIC_MAIN_APP_URL || "https://bookwish.io",
storesBaseUrl: process.env.NEXT_PUBLIC_STORES_URL || "https://bookwish.shop",
companyName: "Willow Tree Creative LLC",
supportEmail: "support@bookwish.io",

getStoreUrl(storeSlug: string): string {
return `${this.storesBaseUrl}/${storeSlug}`;
},
};

Landing Page

The root page (app/page.tsx) serves as a directory for discovering bookstores:

  • Hero with call-to-action
  • "How it Works" section
  • "For Bookstores" promotional section
  • Links to list your store ($49.99/month tier)

Image Optimization

Uses Next.js <Image> component for:

  • Automatic optimization
  • Responsive sizing
  • Lazy loading
  • WebP/AVIF conversion

Responsive Design

Mobile-first approach:

  • Tailwind CSS utility classes
  • Responsive grids: grid-cols-2 md:grid-cols-4 lg:grid-cols-6
  • Touch-friendly UI elements
  • Optimized for phone, tablet, desktop

Performance Optimizations

  1. Server-Side Rendering: All store pages are SSR
  2. Incremental Static Regeneration: 5-minute revalidation
  3. Image Optimization: Next.js Image component
  4. Code Splitting: Automatic per-route splitting
  5. Edge Caching: Vercel edge network

Future Enhancements

  • Custom domain support per store
  • Advanced theme customization
  • Store analytics dashboard
  • Enhanced search with filters
  • Store hours display
  • Events calendar