Skip to content

Commit 7a60022

Browse files
authored
feat(saas): Improved SEO, Metadata to pages (#30)
### TL;DR Implemented page configurations and metadata for various pages in the SaaS application. Added dynamic metadata generation based on page content. ### What changed? - Added page configurations for blog, changelog, pricing, support, login, signup, docs, invite, maintenance, waitlist, and shared metadata. - Implemented dynamic metadata generation for blog, changelog, pricing, support, login, signup, docs, invite, and maintenance pages. ### How to test? Review the changes in the PR diff to ensure the correct metadata is generated for each page. ### Why make this change? To improve SEO and enhance user experience by providing accurate and relevant metadata for each page. ---
2 parents 676e55e + 6a4ce33 commit 7a60022

File tree

35 files changed

+325
-51
lines changed

35 files changed

+325
-51
lines changed

starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx renamed to starterkits/saas/src/app/(web)/blog/[slug]/page.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,38 @@ import { getBlogs } from "@/server/actions/blog";
55
import { format } from "date-fns";
66
import Image from "next/image";
77
import { notFound, redirect } from "next/navigation";
8+
import { type Metadata } from "next";
89

910
export const dynamic = "force-static";
1011

1112
type BlogSlugPageProps = {
1213
params: {
13-
slug: string[];
14+
slug: string;
1415
};
1516
};
1617

18+
export async function generateMetadata({
19+
params,
20+
}: BlogSlugPageProps): Promise<Metadata> {
21+
const slug = params.slug;
22+
23+
const blog = (await getBlogs()).find((b) => b.metaData.slug === slug);
24+
25+
if (!blog) {
26+
return notFound();
27+
}
28+
29+
return {
30+
title: blog.metaData.title,
31+
description: blog.metaData.description,
32+
};
33+
}
34+
1735
export async function generateStaticParams() {
1836
const blogs = await getBlogs();
1937

2038
return blogs.map((blog) => ({
21-
slug: blog.metaData.slug.split("/"),
39+
slug: blog.metaData.slug,
2240
}));
2341
}
2442

@@ -27,7 +45,7 @@ export default async function BlogSlugPage({ params }: BlogSlugPageProps) {
2745
return redirect(siteUrls.blog);
2846
}
2947

30-
const slug = params.slug.join("/");
48+
const slug = params.slug;
3149

3250
const blog = (await getBlogs()).find((b) => b.metaData.slug === slug);
3351

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const blogPageConfig = {
2+
title: "Blog",
3+
} as const;

starterkits/saas/src/app/(web)/blog/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { getBlogs } from "@/server/actions/blog";
88
import { format } from "date-fns";
99
import Image from "next/image";
1010
import Link from "next/link";
11+
import { type Metadata } from "next";
12+
import { blogPageConfig } from "@/app/(web)/blog/_constants/page-config";
13+
14+
export const metadata: Metadata = {
15+
title: blogPageConfig.title,
16+
};
1117

1218
export const dynamic = "force-static";
1319

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const changelogPageConfig = {
2+
title: "Change Log",
3+
} as const;

starterkits/saas/src/app/(web)/changelog/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import { siteConfig } from "@/config/site";
1313
import { getChangelogs } from "@/server/actions/changelog";
1414
import { format } from "date-fns";
1515
import Image from "next/image";
16+
import type { Metadata } from "next";
17+
import { changelogPageConfig } from "@/app/(web)/changelog/_constants/page-config";
18+
19+
export const metadata: Metadata = {
20+
title: changelogPageConfig.title,
21+
};
1622

1723
export const dynamic = "force-static";
1824

@@ -35,11 +41,8 @@ export default async function ChangeLogPage() {
3541
</p>
3642
</WebPageHeader>
3743
<div className="grid w-full max-w-4xl gap-8">
38-
{changelogs.map((changelog) => (
39-
<ChangeLogCard
40-
key={changelog.metaData.slug}
41-
{...changelog}
42-
/>
44+
{changelogs.map((changelog, index) => (
45+
<ChangeLogCard key={index} {...changelog} />
4346
))}
4447
</div>
4548
</WebPageWrapper>

starterkits/saas/src/app/(web)/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import { siteUrls } from "@/config/urls";
1111
import Image from "next/image";
1212
import Link from "next/link";
1313
import Balancer from "react-wrap-balancer";
14+
import type { Metadata } from "next";
15+
16+
export const metadata: Metadata = {
17+
title: "Build Your MVP in Days, not weeks. Next.js Starter Kit",
18+
};
1419

1520
export const dynamic = "force-static";
1621

@@ -19,7 +24,7 @@ export default async function HomePage() {
1924
<WebPageWrapper>
2025
<WebPageHeader
2126
badge="Launch your saas in 24 hours"
22-
title="Rapidly launch your MVP with Beautiful Starterkits, Blocks, and more."
27+
title="Build Your MVP in Days, not weeks. Open Source Starter Kit"
2328
>
2429
<Balancer
2530
as="p"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const pricingPageConfig = {
2+
title: "Pricing",
3+
} as const;

starterkits/saas/src/app/(web)/pricing/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
WebPageHeader,
44
WebPageWrapper,
55
} from "@/app/(web)/_components/general-components";
6+
import { type Metadata } from "next";
7+
import { pricingPageConfig } from "@/app/(web)/pricing/_constants/page-config";
68

79
/**
810
* Customize the pricing page to your needs. You can use the `PricingPlans` component to display the pricing plans.
@@ -11,6 +13,10 @@ import {
1113
* To customize the pricing plans, you can modify the `PricingPlans` component. @see /app/(web)/pricing/components/pricing-plans.tsx
1214
*/
1315

16+
export const metadata: Metadata = {
17+
title: pricingPageConfig.title,
18+
};
19+
1420
export default function PricingPage() {
1521
return (
1622
<WebPageWrapper>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { siteConfig } from "@/config/site";
2+
3+
export const supportPageConfig = {
4+
title: "Support",
5+
description: `Get support from ${siteConfig.name} to get started building your next project.`,
6+
} as const;

starterkits/saas/src/app/(web)/support/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,21 @@ import {
1212
import { type SupportInfo, supportInfos } from "@/config/support";
1313
import { ArrowRightIcon } from "lucide-react";
1414
import Link from "next/link";
15+
import { type Metadata } from "next";
16+
import { supportPageConfig } from "@/app/(web)/support/_constants/page-config";
17+
18+
export const metadata: Metadata = {
19+
title: supportPageConfig.title,
20+
description: supportPageConfig.description,
21+
};
1522

1623
export default function ContactPage() {
1724
return (
1825
<WebPageWrapper>
19-
<WebPageHeader title="Support for You" badge="Get in touch with us">
26+
<WebPageHeader
27+
title={supportPageConfig.title}
28+
badge="Get in touch with us"
29+
>
2030
<p>
2131
If you have any questions or need help, feel free to reach
2232
out to us.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { siteConfig } from "@/config/site";
2+
3+
export const loginPageConfig = {
4+
title: "Login",
5+
description: `Login to ${siteConfig.name} to get started building your next project.`,
6+
} as const;

starterkits/saas/src/app/auth/login/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { AuthForm } from "@/app/auth/_components/auth-form";
2+
import { loginPageConfig } from "@/app/auth/login/_constants/page-config";
3+
import { type Metadata } from "next";
4+
5+
export const metadata: Metadata = {
6+
title: loginPageConfig.title,
7+
description: loginPageConfig.description,
8+
};
29

310
export default function Login() {
411
return <AuthForm type="login" />;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { siteConfig } from "@/config/site";
2+
3+
export const signupPageConfig = {
4+
title: "Signup",
5+
description: `Signup to ${siteConfig.name} to get started building your next project.`,
6+
} as const;

starterkits/saas/src/app/auth/signup/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { AuthForm } from "@/app/auth/_components/auth-form";
2+
import { signupPageConfig } from "@/app/auth/signup/_constants/page-config";
3+
import { type Metadata } from "next";
4+
5+
export const metadata: Metadata = {
6+
title: signupPageConfig.title,
7+
description: signupPageConfig.description,
8+
};
29

310
export default function Signup() {
411
return <AuthForm type="signup" />;

starterkits/saas/src/app/docs/[[...slug]]/page.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { notFound } from "next/navigation";
22
import { Toc } from "@/components/toc";
33
import { getDocs } from "@/server/actions/docs";
4+
import { type Metadata } from "next";
45

56
export const dynamic = "force-static";
67

@@ -10,13 +11,26 @@ type DocsSlugPageProps = {
1011
};
1112
};
1213

14+
export async function generateMetadata({
15+
params,
16+
}: DocsSlugPageProps): Promise<Metadata> {
17+
const slug = Array.isArray(params.slug) ? params.slug.join("/") : "/";
18+
19+
const doc = (await getDocs()).find((doc) => doc.metaData.slug === slug);
20+
21+
if (!doc) {
22+
return notFound();
23+
}
24+
25+
return {
26+
title: doc.metaData.title,
27+
description: doc.metaData.description,
28+
};
29+
}
30+
1331
export async function generateStaticParams() {
1432
const docs = await getDocs();
1533

16-
docs.map((doc) => {
17-
console.log(doc.metaData.slug.split("/") || ["/"]);
18-
});
19-
2034
return docs.map((doc) => ({
2135
slug: doc.metaData.slug.split("/") || ["/"],
2236
}));
@@ -27,6 +41,8 @@ export default async function DocsSlugPage({ params }: DocsSlugPageProps) {
2741

2842
const doc = (await getDocs()).find((doc) => doc.metaData.slug === slug);
2943

44+
console.log(["gettings-started", "installation"].join("/"), params.slug);
45+
3046
if (!doc) {
3147
return notFound();
3248
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const invitePageConfig = {
2+
title: ({ orgName }: { orgName: string }) => `Invite to ${orgName}`,
3+
description: ({ orgName }: { orgName: string }) =>
4+
`Invite your team to ${orgName} and get started building your next project.`,
5+
} as const;

starterkits/saas/src/app/invite/org/[orgId]/page.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { getOrgByIdQuery } from "@/server/actions/organization/queries";
22
import { RequestCard } from "@/app/invite/org/[orgId]/_components/request-card";
33
import { notFound } from "next/navigation";
4+
import { type Metadata } from "next";
45

5-
type OrgRequestProps = {
6+
export type OrgRequestProps = {
67
params: {
78
orgId: string;
89
};
@@ -23,3 +24,18 @@ export default async function OrgRequestPage({
2324
</main>
2425
);
2526
}
27+
28+
export async function generateMetadata({
29+
params,
30+
}: OrgRequestProps): Promise<Metadata> {
31+
const org = await getOrgByIdQuery({ orgId: params.orgId });
32+
33+
if (!org) {
34+
return notFound();
35+
}
36+
37+
return {
38+
title: `Invite to ${org.name}`,
39+
description: `Invite your team to ${org.name} and get started building your next project.`,
40+
};
41+
}

starterkits/saas/src/app/layout.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ import { Toaster } from "@/components/ui/sonner";
44
import "@/styles/globals.css";
55
import "@/styles/prism.css";
66
import { fontHeading, fontSans } from "@/lib/fonts";
7+
import { type Metadata } from "next";
8+
import {
9+
defaultMetadata,
10+
twitterMetadata,
11+
ogMetadata,
12+
} from "@/app/shared-metadata";
713

8-
export const metadata = {
9-
title: "RapidLaunch - Next.js Boilerplate",
10-
description:
11-
"Next.js boilerplate with shadcn ui, TRPC, TailwindCSS, and Drizzle.",
12-
icons: [{ rel: "icon", url: "/favicon.ico" }],
14+
export const metadata: Metadata = {
15+
...defaultMetadata,
16+
twitter: {
17+
...twitterMetadata,
18+
},
19+
openGraph: {
20+
...ogMetadata,
21+
},
1322
};
1423

1524
export default function RootLayout({
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const maintenancePageConfig = {
2+
title: "Maintenance",
3+
description:
4+
"We&apos;re currently undergoing maintenance. Please check back later.",
5+
} as const;

starterkits/saas/src/app/maintenance/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import { maintenancePageConfig } from "@/app/maintenance/_constants/page-config";
12
import { siteConfig } from "@/config/site";
3+
import { type Metadata } from "next";
4+
5+
export const metadata: Metadata = {
6+
title: maintenancePageConfig.title,
7+
description: maintenancePageConfig.description,
8+
};
29

310
export default function Maintenance() {
411
return (

starterkits/saas/src/app/robots.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { siteUrls } from "@/config/urls";
2+
import type { MetadataRoute } from "next";
3+
4+
export default function robots(): MetadataRoute.Robots {
5+
return {
6+
rules: {
7+
userAgent: "*",
8+
allow: "/",
9+
},
10+
sitemap: `${siteUrls.publicUrl}/sitemap.xml`,
11+
};
12+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { siteConfig } from "@/config/site";
2+
import { siteUrls } from "@/config/urls";
3+
import type { Metadata } from "next";
4+
5+
export const defaultMetadata: Metadata = {
6+
title: {
7+
template: `%s | ${siteConfig.name}`,
8+
default: siteConfig.name,
9+
},
10+
description: siteConfig.description,
11+
metadataBase: new URL(siteUrls.publicUrl),
12+
keywords: [
13+
"Next.js",
14+
"React",
15+
"Next.js Starter kit",
16+
"SaaS Starter Kit",
17+
"Shadcn UI",
18+
],
19+
authors: [{ name: "Ali Farooq", url: "https://twitter.com/alifarooqdev" }],
20+
creator: "AliFarooqDev",
21+
};
22+
23+
export const twitterMetadata: Metadata["twitter"] = {
24+
title: siteConfig.name,
25+
description: siteConfig.description,
26+
card: "summary_large_image",
27+
images: [siteConfig.orgImage],
28+
creator: "@alifarooqdev",
29+
};
30+
31+
export const ogMetadata: Metadata["openGraph"] = {
32+
title: siteConfig.name,
33+
description: siteConfig.description,
34+
type: "website",
35+
images: [{ url: siteConfig.orgImage, alt: siteConfig.name }],
36+
locale: "en_US",
37+
url: siteUrls.publicUrl,
38+
siteName: siteConfig.name,
39+
};

0 commit comments

Comments
 (0)