If you have any questions or need help, feel free to reach
out to us.
diff --git a/starterkits/saas/src/app/auth/login/_constants/page-config.ts b/starterkits/saas/src/app/auth/login/_constants/page-config.ts
new file mode 100644
index 0000000..d2797b9
--- /dev/null
+++ b/starterkits/saas/src/app/auth/login/_constants/page-config.ts
@@ -0,0 +1,6 @@
+import { siteConfig } from "@/config/site";
+
+export const loginPageConfig = {
+ title: "Login",
+ description: `Login to ${siteConfig.name} to get started building your next project.`,
+} as const;
diff --git a/starterkits/saas/src/app/auth/login/page.tsx b/starterkits/saas/src/app/auth/login/page.tsx
index cae58a0..e5d92b1 100644
--- a/starterkits/saas/src/app/auth/login/page.tsx
+++ b/starterkits/saas/src/app/auth/login/page.tsx
@@ -1,4 +1,11 @@
import { AuthForm } from "@/app/auth/_components/auth-form";
+import { loginPageConfig } from "@/app/auth/login/_constants/page-config";
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: loginPageConfig.title,
+ description: loginPageConfig.description,
+};
export default function Login() {
return ;
diff --git a/starterkits/saas/src/app/auth/signup/_constants/page-config.ts b/starterkits/saas/src/app/auth/signup/_constants/page-config.ts
new file mode 100644
index 0000000..ac783bc
--- /dev/null
+++ b/starterkits/saas/src/app/auth/signup/_constants/page-config.ts
@@ -0,0 +1,6 @@
+import { siteConfig } from "@/config/site";
+
+export const signupPageConfig = {
+ title: "Signup",
+ description: `Signup to ${siteConfig.name} to get started building your next project.`,
+} as const;
diff --git a/starterkits/saas/src/app/auth/signup/page.tsx b/starterkits/saas/src/app/auth/signup/page.tsx
index 7e4f572..327138a 100644
--- a/starterkits/saas/src/app/auth/signup/page.tsx
+++ b/starterkits/saas/src/app/auth/signup/page.tsx
@@ -1,4 +1,11 @@
import { AuthForm } from "@/app/auth/_components/auth-form";
+import { signupPageConfig } from "@/app/auth/signup/_constants/page-config";
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: signupPageConfig.title,
+ description: signupPageConfig.description,
+};
export default function Signup() {
return ;
diff --git a/starterkits/saas/src/app/docs/[[...slug]]/page.tsx b/starterkits/saas/src/app/docs/[[...slug]]/page.tsx
index 20f176d..5f74f6e 100644
--- a/starterkits/saas/src/app/docs/[[...slug]]/page.tsx
+++ b/starterkits/saas/src/app/docs/[[...slug]]/page.tsx
@@ -1,6 +1,7 @@
import { notFound } from "next/navigation";
import { Toc } from "@/components/toc";
import { getDocs } from "@/server/actions/docs";
+import { type Metadata } from "next";
export const dynamic = "force-static";
@@ -10,13 +11,26 @@ type DocsSlugPageProps = {
};
};
+export async function generateMetadata({
+ params,
+}: DocsSlugPageProps): Promise {
+ const slug = Array.isArray(params.slug) ? params.slug.join("/") : "/";
+
+ const doc = (await getDocs()).find((doc) => doc.metaData.slug === slug);
+
+ if (!doc) {
+ return notFound();
+ }
+
+ return {
+ title: doc.metaData.title,
+ description: doc.metaData.description,
+ };
+}
+
export async function generateStaticParams() {
const docs = await getDocs();
- docs.map((doc) => {
- console.log(doc.metaData.slug.split("/") || ["/"]);
- });
-
return docs.map((doc) => ({
slug: doc.metaData.slug.split("/") || ["/"],
}));
@@ -27,6 +41,8 @@ export default async function DocsSlugPage({ params }: DocsSlugPageProps) {
const doc = (await getDocs()).find((doc) => doc.metaData.slug === slug);
+ console.log(["gettings-started", "installation"].join("/"), params.slug);
+
if (!doc) {
return notFound();
}
diff --git a/starterkits/saas/src/app/invite/org/[orgId]/_constants/page-config.ts b/starterkits/saas/src/app/invite/org/[orgId]/_constants/page-config.ts
new file mode 100644
index 0000000..7284ceb
--- /dev/null
+++ b/starterkits/saas/src/app/invite/org/[orgId]/_constants/page-config.ts
@@ -0,0 +1,5 @@
+export const invitePageConfig = {
+ title: ({ orgName }: { orgName: string }) => `Invite to ${orgName}`,
+ description: ({ orgName }: { orgName: string }) =>
+ `Invite your team to ${orgName} and get started building your next project.`,
+} as const;
diff --git a/starterkits/saas/src/app/invite/org/[orgId]/page.tsx b/starterkits/saas/src/app/invite/org/[orgId]/page.tsx
index a582e8a..2149e7c 100644
--- a/starterkits/saas/src/app/invite/org/[orgId]/page.tsx
+++ b/starterkits/saas/src/app/invite/org/[orgId]/page.tsx
@@ -1,8 +1,9 @@
import { getOrgByIdQuery } from "@/server/actions/organization/queries";
import { RequestCard } from "@/app/invite/org/[orgId]/_components/request-card";
import { notFound } from "next/navigation";
+import { type Metadata } from "next";
-type OrgRequestProps = {
+export type OrgRequestProps = {
params: {
orgId: string;
};
@@ -23,3 +24,18 @@ export default async function OrgRequestPage({
);
}
+
+export async function generateMetadata({
+ params,
+}: OrgRequestProps): Promise {
+ const org = await getOrgByIdQuery({ orgId: params.orgId });
+
+ if (!org) {
+ return notFound();
+ }
+
+ return {
+ title: `Invite to ${org.name}`,
+ description: `Invite your team to ${org.name} and get started building your next project.`,
+ };
+}
diff --git a/starterkits/saas/src/app/layout.tsx b/starterkits/saas/src/app/layout.tsx
index c3ed38e..f98910c 100644
--- a/starterkits/saas/src/app/layout.tsx
+++ b/starterkits/saas/src/app/layout.tsx
@@ -4,12 +4,21 @@ import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css";
import "@/styles/prism.css";
import { fontHeading, fontSans } from "@/lib/fonts";
+import { type Metadata } from "next";
+import {
+ defaultMetadata,
+ twitterMetadata,
+ ogMetadata,
+} from "@/app/shared-metadata";
-export const metadata = {
- title: "RapidLaunch - Next.js Boilerplate",
- description:
- "Next.js boilerplate with shadcn ui, TRPC, TailwindCSS, and Drizzle.",
- icons: [{ rel: "icon", url: "/favicon.ico" }],
+export const metadata: Metadata = {
+ ...defaultMetadata,
+ twitter: {
+ ...twitterMetadata,
+ },
+ openGraph: {
+ ...ogMetadata,
+ },
};
export default function RootLayout({
diff --git a/starterkits/saas/src/app/maintenance/_constants/page-config.ts b/starterkits/saas/src/app/maintenance/_constants/page-config.ts
new file mode 100644
index 0000000..52aa684
--- /dev/null
+++ b/starterkits/saas/src/app/maintenance/_constants/page-config.ts
@@ -0,0 +1,5 @@
+export const maintenancePageConfig = {
+ title: "Maintenance",
+ description:
+ "We're currently undergoing maintenance. Please check back later.",
+} as const;
diff --git a/starterkits/saas/src/app/maintenance/page.tsx b/starterkits/saas/src/app/maintenance/page.tsx
index f8f68fd..7cff144 100644
--- a/starterkits/saas/src/app/maintenance/page.tsx
+++ b/starterkits/saas/src/app/maintenance/page.tsx
@@ -1,4 +1,11 @@
+import { maintenancePageConfig } from "@/app/maintenance/_constants/page-config";
import { siteConfig } from "@/config/site";
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: maintenancePageConfig.title,
+ description: maintenancePageConfig.description,
+};
export default function Maintenance() {
return (
diff --git a/starterkits/saas/src/app/robots.ts b/starterkits/saas/src/app/robots.ts
new file mode 100644
index 0000000..559d2af
--- /dev/null
+++ b/starterkits/saas/src/app/robots.ts
@@ -0,0 +1,12 @@
+import { siteUrls } from "@/config/urls";
+import type { MetadataRoute } from "next";
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: {
+ userAgent: "*",
+ allow: "/",
+ },
+ sitemap: `${siteUrls.publicUrl}/sitemap.xml`,
+ };
+}
diff --git a/starterkits/saas/src/app/shared-metadata.ts b/starterkits/saas/src/app/shared-metadata.ts
new file mode 100644
index 0000000..63818fe
--- /dev/null
+++ b/starterkits/saas/src/app/shared-metadata.ts
@@ -0,0 +1,39 @@
+import { siteConfig } from "@/config/site";
+import { siteUrls } from "@/config/urls";
+import type { Metadata } from "next";
+
+export const defaultMetadata: Metadata = {
+ title: {
+ template: `%s | ${siteConfig.name}`,
+ default: siteConfig.name,
+ },
+ description: siteConfig.description,
+ metadataBase: new URL(siteUrls.publicUrl),
+ keywords: [
+ "Next.js",
+ "React",
+ "Next.js Starter kit",
+ "SaaS Starter Kit",
+ "Shadcn UI",
+ ],
+ authors: [{ name: "Ali Farooq", url: "https://twitter.com/alifarooqdev" }],
+ creator: "AliFarooqDev",
+};
+
+export const twitterMetadata: Metadata["twitter"] = {
+ title: siteConfig.name,
+ description: siteConfig.description,
+ card: "summary_large_image",
+ images: [siteConfig.orgImage],
+ creator: "@alifarooqdev",
+};
+
+export const ogMetadata: Metadata["openGraph"] = {
+ title: siteConfig.name,
+ description: siteConfig.description,
+ type: "website",
+ images: [{ url: siteConfig.orgImage, alt: siteConfig.name }],
+ locale: "en_US",
+ url: siteUrls.publicUrl,
+ siteName: siteConfig.name,
+};
diff --git a/starterkits/saas/src/app/sitemap.ts b/starterkits/saas/src/app/sitemap.ts
new file mode 100644
index 0000000..ec8ac6e
--- /dev/null
+++ b/starterkits/saas/src/app/sitemap.ts
@@ -0,0 +1,36 @@
+import { publicRoutes, siteUrls } from "@/config/urls";
+import { getBlogs } from "@/server/actions/blog";
+import { getDocs } from "@/server/actions/docs";
+import type { MetadataRoute } from "next";
+
+const addPathToBaseURL = (path: string) => `${siteUrls.publicUrl}${path}`;
+
+export default async function sitemap(): Promise {
+ const allBlogs = await getBlogs();
+
+ const blogs = allBlogs.map((blog) => ({
+ url: addPathToBaseURL(`${siteUrls.blog}/${blog.metaData.slug}`),
+ lastModified: new Date(blog.metaData.updatedAt),
+ }));
+
+ const allDocs = await getDocs();
+
+ const docs = allDocs.map((doc) => ({
+ url: addPathToBaseURL(
+ `${siteUrls.docs}/${doc.metaData.slug === "/" ? "" : doc.metaData.slug}`,
+ ),
+ lastModified: new Date(doc.metaData.publishedAt),
+ }));
+
+ const publicRoutesWithoutPublicUrl = publicRoutes.filter(
+ (route) =>
+ route !== siteUrls.publicUrl && route !== siteUrls.rapidlaunch,
+ );
+
+ const routes = publicRoutesWithoutPublicUrl.map((route) => ({
+ url: addPathToBaseURL(route),
+ lastModified: new Date(),
+ }));
+
+ return [...routes, ...blogs, ...docs];
+}
diff --git a/starterkits/saas/src/app/waitlist/_constants/page-config.ts b/starterkits/saas/src/app/waitlist/_constants/page-config.ts
new file mode 100644
index 0000000..ecaeb6e
--- /dev/null
+++ b/starterkits/saas/src/app/waitlist/_constants/page-config.ts
@@ -0,0 +1,5 @@
+export const waitlistPageConfig = {
+ title: "Join the waitlist",
+ description:
+ "Welcome to Rapidlaunch, a platform which provides resources for building applications faster. We're currently working on adding more features and improving the user experience. In the meantime, you can join our waitlist!",
+} as const;
diff --git a/starterkits/saas/src/app/waitlist/page.tsx b/starterkits/saas/src/app/waitlist/page.tsx
index f322bc8..cfab349 100644
--- a/starterkits/saas/src/app/waitlist/page.tsx
+++ b/starterkits/saas/src/app/waitlist/page.tsx
@@ -1,6 +1,13 @@
import { WaitlistForm } from "@/app/waitlist/_components/waitlist-form";
+import { waitlistPageConfig } from "@/app/waitlist/_constants/page-config";
import { Icons } from "@/components/ui/icons";
import { siteConfig } from "@/config/site";
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: waitlistPageConfig.title,
+ description: waitlistPageConfig.description,
+};
export default function Waitlist() {
return (
diff --git a/starterkits/saas/src/config/site.ts b/starterkits/saas/src/config/site.ts
index 9574491..c467552 100644
--- a/starterkits/saas/src/config/site.ts
+++ b/starterkits/saas/src/config/site.ts
@@ -5,9 +5,11 @@
*/
export const siteConfig = {
- name: "RapidLaunch",
+ name: "Rapidlaunch",
description:
- "Build your SaaS with ease, using our beautiful starterkit. Rapidlaunch is a powerful and flexible SaaS platform that allows you to build and deploy your SaaS quickly and easily.",
+ "Get your startup off the ground quickly with RapidLaunch! This open source Next.js starter kit provides the foundation you need to build your MVP fast – pre-built components, optimized performance, and ready-to-go styling",
+ orgImage:
+ "https://utfs.io/f/4ae0ddb1-4260-46f5-aa7c-70408cc192b9-aadavt.png",
contactEmail: "hello@support.rapidlaunch.xyz",
noReplyEmail: "Rapidlaunch@support.rapidlaunch.xyz",
} as const;
diff --git a/starterkits/saas/src/config/urls.ts b/starterkits/saas/src/config/urls.ts
index fb069b8..d9c25bd 100644
--- a/starterkits/saas/src/config/urls.ts
+++ b/starterkits/saas/src/config/urls.ts
@@ -65,3 +65,23 @@ export const publicRoutes: string[] = [
siteUrls.waitlist,
siteUrls.rapidlaunch,
];
+
+export const protectedRoutes: string[] = [
+ siteUrls.dashboard.home,
+ siteUrls.feedback,
+ siteUrls.organization.members.home,
+ siteUrls.organization.members.invite,
+ siteUrls.organization.settings,
+ siteUrls.organization.plansAndBilling,
+ siteUrls.auth.login,
+ siteUrls.auth.signup,
+ siteUrls.admin.dashboard,
+ siteUrls.admin.users,
+ siteUrls.admin.organizations,
+ siteUrls.admin.settings,
+ siteUrls.admin.waitlist,
+ siteUrls.admin.feedbacks,
+ siteUrls.admin.analytics,
+ siteUrls.profile.settings,
+ siteUrls.profile.billing,
+];
diff --git a/starterkits/saas/src/content/blog/introduction.mdx b/starterkits/saas/src/content/blog/introduction.mdx
index 1f99c33..4984257 100644
--- a/starterkits/saas/src/content/blog/introduction.mdx
+++ b/starterkits/saas/src/content/blog/introduction.mdx
@@ -2,6 +2,7 @@
title: Introduction
slug: introduction
publishedAt: 2022-01-01
+updatedAt: 2024-05-01
readTime: 5 min
tags: ["introduction", "saas"]
description: This is the introduction
diff --git a/starterkits/saas/src/content/changelogs/version-0.0.0.mdx b/starterkits/saas/src/content/changelogs/version-0.0.0.mdx
index c2de931..9c5774a 100644
--- a/starterkits/saas/src/content/changelogs/version-0.0.0.mdx
+++ b/starterkits/saas/src/content/changelogs/version-0.0.0.mdx
@@ -1,6 +1,5 @@
---
title: "Launch of RapidLaunch"
-slug: "launch-of-rapidlaunch"
description: "RapidLaunch is a platform to create and launch SaaS products quickly."
version: "0.0.0"
publishedAt: 2024-04-01
diff --git a/starterkits/saas/src/content/changelogs/version-0.1.0.mdx b/starterkits/saas/src/content/changelogs/version-0.1.0.mdx
index 3e3f2de..7ccbe3a 100644
--- a/starterkits/saas/src/content/changelogs/version-0.1.0.mdx
+++ b/starterkits/saas/src/content/changelogs/version-0.1.0.mdx
@@ -1,6 +1,5 @@
---
title: "Launch of RapidLaunch 2"
-slug: "launch-of-rapidlaunch-2"
description: "RapidLaunch is a platform to create and launch SaaS products quickly."
version: "0.1.0"
publishedAt: 2024-08-01
diff --git a/starterkits/saas/src/content/changelogs/version-0.2.4.mdx b/starterkits/saas/src/content/changelogs/version-0.2.4.mdx
index 0a31bde..11018c6 100644
--- a/starterkits/saas/src/content/changelogs/version-0.2.4.mdx
+++ b/starterkits/saas/src/content/changelogs/version-0.2.4.mdx
@@ -1,6 +1,5 @@
---
title: "Launch of RapidLaunch 2"
-slug: "launch-of-rapidlaunch-2"
description: "RapidLaunch is a platform to create and launch SaaS products quickly."
version: "0.4.0"
publishedAt: 2024-10-06
diff --git a/starterkits/saas/src/content/docs/installation.mdx b/starterkits/saas/src/content/docs/installation.mdx
index 81195b1..07030a8 100644
--- a/starterkits/saas/src/content/docs/installation.mdx
+++ b/starterkits/saas/src/content/docs/installation.mdx
@@ -2,6 +2,7 @@
title: "Installation"
slug: "getting-started/installation"
description: "Learn how to install rapidlaunch"
+publishedAt: 2024-01-01
---
Added your your own doumentation by create a .mdx file in "*src/content/docs*" folder.
diff --git a/starterkits/saas/src/content/docs/introduction.mdx b/starterkits/saas/src/content/docs/introduction.mdx
index 0de1cc8..2225ede 100644
--- a/starterkits/saas/src/content/docs/introduction.mdx
+++ b/starterkits/saas/src/content/docs/introduction.mdx
@@ -2,6 +2,7 @@
title: "Introduction"
slug: "/"
description: "Rapidlaunch is an open-source Next.js SaaS Starterkit/Boilerplate designed to expedite the development process of Software as a Service (SaaS) applications. Launch your MVP in days."
+publishedAt: 2024-01-01
---
## Creator
diff --git a/starterkits/saas/src/middleware.ts b/starterkits/saas/src/middleware.ts
index 6fdb8eb..64139de 100644
--- a/starterkits/saas/src/middleware.ts
+++ b/starterkits/saas/src/middleware.ts
@@ -1,7 +1,7 @@
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
-import { publicRoutes, siteUrls } from "@/config/urls";
+import { protectedRoutes, siteUrls } from "@/config/urls";
import { getAbsoluteUrl } from "@/lib/utils";
import { env } from "@/env";
@@ -31,36 +31,42 @@ export async function middleware(request: NextRequest) {
}
/** if path is public route than do nothing */
- if (
- publicRoutes
- .map((route) => route.startsWith(request.nextUrl.pathname))
- .includes(true)
- ) {
- return NextResponse.next();
- }
+ if (protectedRoutes.includes(request.nextUrl.pathname)) {
+ const session = await getToken({ req: request });
- const session = await getToken({ req: request });
+ /** if path name starts from /auth, and session is there redirect to dashboard */
+ if (session && request.nextUrl.pathname.startsWith("/auth")) {
+ return NextResponse.redirect(
+ getAbsoluteUrl(siteUrls.dashboard.home),
+ );
+ }
- /** if path name starts from /auth, and session is there redirect to dashboard */
- if (session && request.nextUrl.pathname.startsWith("/auth")) {
- return NextResponse.redirect(getAbsoluteUrl(siteUrls.dashboard.home));
- }
-
- /** if path name does not start from /auth, and session is not there redirect to login */
- if (!session && !request.nextUrl.pathname.startsWith("/auth")) {
- return NextResponse.redirect(getAbsoluteUrl(siteUrls.auth.login));
- }
+ /** if path name does not start from /auth, and session is not there redirect to login */
+ if (!session && !request.nextUrl.pathname.startsWith("/auth")) {
+ return NextResponse.redirect(getAbsoluteUrl(siteUrls.auth.login));
+ }
- /** if path name start from admin, and session role is not admin or super admin redirect to dashboard */
- const isAdmin =
- session?.role === "Admin" || session?.role === "Super Admin";
+ /** if path name start from admin, and session role is not admin or super admin redirect to dashboard */
+ const isAdmin =
+ session?.role === "Admin" || session?.role === "Super Admin";
- if (session && request.nextUrl.pathname.startsWith("/admin") && !isAdmin) {
- return NextResponse.redirect(getAbsoluteUrl(siteUrls.dashboard.home));
+ if (
+ session &&
+ request.nextUrl.pathname.startsWith("/admin") &&
+ !isAdmin
+ ) {
+ return NextResponse.redirect(
+ getAbsoluteUrl(siteUrls.dashboard.home),
+ );
+ }
+ } else {
+ return NextResponse.next();
}
}
// See "Matching Paths" below to learn more
export const config = {
- matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
+ matcher: [
+ "/((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
+ ],
};
diff --git a/starterkits/saas/src/validations/mdx-content.ts b/starterkits/saas/src/validations/mdx-content.ts
index 28da78f..c51ccde 100644
--- a/starterkits/saas/src/validations/mdx-content.ts
+++ b/starterkits/saas/src/validations/mdx-content.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
export const docsMetaSchema = z.object({
title: z.string(),
slug: z.string(),
+ publishedAt: z.date(),
tags: z.array(z.string()).optional(),
description: z.string().optional(),
isDraft: z.boolean().optional(),
@@ -14,7 +15,7 @@ export const blogMetaSchema = z.object({
title: z.string(),
slug: z.string(),
publishedAt: z.date(),
- updatedAt: z.date().optional(),
+ updatedAt: z.date(),
readTime: z.string(),
tags: z.array(z.string()).optional(),
description: z.string(),
@@ -27,7 +28,6 @@ export type BlogMetaData = z.infer;
export const changelogMetaSchema = z.object({
title: z.string(),
- slug: z.string(),
publishedAt: z.date(),
thumbnail: z.string().url(),
description: z.string(),