Production upgrade: Clerk auth, Stripe billing, analytics, PWA support
Backend & Auth: - Add @clerk/nextjs for production authentication - Create src/proxy.ts with clerkMiddleware() for route protection - Implement multi-tenant auth with role-based access control - Add Clerk components (Show, UserButton, SignInButton, SignUpButton) Billing & Payments: - Full Stripe integration (subscriptions, add-ons, customer portal) - Plan tiers: Starter 9/mo, Farm 49/mo, Enterprise 99/mo - Webhook handling for subscription events - createSubscription(), createAddonSubscription(), createCustomerPortalSession() API & Security: - Rate limiting with @upstash/ratelimit (100 req/min API, 20 req/min checkout) - Zod validation schemas for all endpoints (orders, products, campaigns, etc.) - Security headers (CSP, HSTS, X-Frame-Options) - API routes: /api/v1/ with validated, rate-limited endpoints Monitoring: - Sentry error tracking with performance monitoring - PostHog analytics for feature usage, funnels, cohorts - User activity logging and breadcrumb tracking Admin Features: - Analytics dashboard with revenue charts, customer growth, conversion funnel - Onboarding flow with 6-step interactive tour - Referral system with share tracking and reward redemption - Changelog feed with in-app notifications PWA & SEO: - Web app manifest with icons and shortcuts - Service worker for offline support and caching - Full SEO metadata, OpenGraph, Twitter cards - Structured data (JSON-LD) for organization and products Database: - Add referral_codes, changelogs, onboarding_progress tables - Add user_activity_logs, api_keys, notification_preferences - Comprehensive RLS policies for all new tables - Seed data for demo brands and products
This commit is contained in:
+218
@@ -0,0 +1,218 @@
|
||||
// SEO and Metadata Utilities
|
||||
|
||||
import { Metadata } from "next";
|
||||
|
||||
// Base URL for the application
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://routecommerce.com";
|
||||
|
||||
// Default SEO configuration
|
||||
export const defaultSEO: Metadata = {
|
||||
metadataBase: new URL(BASE_URL),
|
||||
title: {
|
||||
default: "Route Commerce | Fresh Produce Wholesale Platform",
|
||||
template: "%s | Route Commerce",
|
||||
},
|
||||
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments.",
|
||||
keywords: [
|
||||
"wholesale produce",
|
||||
"farm fresh",
|
||||
"B2B e-commerce",
|
||||
"produce distribution",
|
||||
"pickup stops",
|
||||
"fresh produce",
|
||||
"farm management",
|
||||
"order management",
|
||||
"customer portal",
|
||||
"wholesale ordering",
|
||||
],
|
||||
authors: [{ name: "Route Commerce" }],
|
||||
creator: "Route Commerce",
|
||||
publisher: "Route Commerce",
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: BASE_URL,
|
||||
siteName: "Route Commerce",
|
||||
title: "Route Commerce | Fresh Produce Wholesale Platform",
|
||||
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution.",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Route Commerce Platform",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Route Commerce | Fresh Produce Wholesale Platform",
|
||||
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution.",
|
||||
site: "@RouteCommerce",
|
||||
creator: "@RouteCommerce",
|
||||
images: ["/og-image.png"],
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
],
|
||||
apple: "/icons/apple-touch-icon.png",
|
||||
other: [
|
||||
{
|
||||
rel: "manifest",
|
||||
url: "/manifest.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
alternates: {
|
||||
canonical: BASE_URL,
|
||||
languages: {
|
||||
"en-US": BASE_URL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Brand-specific metadata
|
||||
export function getBrandMetadata(brand: {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
logo_url?: string;
|
||||
}) {
|
||||
return {
|
||||
title: `${brand.name} | Fresh Produce`,
|
||||
description: brand.description || `Order fresh produce from ${brand.name}. Pick up at scheduled stops or get deliveries.`,
|
||||
openGraph: {
|
||||
title: `${brand.name} | Fresh Produce`,
|
||||
description: brand.description || `Order fresh produce from ${brand.name}`,
|
||||
url: `${BASE_URL}/${brand.slug}`,
|
||||
siteName: brand.name,
|
||||
images: brand.logo_url ? [{ url: brand.logo_url }] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Product metadata
|
||||
export function getProductMetadata(product: {
|
||||
name: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
image_url?: string;
|
||||
}) {
|
||||
return {
|
||||
title: `${product.name} | Route Commerce`,
|
||||
description: product.description || `Order ${product.name} from Route Commerce`,
|
||||
openGraph: {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
images: product.image_url ? [{ url: product.image_url }] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Admin page metadata
|
||||
export function getAdminMetadata(pageTitle: string) {
|
||||
return {
|
||||
title: `${pageTitle} | Admin | Route Commerce`,
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Structured data (JSON-LD)
|
||||
export function getOrganizationSchema() {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Route Commerce",
|
||||
url: BASE_URL,
|
||||
logo: `${BASE_URL}/logo.svg`,
|
||||
sameAs: [
|
||||
"https://twitter.com/RouteCommerce",
|
||||
"https://linkedin.com/company/route-commerce",
|
||||
],
|
||||
contactPoint: {
|
||||
"@type": "ContactPoint",
|
||||
contactType: "customer service",
|
||||
email: "support@routecommerce.com",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getProductSchema(product: {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
currency?: string;
|
||||
availability?: string;
|
||||
image?: string;
|
||||
brand?: string;
|
||||
}) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
image: product.image,
|
||||
brand: {
|
||||
"@type": "Brand",
|
||||
name: product.brand || "Route Commerce",
|
||||
},
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: product.price,
|
||||
priceCurrency: product.currency || "USD",
|
||||
availability: product.availability || "https://schema.org/InStock",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getLocalBusinessSchema(business: {
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postalCode: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
openingHours?: string[];
|
||||
}) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
name: business.name,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress: business.address,
|
||||
addressLocality: business.city,
|
||||
addressRegion: business.state,
|
||||
postalCode: business.postalCode,
|
||||
},
|
||||
telephone: business.phone,
|
||||
email: business.email,
|
||||
openingHours: business.openingHours,
|
||||
};
|
||||
}
|
||||
|
||||
// Generate sitemap data
|
||||
export function generateSitemap(pages: { url: string; lastModified?: Date; changeFrequency?: string; priority?: number }[]) {
|
||||
return pages.map((page) => ({
|
||||
url: `${BASE_URL}${page.url}`,
|
||||
lastModified: page.lastModified || new Date(),
|
||||
changeFrequency: page.changeFrequency || "weekly",
|
||||
priority: page.priority || 0.5,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user