diff --git a/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx b/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx index 8f11473..75eb566 100644 --- a/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx +++ b/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx @@ -5,6 +5,7 @@ import { getBlogs } from "@/server/actions/blog"; import { format } from "date-fns"; import Image from "next/image"; import { notFound, redirect } from "next/navigation"; +import { type Metadata } from "next"; export const dynamic = "force-static"; @@ -14,6 +15,23 @@ type BlogSlugPageProps = { }; }; +export async function generateMetadata({ + params, +}: BlogSlugPageProps): Promise { + const slug = params.slug.join("/"); + + const blog = (await getBlogs()).find((b) => b.metaData.slug === slug); + + if (!blog) { + return notFound(); + } + + return { + title: blog.metaData.title, + description: blog.metaData.description, + }; +} + export async function generateStaticParams() { const blogs = await getBlogs(); diff --git a/starterkits/saas/src/app/(web)/blog/_constants/page-config.ts b/starterkits/saas/src/app/(web)/blog/_constants/page-config.ts new file mode 100644 index 0000000..1bfb27d --- /dev/null +++ b/starterkits/saas/src/app/(web)/blog/_constants/page-config.ts @@ -0,0 +1,3 @@ +export const blogPageConfig = { + title: "Blog", +} as const; diff --git a/starterkits/saas/src/app/(web)/blog/page.tsx b/starterkits/saas/src/app/(web)/blog/page.tsx index cd6794a..ccdb5a5 100644 --- a/starterkits/saas/src/app/(web)/blog/page.tsx +++ b/starterkits/saas/src/app/(web)/blog/page.tsx @@ -8,6 +8,12 @@ import { getBlogs } from "@/server/actions/blog"; import { format } from "date-fns"; import Image from "next/image"; import Link from "next/link"; +import { type Metadata } from "next"; +import { blogPageConfig } from "@/app/(web)/blog/_constants/page-config"; + +export const metadata: Metadata = { + title: blogPageConfig.title, +}; export const dynamic = "force-static"; diff --git a/starterkits/saas/src/app/(web)/changelog/_constants/page-config.ts b/starterkits/saas/src/app/(web)/changelog/_constants/page-config.ts new file mode 100644 index 0000000..044c199 --- /dev/null +++ b/starterkits/saas/src/app/(web)/changelog/_constants/page-config.ts @@ -0,0 +1,3 @@ +export const changelogPageConfig = { + title: "Change Log", +} as const; diff --git a/starterkits/saas/src/app/(web)/changelog/page.tsx b/starterkits/saas/src/app/(web)/changelog/page.tsx index 09386f9..e9697d2 100644 --- a/starterkits/saas/src/app/(web)/changelog/page.tsx +++ b/starterkits/saas/src/app/(web)/changelog/page.tsx @@ -13,6 +13,12 @@ import { siteConfig } from "@/config/site"; import { getChangelogs } from "@/server/actions/changelog"; import { format } from "date-fns"; import Image from "next/image"; +import type { Metadata } from "next"; +import { changelogPageConfig } from "@/app/(web)/changelog/_constants/page-config"; + +export const metadata: Metadata = { + title: changelogPageConfig.title, +}; export const dynamic = "force-static"; diff --git a/starterkits/saas/src/app/(web)/page.tsx b/starterkits/saas/src/app/(web)/page.tsx index 8591bf4..e07ad7b 100644 --- a/starterkits/saas/src/app/(web)/page.tsx +++ b/starterkits/saas/src/app/(web)/page.tsx @@ -11,6 +11,11 @@ import { siteUrls } from "@/config/urls"; import Image from "next/image"; import Link from "next/link"; import Balancer from "react-wrap-balancer"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Build Your MVP in Days, not weeks. Next.js Starter Kit", +}; export const dynamic = "force-static"; @@ -19,7 +24,7 @@ export default async function HomePage() { diff --git a/starterkits/saas/src/app/(web)/support/_constants/page-config.ts b/starterkits/saas/src/app/(web)/support/_constants/page-config.ts new file mode 100644 index 0000000..31806e9 --- /dev/null +++ b/starterkits/saas/src/app/(web)/support/_constants/page-config.ts @@ -0,0 +1,6 @@ +import { siteConfig } from "@/config/site"; + +export const supportPageConfig = { + title: "Support", + description: `Get support from ${siteConfig.name} to get started building your next project.`, +} as const; diff --git a/starterkits/saas/src/app/(web)/support/page.tsx b/starterkits/saas/src/app/(web)/support/page.tsx index b5f2c19..3c2225d 100644 --- a/starterkits/saas/src/app/(web)/support/page.tsx +++ b/starterkits/saas/src/app/(web)/support/page.tsx @@ -12,11 +12,21 @@ import { import { type SupportInfo, supportInfos } from "@/config/support"; import { ArrowRightIcon } from "lucide-react"; import Link from "next/link"; +import { type Metadata } from "next"; +import { supportPageConfig } from "@/app/(web)/support/_constants/page-config"; + +export const metadata: Metadata = { + title: supportPageConfig.title, + description: supportPageConfig.description, +}; export default function ContactPage() { return ( - +

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..1ac14a2 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,6 +11,23 @@ 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(); 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/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/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/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).*)", + ], };