Shipr provides a complete SEO setup out of the box using Next.js App Router’s Metadata API, dynamic sitemaps, robots.txt, and JSON-LD structured data.
Quick Start
Update Site Configuration
Edit src/lib/constants.ts to set your site name, URL, and description:export const SITE_CONFIG = {
name: "Your SaaS Name",
tagline: "Your tagline here",
description: "Your site description (max 160 characters)",
url: process.env.NEXT_PUBLIC_SITE_URL || "https://yourdomain.com",
// ... rest of config
};
Set Environment Variable
Add your production URL to .env.local:NEXT_PUBLIC_SITE_URL=https://yourdomain.com
Customize Page SEO
Update page-specific metadata in the PAGE_SEO object in src/lib/constants.ts.
Global metadata is configured in src/app/layout.tsx using Next.js Metadata API. All values pull from src/lib/constants.ts for centralized management.
Key Constants
export const SITE_CONFIG = {
name: "Shipr",
tagline: "Ship Your SaaS This Weekend.",
description:
"Shipr is a free, open-source SaaS starter that helps you go from zero to launch in 48 hours with Next.js, Convex, Clerk, auth, database wiring, analytics, docs, dashboard, secure uploads, and AI-ready foundations.",
url: process.env.NEXT_PUBLIC_SITE_URL || "https://shipr.dev",
locale: "en_US",
language: "en",
creator: "Ege Uysal",
email: "[email protected]",
social: {
github: "https://github.com/egeuysall",
twitter: "https://x.com/egewrk",
twitterHandle: "@egewrk",
},
} as const;
export const METADATA_DEFAULTS = {
titleTemplate: `%s | ${SITE_CONFIG.name}`,
titleDefault: `${SITE_CONFIG.name}: ${SITE_CONFIG.tagline}`,
descriptionMaxLength: 160,
titleMaxLength: 60,
} as const;
export const OG_IMAGE_DEFAULTS = {
width: 1200,
height: 630,
type: "image/png",
alt: `${SITE_CONFIG.name}: ${SITE_CONFIG.tagline}`,
} as const;
Per-Page Metadata
Each route exports its own metadata object. Example from src/app/(marketing)/page.tsx:
import { Metadata } from "next";
import { PAGE_SEO, SITE_CONFIG } from "@/lib/constants";
export const metadata: Metadata = {
title: PAGE_SEO.home.title,
description: PAGE_SEO.home.description,
keywords: [...PAGE_SEO.home.keywords],
alternates: { canonical: SITE_CONFIG.url },
};
Adding a New Page
Add to PAGE_SEO
Add an entry to the PAGE_SEO object in src/lib/constants.ts:export const PAGE_SEO = {
// ... existing pages
newPage: {
title: "New Page Title",
description: "Page description for search engines",
keywords: ["keyword1", "keyword2", "keyword3"],
},
};
Export Metadata
In your new page file, export the metadata:src/app/new-page/page.tsx
import { Metadata } from "next";
import { PAGE_SEO } from "@/lib/constants";
export const metadata: Metadata = {
title: PAGE_SEO.newPage.title,
description: PAGE_SEO.newPage.description,
keywords: [...PAGE_SEO.newPage.keywords],
};
Sitemap
src/app/sitemap.ts generates /sitemap.xml at build time. Routes come from SITEMAP_ROUTES in constants:
export const SITEMAP_ROUTES = [
{
path: ROUTES.public.home,
priority: 1.0,
changeFrequency: "weekly" as const,
},
{
path: ROUTES.public.features,
priority: 0.9,
changeFrequency: "weekly" as const,
},
{
path: ROUTES.public.pricing,
priority: 0.9,
changeFrequency: "weekly" as const,
},
// ... more routes
] as const;
Add new public routes to SITEMAP_ROUTES and they’ll appear in the sitemap automatically.
Robots.txt
src/app/robots.ts generates /robots.txt. Protected and internal routes are blocked via ROBOTS_DISALLOWED:
export const ROBOTS_DISALLOWED = [
"/dashboard",
"/dashboard/*",
"/api",
"/api/*",
"/sign-in",
"/sign-up",
"/monitoring",
"/ingest",
"/ingest/*",
] as const;
Structured Data (JSON-LD)
src/lib/structured-data.tsx provides server-safe JSON-LD components for rich search results:
Available Components
| Component | Schema Type | Where to Use |
|---|
OrganizationJsonLd | Organization | Root layout <head> |
WebSiteJsonLd | WebSite | Root layout <head> |
SoftwareApplicationJsonLd | SoftwareApplication | Home / pricing pages |
ProductJsonLd | Product | Pricing page |
FaqJsonLd | FAQPage | Any page with FAQs |
BreadcrumbJsonLd | BreadcrumbList | Nested pages |
ArticleJsonLd | Article | Blog posts |
Example: FAQ Schema
import { FaqJsonLd } from "@/lib/structured-data";
const faqItems = [
{
question: "How do I get started?",
answer: "Clone the repo, install dependencies, and run the dev server.",
},
{
question: "Is it free?",
answer: "Yes, Shipr is completely free and open-source.",
},
];
export default function FaqPage() {
return (
<>
<FaqJsonLd items={faqItems} />
{/* Your FAQ UI */}
</>
);
}
Example: Product Schema
import { ProductJsonLd } from "@/lib/structured-data";
const pricingPlans = [
{
name: "Free",
price: "0",
description: "For individuals and small projects",
},
{
name: "Pro",
price: "12",
description: "For growing teams and businesses",
},
];
export default function PricingPage() {
return (
<>
<ProductJsonLd plans={pricingPlans} />
{/* Your pricing UI */}
</>
);
}
Creating Custom Schemas
The base JsonLd component in src/lib/structured-data.tsx can be extended for custom schemas:
src/lib/structured-data.tsx
function JsonLd({ data }: JsonLdProps): React.ReactElement {
const jsonLd = {
"@context": "https://schema.org",
...data,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
Define the Data Shape
Create a TypeScript interface for your schema:interface CustomSchemaProps {
title: string;
date: string;
}
Create the Component
Build a component that renders <JsonLd>:export function CustomSchemaJsonLd({ title, date }: CustomSchemaProps) {
return (
<JsonLd
data={{
"@type": "Event",
name: title,
startDate: date,
}}
/>
);
}
Use in Your Page
Drop it into the relevant page or layout:<CustomSchemaJsonLd title="Launch Event" date="2024-12-01" />
Open Graph & Social Images
Static image files live in src/app/:
opengraph-image.png (1200×630)
twitter-image.png (1200×630)
apple-touch-icon.png
favicon.ico / icon.svg
Next.js automatically serves these at the correct routes for social media previews.
Use tools like Figma or Canva to create Open Graph images at 1200×630 pixels.
Environment Variables Reference
| Variable | Required | Description |
|---|
NEXT_PUBLIC_SITE_URL | Yes | Your production site URL (e.g., https://yourdomain.com) |