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:
- Store exists in database
websiteEnabledflag is true- 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
- Server-Side Rendering: All store pages are SSR
- Incremental Static Regeneration: 5-minute revalidation
- Image Optimization: Next.js Image component
- Code Splitting: Automatic per-route splitting
- 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