Audiofast (audiofast.pl) is a Polish high-end audio equipment distributor based in Łódź, operating for over 20 years. They carry premium brands like Wilson Audio, dCS, Gryphon, Audio Research, Dan D'Agostino, Aurender, Shunyata, Ayre, PrimaLuna, Weiss Engineering, Usher Audio, GoldenEar, Keces Audio, and StromTank. Their offerings include amplifiers, speakers, DACs, streamers, cables, and power conditioners — products at the top tier of audiophile equipment.
The website serves as their digital storefront: a product catalog, brand showcase, blog/review hub, and contact point for potential buyers. It is not a traditional e-commerce store with a checkout flow — instead it presents products with pricing and provides inquiry forms for purchase consultation.
| Layer | Technology |
|---|---|
| Monorepo | Turborepo v2.6.0 |
| Package Manager | Bun v1.1.42 |
| Frontend | Next.js 16 (App Router), React 19, TypeScript |
| Styling | SCSS Modules with CSS variables |
| CMS | Sanity v5 (GROQ queries, Portable Text) |
| Database | Supabase (PostgreSQL) — product pricing only |
| Microsoft Graph API + React Email templates | |
| Newsletter | Mailchimp |
| Analytics | Google Tag Manager + Meta Pixel (dual tracking) |
| Maps | Leaflet (react-leaflet) for store locators |
| Deployment | Vercel (web), Sanity hosting (studio) |
| CI/CD | GitHub Actions (Sanity deploy), Vercel (web auto-deploy) |
audiofast/
├── apps/
│ ├── web/ # Next.js 16 website (the public-facing site)
│ │ ├── src/
│ │ │ ├── app/ # App Router — pages, layouts, API routes
│ │ │ ├── components/ # All React components
│ │ │ ├── emails/ # React Email templates
│ │ │ ├── generated/ # Auto-generated files (redirects map)
│ │ │ └── global/ # Shared utilities, config, integrations
│ │ ├── public/ # Static assets
│ │ ├── scripts/ # Build-time scripts (redirect generation)
│ │ ├── next.config.ts # Next.js configuration
│ │ └── vercel.json # Vercel deployment config
│ │
│ └── studio/ # Sanity CMS Studio (content management)
│ ├── schemaTypes/ # Content model (document, block, object schemas)
│ ├── plugins/ # Custom Sanity plugins (bulk-actions-table)
│ ├── tools/ # Custom studio tools (newsletter, comparator)
│ ├── components/ # Custom UI components (technical data editor, filters)
│ ├── scripts/ # Migration scripts (products, brands, reviews, etc.)
│ ├── actions/ # Custom document actions (denormalization, unpublish)
│ ├── structure.ts # Studio desk structure definition
│ └── sanity.config.ts # Sanity configuration
│
├── packages/
│ ├── eslint-config/ # Shared ESLint configurations
│ └── typescript-config/ # Shared TypeScript configurations
│
├── turbo.json # Turborepo pipeline configuration
├── package.json # Root workspace config
├── tsconfig.json # Root TypeScript config
└── .cursorrules # SCSS/React coding guidelines
All routes use the Next.js App Router with trailing slashes.
| Route | Purpose |
|---|---|
/ |
Homepage (Page Builder) |
/produkty/ |
Products listing with filters |
/produkty/kategoria/[category]/ |
Products filtered by category |
/produkty/[slug]/ |
Product detail page |
/produkty/[slug]/pliki/[pdfKey]/ |
Product PDF download |
/produkty/archiwalne/ |
Archived products |
/marki/ |
Brands listing |
/marki/[slug]/ |
Brand detail page |
/blog/ |
Blog listing |
/blog/kategoria/[category]/ |
Blog filtered by category |
/blog/[slug]/ |
Blog article page |
/recenzje/[slug]/ |
Review page |
/recenzje/pdf/[slug]/ |
Review PDF download |
/recenzje/archiwalne/ |
Orphan reviews |
/porownaj/ |
Product comparison tool |
/certyfikowany-sprzet-uzywany/ |
Certified Pre-Owned page |
/polityka-prywatnosci/ |
Privacy policy |
/regulamin/ |
Terms and conditions |
/[slug]/ |
Dynamic CMS pages (catch-all) |
API Routes:
| Endpoint | Method | Purpose |
|---|---|---|
/api/revalidate/ |
POST | Cache invalidation (Sanity webhooks) |
/api/newsletter/ |
POST | Newsletter subscription (Mailchimp) |
/api/newsletter/generate/ |
POST | Newsletter HTML generation |
/api/contact/ |
POST | Contact & product inquiry forms |
/api/analytics/meta/ |
GET | Analytics metadata |
Generated Files:
sitemap.ts— dynamic sitemap from Sanity contentrobots.ts— robots.txtmanifest.ts— web app manifest
Components are organized by domain, with a clear separation between page-level blocks, shared UI, and feature-specific components.
The site uses a modular Page Builder pattern. Pages in Sanity are composed of an array of blocks, and the PageBuilder component (components/shared/PageBuilder.tsx) maps each block type to its React component. There are 20+ block types:
| Block | Purpose |
|---|---|
HeroCarousel |
Rotating hero banners with slides |
HeroStatic |
Static hero section |
LatestPublication |
Showcases the most recent content |
FeaturedPublications |
Carousel of selected articles/reviews |
FeaturedProducts |
Highlighted products grid |
ProductsCarousel |
Scrollable product carousel |
ProductsListing |
Full product listing (CPO page) |
BrandsMarquee |
Infinite-scrolling brand logo ticker |
BrandsList |
Grid of brand logos |
BrandsByCategoriesSection |
Brands organized by product category |
ImageTextColumns |
Image paired with text columns |
ImageWithVideo |
Image with embedded video |
ImageWithTextBoxes |
Image with overlaid text boxes |
BlurLinesTextImage |
Decorative blur-line effect with text/image |
GallerySection |
Image gallery |
ContactForm |
Contact form with email integration |
ContactMap |
Map showing store locations |
FaqSection |
Expandable FAQ accordion |
TeamSection |
Team member profiles |
PhoneImageCta |
CTA with phone/image |
StepList |
Step-by-step instructional content |
Reusable interface elements: Header, Footer, Breadcrumbs, Button, Input, Checkbox, Switch, Accordion, Pagination, Searchbar, SortDropdown, ProductCard, PublicationCard, ProductGallery, ConfirmationModal, TableOfContent, PillsStickyNav, StoreLocations, BlogAside, EmptyState, Error, and more.
The full product browsing experience: ProductHero, ProductsListing, ProductsListingContainer, ProductsAside (filter sidebar), TechnicalData (specifications table), DownloadSection, ProductInquiryModal, PriceRange, RangeFilter, CustomFiltersBar.
Product comparison feature: ComparisonTable, ComparisonProductCard, ProductSelector, FloatingComparisonBox (persistent widget).
ArticleBody (rich content rendering), BlogListing.
Custom renderers for Sanity Portable Text: images (full/minimal/inline), videos (YouTube/Vimeo), buttons, headings, lists (arrow/circle-numbered), CTA sections, tables, quotes, page breaks, image sliders, review embeds.
JSON-LD structured data generators: OrganizationSchema, WebSiteSchema, ArticleSchema, BlogPostSchema, BrandSchema, CollectionPageSchema.
The application uses a dual-database strategy:
All editorial content lives in Sanity:
- Product information (name, description, images, categories, brand, technical specs, reviews, related products, store availability)
- Brand pages (logo, description, hero images, content, galleries, featured reviews)
- Blog articles (content, categories, authors, publication dates)
- Reviews (content, authors, linked products)
- Pages (modular page builder content)
- Site configuration (navigation, footer, settings, FAQ, team, social media)
- Redirects (old URL → new URL mappings)
Product pricing is stored separately in Supabase (PostgreSQL):
pricing_variants— product models/variants with base pricespricing_option_groups— option groups (dropdown selects, numeric steppers)pricing_option_values— dropdown options with price deltaspricing_numeric_rules— numeric input rules (min, max, step, price-per-step)
This separation allows pricing to be managed independently from content, with its own update cadence.
┌─────────────────┐ GROQ Queries ┌──────────────────┐
│ Sanity CMS │ ◄────────────────────► │ │
│ (Content data) │ │ Next.js App │
└─────────────────┘ │ (Server Comp) │
│ │
┌─────────────────┐ SQL Queries │ │
│ Supabase │ ◄────────────────────► │ │
│ (Pricing data) │ └──────────────────┘
└─────────────────┘ │
│ HTML
▼
┌──────────────────┐
│ Browser │
│ (Client comps) │
└──────────────────┘
Queries live in src/global/sanity/query.ts (~2400+ lines). They use a fragment-based pattern for reusability:
- Fragments:
imageFragment,imageFragmentLite,portableTextFragment,productFragment,brandFragment,publicationFragment— reusable projections composed into larger queries - Page queries:
queryHomePage,queryPageBySlug,queryBlogPostBySlug,queryReviewBySlug,queryProductBySlug,queryBrandBySlug - Listing queries:
queryProductsListingwith sort variants (newest, oldest, priceAsc, priceDesc, orderRank, relevance) - SEO-only queries: lightweight variants (e.g.,
queryProductSeoBySlug) that fetch only metadata fields, reducing payload forgenerateMetadata() - Filter metadata:
queryAllProductsFilterMetadata— a single lightweight query (~80KB for 500+ products) powering client-side filter computation
Products store denormalized fields to avoid expensive dereferencing in GROQ:
denormBrandSlug,denormBrandName— avoidsbrand->sluglookupsdenormCategorySlugs,denormParentCategorySlugs— avoids category reference resolutiondenormFilterKeys— pre-computed filter keys
These fields are automatically recomputed when a product is published (via a custom Sanity publish action) and when brands/categories change (via the revalidation webhook).
The application uses Next.js use cache directive with a sophisticated invalidation strategy:
- Development:
cacheLife('seconds')— fresh data on every request - Production:
cacheLife('weeks')— 30-day stale window, invalidated on demand
Every cached query is tagged with specific identifiers:
product:yamaha-thr10ii— a specific product pagebrand:wilson-audio— a specific brand pageproducts— the products listing pageblog-article:some-slug— a specific blog postproduct-pricing:slug— pricing data for a product
When a Sanity document is edited, the /api/revalidate/ endpoint performs a reverse lookup to find all pages that reference that document:
- Receives webhook from Sanity with the edited document ID
- Queries Sanity:
*[references($id) && _type in [...]]to find referencing documents - Extracts slugs from those documents
- Invalidates only the specific cache tags for affected pages
This avoids the naive approach of invalidating entire categories when a single product changes.
When a brand or category is updated, the revalidation endpoint also triggers denormalization updates — it patches all referencing products with the new denormalized values, ensuring data consistency without manual intervention.
Filtering uses a client-side computation approach for instant responsiveness:
- On page load: A single lightweight query fetches filter metadata for all products (~150 bytes per product, ~80KB total for 500+ products)
- On filter change: Client-side
computeFilters()recalculates available options, counts, and price ranges in ~1-2ms - Key rule: Filter X does not reduce options for Filter X (e.g., selecting brand "Wilson Audio" doesn't hide other brands from the brand filter)
| Filter | Type | Logic |
|---|---|---|
| Category | Single select | Matches against denormalized category slugs |
| Brand | Multi-select | Array of brand slugs (OR within, AND with other filters) |
| Price | Range (min/max) | Cents-based comparison |
| Custom Dropdowns | Category-specific | Per-category configurable filters (e.g., "Cable Length") |
| Range Filters | Numeric range | Per-category numeric ranges (e.g., "Impedance: 4-12Ω") |
| CPO | Boolean | Certified Pre-Owned toggle |
| Search | Text | Matches product name; supports semantic search via embeddings |
A cookie-based comparison tool:
- Storage:
audiofast_comparisoncookie (7-day expiry) - Constraints: Max 3 products, must be from the same category
- Processing: Technical data is aligned across products/variants into a comparison table
- Configuration: Per-category comparison parameters managed via the Comparator Tool in Sanity Studio
- UI:
FloatingComparisonBox— persistent widget that appears when products are added
Outbound Email (Microsoft Graph API):
- Contact form submissions → notification to Audiofast team + confirmation to user
- Product inquiry forms → same flow with product context
- Templates built with React Email (
src/emails/)
Newsletter (Mailchimp):
- Subscription via
/api/newsletter/ - Newsletter generation tool in Sanity Studio: select date range → auto-fetch articles/reviews/products → generate HTML → create Mailchimp draft
Dual-tracking with consent management:
- Google Tag Manager: standard GA4 events
- Meta Pixel: client-side pixel + server-side Conversion API
- Consent: Cookie consent banner controls
ad_storage,analytics_storage, etc. - Event queue: Events are queued until consent is granted
- Advanced matching: User data (email, phone, name) hashed for Meta Pixel
- User storage: LocalStorage for persistent user data, SessionStorage for UTM parameters
- Dynamic metadata: Every page generates metadata via
generateMetadata()using lightweight SEO-only queries - OpenGraph images: 1200×630px from Sanity CDN
- Structured data (JSON-LD): Organization, WebSite, Article, BlogPost, Brand, CollectionPage schemas
- Dynamic sitemap: Generated from all published Sanity documents
- Robots.txt: Standard configuration
doNotIndexflag: Per-page control over indexing- Canonical URLs: Auto-generated from
BASE_URL+ slug
- SCSS Modules: Every component has a co-located
styles.module.scss - Global styles:
src/global/global.scss— CSS variables, reset, typography - Design tokens: Color palette (neutral grays, primary red
#fe0140), spacing scale, z-index scale - Typography: Poppins (headings) + Switzer (body), fluid sizing with
clamp() - Breakpoints:
56.1875rem(899px tablet),47.9375rem(767px mobile),35.9375rem(575px small mobile) - Conventions: Nested SCSS with BEM-like camelCase naming, media queries nested inside parent classes, rem units everywhere, transitions in milliseconds with explicit properties
| Type | Purpose | Key Features |
|---|---|---|
product |
Products | Brand/category refs, denormalized fields, technical data, custom filter values, image gallery, PDFs, reviews, related products, page builder, CPO/archived flags, pricing sync |
brand |
Brands | Logo, description, hero/banner images, unified content, distribution year, image gallery, stores, featured reviews |
blog-article |
Blog articles | Category ref, portable text content, page builder, author (internal/external), keywords |
blog-category |
Blog categories | Orderable |
review |
Reviews | Multiple destination types (page, PDF, external), product links |
reviewAuthor |
Review authors | Orderable |
productCategoryParent |
Parent product categories | Orderable |
productCategorySub |
Sub product categories | Parent ref, custom filter definitions |
store |
Stores/salons | Location data for store finder |
teamMember |
Team members | Staff profiles |
award |
Awards | Product awards |
faq |
FAQs | Question/answer pairs |
socialMedia |
Social media links | Restricted actions |
page |
Generic CMS pages | Page builder, slug validation |
| Type | Purpose |
|---|---|
homePage |
Homepage content |
settings |
Global settings (contact info, form config, Mailchimp, analytics, SEO, structured data) |
footer |
Footer content |
navbar |
Navigation bar |
redirects |
URL redirect mappings |
privacyPolicy |
Privacy policy content |
termsAndConditions |
Terms content |
notFound |
404 page content |
blog |
Blog listing page config |
products |
Products listing page config |
brands |
Brands listing page config |
cpoPage |
Certified Pre-Owned page config |
comparatorConfig |
Product comparison parameters per category |
heroCarousel, heroStatic, latestPublication, imageTextColumns, featuredPublications, featuredProducts, brandsMarquee, brandsList, brandsByCategoriesSection, faqSection, contactForm, contactMap, imageWithVideo, imageWithTextBoxes, blurLinesTextImage, gallerySection, teamSection, phoneImageCta, stepList, productsCarousel, productsListing
ptImage, ptMinimalImage, ptInlineImage, ptImageSlider, ptArrowList, ptCircleNumberedList, ptCtaSection, ptTwoColumnTable, ptFeaturedProducts, ptQuote, ptButton, ptHeading, ptYoutubeVideo, ptVimeoVideo, ptPageBreak, ptTwoColumnLine, ptHorizontalLine, ptReviewEmbed
The studio desk is organized into logical sections:
├── Home Page (singleton)
├── Podstrony (generic pages)
├── Produkty
│ ├── Products listing singleton
│ ├── Tabela produktów (bulk actions table)
│ ├── Produkty według marek (by brand)
│ ├── Produkty według kategorii (parent → sub)
│ ├── Produkty według statusu (active / archived)
│ ├── Lista nagród (awards)
│ ├── Kategorie nadrzędne (parent categories)
│ └── Kategorie podrzędne (sub-categories by parent)
├── Marki
│ ├── Brands listing singleton
│ ├── Tabela marek (bulk actions table)
│ └── Lista marek (orderable)
├── Recenzje
│ ├── Tabela recenzji (bulk actions table)
│ ├── Recenzje według autorów (by author)
│ └── Lista autorów (orderable)
├── Blog
│ ├── Blog listing singleton
│ ├── Tabela artykułów (bulk actions table)
│ ├── Wpisy na blogu (by year)
│ └── Kategorie bloga (orderable)
├── Salony (stores)
├── CPO
│ ├── CPO Page singleton
│ └── Produkty CPO (filtered)
├── Zespół (team members)
├── FAQ
└── Konfiguracja strony
├── Navbar, Footer, Settings (singletons)
├── Social Media
├── NotFound, Terms, Privacy, Redirects (singletons)
- Product documents: 3 tabs — Content, Technical Data (table editor), Filters (custom filter values)
- Sub-category documents: 2 tabs — Content, Filters Config (define available filters for the category)
A custom table interface for managing large collections:
- Bulk selection and editing
- Filtering and search
- Reference-based filters (e.g., filter products by brand)
- Column selection
- Pagination and sorting
- Used for: products, brands, reviews, blog articles
Generates newsletters from CMS content:
- Select date range
- Auto-fetches matching blog articles, reviews, and products
- Configure hero image
- Enable/disable content sections
- Generate HTML preview or create Mailchimp draft
Configures the product comparison feature:
- Discovers all technical data parameters from products
- Category-based parameter configuration
- Drag-and-drop parameter ordering
- Custom display names
- Parameter transformation (rename across products)
- Saves to
comparatorConfigsingleton
Wraps the standard publish action for products:
- Before publishing, computes denormalized fields (brand slug/name, category slugs, filter keys)
- Patches the draft with computed values
- Then proceeds with the standard publish
Restores the unpublish action for orderable document types (product, brand, blog-category) — which @sanity/orderable-document-list removes by default.
Comprehensive migration tooling for importing/transforming data:
| Domain | Scripts | Purpose |
|---|---|---|
| Products | 8+ scripts | Import, brand-based migration, PDF migration, unified content migration, date fixes, reference fixes |
| Brands | 10+ scripts | CSV import, content blocks migration, unified content, reviews, galleries |
| Reviews | 4+ scripts | Import, author resolution, HTML-to-Portable-Text conversion |
| Blog | 2+ scripts | Article import, content migration |
| Categories | 3+ scripts | Category import and transformation |
| Stores | 4+ scripts | Store import and transformation |
| Awards | 4+ scripts | Award import, image processing |
| Brand-Stores | 1 script | Brand-store relationship mapping |
| Denormalization | 1 script | Batch recomputation of all denormalized fields |
Each migration domain includes parsers (CSV/HTML), transformers, and Sanity client utilities.
Shared ESLint configurations exported as:
base— core TypeScript + Prettier rulesnext-js— Next.js specific rulesreact-internal— React library rules
Shared TypeScript configurations:
base.json— strict mode, modern module resolutionnextjs.json— extends base with Next.js settingsreact-library.json— extends base for React packages
| Variable | Purpose |
|---|---|
SANITY_API_READ_TOKEN |
Sanity read access |
SANITY_API_WRITE_TOKEN |
Sanity write access (revalidation denorm) |
SANITY_WEBHOOK_SECRET |
Webhook authentication |
NEXT_REVALIDATE_TOKEN |
Cache revalidation auth |
NEXT_PUBLIC_SUPABASE_URL |
Supabase endpoint |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase public key |
AZURE_TENANT_ID |
Microsoft Graph auth |
AZURE_CLIENT_ID |
Microsoft Graph auth |
AZURE_CLIENT_SECRET |
Microsoft Graph auth |
MS_GRAPH_SENDER_EMAIL |
Email sender address |
MS_GRAPH_REPLY_TO |
Email reply-to address |
MAILCHIMP_API_KEY |
Mailchimp API key |
MAILCHIMP_SERVER_PREFIX |
Mailchimp server prefix |
EMBEDDINGS_INDEX_BEARER_TOKEN |
Semantic search auth |
VERCEL_URL, VERCEL_PROJECT_PRODUCTION_URL, VERCEL_ENV |
Vercel environment |
| Variable | Purpose |
|---|---|
SANITY_STUDIO_PROJECT_ID |
Sanity project ID |
SANITY_STUDIO_DATASET |
Sanity dataset name |
SANITY_STUDIO_TITLE |
Studio title |
- Node.js ≥ 20
- Bun v1.1.42
# Install dependencies
bun install
# Start all dev servers (Next.js + Sanity Studio)
bun run dev
# Build everything
bun run build
# Lint
bun run lint
# Type check
bun run check-types
# Format code
bun run format
# Generate Sanity TypeScript types
bun run typegen
# Generate redirects map
bun run generate:redirects # (in apps/web)- Partial Pre-Rendering (PPR): Static shell rendered at build time with dynamic islands hydrated on request — fastest possible Time to First Byte
- Dual database: Content in Sanity (editorial flexibility) + pricing in Supabase (structured data with relational integrity)
- Client-side filter computation: Lightweight metadata shipped to the browser enables instant filtering without server round-trips
- Denormalization: Pre-computed fields on products avoid expensive joins in GROQ queries, maintained automatically via publish hooks and revalidation webhooks
- Granular cache invalidation: Reverse lookup mechanism ensures only affected pages are invalidated when content changes, rather than purging entire categories
- Fragment-based queries: GROQ query fragments are composed into larger queries, promoting reuse and consistency across ~2400 lines of query code
- Modular page builder: 20+ block types allow content editors to compose pages freely without developer intervention
- Cookie-based comparison: No authentication required — comparison state persists via cookies with a 7-day expiry