Skip to main content
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

1

Update Site Configuration

Edit src/lib/constants.ts to set your site name, URL, and description:
src/lib/constants.ts
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
};
2

Set Environment Variable

Add your production URL to .env.local:
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
3

Customize Page SEO

Update page-specific metadata in the PAGE_SEO object in src/lib/constants.ts.

Metadata Configuration

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

src/lib/constants.ts
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

1

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"],
  },
};
2

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:
src/lib/constants.ts
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:
src/lib/constants.ts
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

ComponentSchema TypeWhere to Use
OrganizationJsonLdOrganizationRoot layout <head>
WebSiteJsonLdWebSiteRoot layout <head>
SoftwareApplicationJsonLdSoftwareApplicationHome / pricing pages
ProductJsonLdProductPricing page
FaqJsonLdFAQPageAny page with FAQs
BreadcrumbJsonLdBreadcrumbListNested pages
ArticleJsonLdArticleBlog 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) }}
    />
  );
}
1

Define the Data Shape

Create a TypeScript interface for your schema:
interface CustomSchemaProps {
  title: string;
  date: string;
}
2

Create the Component

Build a component that renders <JsonLd>:
export function CustomSchemaJsonLd({ title, date }: CustomSchemaProps) {
  return (
    <JsonLd
      data={{
        "@type": "Event",
        name: title,
        startDate: date,
      }}
    />
  );
}
3

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

VariableRequiredDescription
NEXT_PUBLIC_SITE_URLYesYour production site URL (e.g., https://yourdomain.com)

Build docs developers (and LLMs) love