Skip to content

Commit 4aecadb

Browse files
authored
App Router Migration (#221)
1 parent d059298 commit 4aecadb

File tree

183 files changed

+13445
-12367
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

183 files changed

+13445
-12367
lines changed

.env.example

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
# Rename to .env
1+
# DON'T FORGET TO RENAME TO .env OR .env.local BEFORE PUSHING TO GIT
22

33
### DEVELOPMENT ONLY VARIABLES
4-
# These variables need to be set
5-
# for local development only
4+
# These variables need to be set for local development only
65

76
# Mandatory next-auth URL for localhost
87
NEXTAUTH_URL=http://app.localhost:3000
98

109
### PRODUCTION & DEVELOPMENT VARIABLES
11-
# These variables need to be set
12-
# for local development and when deployed on Vercel
10+
# These variables need to be set for local development and when deployed on Vercel
1311

14-
# MySQL database URL for Prisma
15-
DATABASE_URL=mysql://[email protected]:3309/platforms
12+
# Change this to your own domain
13+
NEXT_PUBLIC_ROOT_DOMAIN=vercel.pub
1614

17-
# GitHub OAuth https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
15+
# PostgreSQL database URL – get one here: https://vercel.com/docs/storage/vercel-postgres/quickstart
16+
POSTGRES_PRISMA_URL=
17+
18+
# Vercel Blob Storage for image uploads – get one here: https://vercel.com/docs/storage/blob/quickstart
19+
BLOB_READ_WRITE_TOKEN=
20+
21+
# GitHub OAuth for auth & login – get one here: https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
1822
GITHUB_ID=
1923
GITHUB_SECRET=
2024

21-
# Twitter Auth Bearer token (for static tweets)
22-
TWITTER_AUTH_TOKEN=
23-
24-
# Secret key (generate one here: https://generate-secret.vercel.app/32)
25+
# Secret key for NextAuth (generate one here: https://generate-secret.vercel.app/32)
2526
NEXTAUTH_SECRET=
2627

2728
# https://vercel.com/account/tokens

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
**.env
44
**.DS_Store
55
**.vercel
6+
.vscode
67
**.git
78
.vercel

.vscode/settings.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
## Introduction
2727

28-
Multi-tenant applications serve multiple customers across different subdomains/custom domains with a single unified codebase.
28+
Multi-tenant applications serve multiple customers across different subdomains/custom domains with a single unified codebase.
2929

3030
For example, our demo is a multi-tenant application:
3131

@@ -51,7 +51,7 @@ Forget manually setting up CNAME records, wrestling with DNS, or making custom s
5151

5252
- **Custom domains**: Subdomain and custom domains support with [Edge Functions](https://vercel.com/features/edge-functions) and the [Vercel Domains API](https://domains-api.vercel.app/).
5353
- **Static generation with ISR**: Performance without sacrificing personalization, by combining [Incremental Static Regeneration](https://vercel.com/docs/concepts/next.js/incremental-static-regeneration) (ISR) and [Middleware](https://vercel.com/docs/concepts/functions/edge-functions#middleware). ISR allows you to create new content (with custom domains) on demand without needing to redeploy your application.
54-
- **Uploading custom images**: Allow your customers to upload custom thumbnail images with our Cloudinary integration.
54+
- **Uploading custom images**: Allow your customers to upload custom thumbnail images with [Vercel Blob](https://vercel.com/docs/storage/vercel-blob).
5555
- **Static tweets**: Avoid [Cumulative Layout Shift](https://vercel.com/blog/core-web-vitals) (CLS) from the native Twitter embed by using our [static tweets implementation](https://static-tweets-tailwind.vercel.app/) (supports image, video, gif, poll, retweets, quote retweets, and more).
5656

5757
## Examples of platforms
@@ -60,7 +60,7 @@ Vercel customers like [Hashnode](https://vercel.com/customers/hashnode), [Super]
6060

6161
### 1. Content creation platforms
6262

63-
These are content-heavy platforms (blogs) with simple, standardized page layouts and route structure.
63+
These are content-heavy platforms (blogs) with simple, standardized page layouts and route structure.
6464

6565
> “With Vercel, we spend less time managing our infrastructure and more time delivering value to our users.” — Sandeep Panda, Co-founder, Hashnode
6666
@@ -70,7 +70,7 @@ These are content-heavy platforms (blogs) with simple, standardized page layouts
7070

7171
### 2. Website & e-commerce store builders
7272

73-
No-code site builders with customizable pages.
73+
No-code site builders with customizable pages.
7474

7575
By using Next.js and Vercel, [Super](https://super.so/) has fast, globally distributed websites with a no-code editor (Notion). Their customers get all the benefits of Next.js (like [Image Optimization](https://nextjs.org/docs/basic-features/image-optimization)) without touching any code.
7676

@@ -82,7 +82,7 @@ By using Next.js and Vercel, [Super](https://super.so/) has fast, globally distr
8282

8383
Multi-tenant authentication, login, and access controls.
8484

85-
With Vercel and Next.js, platforms like [Instatus](https://instatus.com) are able to create status pages that are *10x faster* than competitors.
85+
With Vercel and Next.js, platforms like [Instatus](https://instatus.com) are able to create status pages that are _10x faster_ than competitors.
8686

8787
1. [Instatus](https://instatus.com/)
8888
2. [Cal.com](https://cal.com/)
@@ -111,12 +111,10 @@ We also have another [example](https://github.com/vercel/examples/tree/main/solu
111111

112112
The beauty about a serverless setup is you won’t have to worry about load since each request invokes a separate serverless function, and once it’s cached, you don’t invoke the server anymore (the page is served directly from the Vercel edge). Read more about the [Vercel Edge Network](https://vercel.com/docs/concepts/edge-network/overview) and [how caching works](https://vercel.com/docs/concepts/edge-network/caching).
113113

114-
115114
## Caveats
116115

117116
- This template does not work with i18n, which is an [advanced feature in Next.js](https://nextjs.org/docs/advanced-features/i18n-routing).
118117

119-
120118
## Contributing
121119

122120
- [Start a discussion](https://github.com/vercel/platforms/discussions) with a question, piece of feedback, or idea you want to share with the team.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* eslint-disable @next/next/no-img-element */
2+
3+
import { truncate } from "@/lib/utils";
4+
import { ImageResponse } from "next/server";
5+
import { sql } from "@vercel/postgres";
6+
7+
export const runtime = "edge";
8+
9+
export default async function PostOG({
10+
params,
11+
}: {
12+
params: { domain: string; slug: string };
13+
}) {
14+
const { domain, slug } = params;
15+
16+
const subdomain = domain.endsWith(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`)
17+
? domain.replace(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`, "")
18+
: null;
19+
20+
const response = await sql`
21+
SELECT post.title, post.description, post.image, "user".name as "authorName", "user".image as "authorImage"
22+
FROM "Post" AS post
23+
INNER JOIN "Site" AS site ON post."siteId" = site.id
24+
INNER JOIN "User" AS "user" ON site."userId" = "user".id
25+
WHERE
26+
(
27+
site.subdomain = ${subdomain}
28+
OR site."customDomain" = ${domain}
29+
)
30+
AND post.slug = ${slug}
31+
LIMIT 1;
32+
`;
33+
34+
const data = response.rows[0];
35+
36+
if (!data) {
37+
return new Response("Not found", { status: 404 });
38+
}
39+
40+
const clashData = await fetch(
41+
new URL("@/styles/CalSans-SemiBold.otf", import.meta.url)
42+
).then((res) => res.arrayBuffer());
43+
44+
return new ImageResponse(
45+
(
46+
<div tw="flex flex-col items-center w-full h-full bg-white">
47+
<div tw="flex flex-col items-center justify-center mt-8">
48+
<h1 tw="text-6xl font-bold text-gray-900 leading-none tracking-tight">
49+
{data.title}
50+
</h1>
51+
<p tw="mt-4 text-xl text-gray-600 max-w-xl text-center">
52+
{truncate(data.description, 120)}
53+
</p>
54+
<div tw="flex items-center justify-center">
55+
<img
56+
tw="w-12 h-12 rounded-full mr-4"
57+
src={data.authorImage}
58+
alt={data.authorName}
59+
/>
60+
<p tw="text-xl font-medium text-gray-900">by {data.authorName}</p>
61+
</div>
62+
<img
63+
tw="mt-4 w-5/6 rounded-2xl border border-gray-200 shadow-md"
64+
src={data.image}
65+
alt={data.title}
66+
/>
67+
</div>
68+
</div>
69+
),
70+
{
71+
width: 1200,
72+
height: 600,
73+
fonts: [
74+
{
75+
name: "Clash",
76+
data: clashData,
77+
},
78+
],
79+
emoji: "blobmoji",
80+
}
81+
);
82+
}

app/[domain]/[slug]/page.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { notFound } from "next/navigation";
2+
import { getPostData } from "@/lib/fetchers";
3+
import BlogCard from "@/components/blog-card";
4+
import BlurImage from "@/components/blur-image";
5+
import MDX from "@/components/mdx";
6+
import { placeholderBlurhash, toDateString } from "@/lib/utils";
7+
8+
export async function generateMetadata({
9+
params,
10+
}: {
11+
params: { domain: string; slug: string };
12+
}) {
13+
const { domain, slug } = params;
14+
const data = await getPostData(domain, slug);
15+
if (!data) {
16+
notFound();
17+
}
18+
const { title, description } = data;
19+
20+
return {
21+
title,
22+
description,
23+
openGraph: {
24+
title,
25+
description,
26+
},
27+
twitter: {
28+
card: "summary_large_image",
29+
title,
30+
description,
31+
creator: "@vercel",
32+
},
33+
};
34+
}
35+
36+
export default async function SitePostPage({
37+
params,
38+
}: {
39+
params: { domain: string; slug: string };
40+
}) {
41+
const { domain, slug } = params;
42+
const data = await getPostData(domain, slug);
43+
44+
if (!data) {
45+
notFound();
46+
}
47+
48+
return (
49+
<>
50+
<div className="flex flex-col justify-center items-center">
51+
<div className="text-center w-full md:w-7/12 m-auto">
52+
<p className="text-sm md:text-base font-light text-gray-500 w-10/12 m-auto my-5">
53+
{toDateString(data.createdAt)}
54+
</p>
55+
<h1 className="font-bold text-3xl font-title md:text-6xl mb-10 text-gray-800">
56+
{data.title}
57+
</h1>
58+
<p className="text-md md:text-lg text-gray-600 w-10/12 m-auto">
59+
{data.description}
60+
</p>
61+
</div>
62+
<a
63+
// if you are using Github OAuth, you can get rid of the Twitter option
64+
href={
65+
data.site?.user?.username
66+
? `https://twitter.com/${data.site.user.username}`
67+
: `https://github.com/${data.site?.user?.gh_username}`
68+
}
69+
rel="noreferrer"
70+
target="_blank"
71+
>
72+
<div className="my-8">
73+
<div className="relative w-8 h-8 md:w-12 md:h-12 rounded-full overflow-hidden inline-block align-middle">
74+
{data.site?.user?.image ? (
75+
<BlurImage
76+
alt={data.site?.user?.name ?? "User Avatar"}
77+
height={80}
78+
src={data.site.user.image}
79+
width={80}
80+
/>
81+
) : (
82+
<div className="absolute flex items-center justify-center w-full h-full bg-gray-100 text-gray-500 text-4xl select-none">
83+
?
84+
</div>
85+
)}
86+
</div>
87+
<div className="inline-block text-md md:text-lg align-middle ml-3">
88+
by <span className="font-semibold">{data.site?.user?.name}</span>
89+
</div>
90+
</div>
91+
</a>
92+
</div>
93+
<div className="relative h-80 md:h-150 w-full max-w-screen-lg lg:w-2/3 md:w-5/6 m-auto mb-10 md:mb-20 md:rounded-2xl overflow-hidden">
94+
<BlurImage
95+
alt={data.title ?? "Post image"}
96+
width={1200}
97+
height={630}
98+
className="w-full h-full object-cover"
99+
placeholder="blur"
100+
blurDataURL={data.imageBlurhash ?? placeholderBlurhash}
101+
src={data.image ?? "/placeholder.png"}
102+
/>
103+
</div>
104+
105+
<MDX source={data.mdxSource} />
106+
107+
{data.adjacentPosts.length > 0 && (
108+
<div className="relative mt-10 sm:mt-20 mb-20">
109+
<div
110+
className="absolute inset-0 flex items-center"
111+
aria-hidden="true"
112+
>
113+
<div className="w-full border-t border-gray-300" />
114+
</div>
115+
<div className="relative flex justify-center">
116+
<span className="px-2 bg-white text-sm text-gray-500">
117+
Continue Reading
118+
</span>
119+
</div>
120+
</div>
121+
)}
122+
{data.adjacentPosts && (
123+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-x-4 gap-y-8 mx-5 lg:mx-12 2xl:mx-auto mb-20 max-w-screen-xl">
124+
{data.adjacentPosts.map((data, index) => (
125+
<BlogCard key={index} data={data} />
126+
))}
127+
</div>
128+
)}
129+
</>
130+
);
131+
}

0 commit comments

Comments
 (0)