Skip to content

Commit

Permalink
App Router Migration (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-tey authored Jun 29, 2023
1 parent d059298 commit 4aecadb
Show file tree
Hide file tree
Showing 183 changed files with 13,445 additions and 12,367 deletions.
25 changes: 13 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
# Rename to .env
# DON'T FORGET TO RENAME TO .env OR .env.local BEFORE PUSHING TO GIT

### DEVELOPMENT ONLY VARIABLES
# These variables need to be set
# for local development only
# These variables need to be set for local development only

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

### PRODUCTION & DEVELOPMENT VARIABLES
# These variables need to be set
# for local development and when deployed on Vercel
# These variables need to be set for local development and when deployed on Vercel

# MySQL database URL for Prisma
DATABASE_URL=mysql://[email protected]:3309/platforms
# Change this to your own domain
NEXT_PUBLIC_ROOT_DOMAIN=vercel.pub

# GitHub OAuth https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
# PostgreSQL database URL – get one here: https://vercel.com/docs/storage/vercel-postgres/quickstart
POSTGRES_PRISMA_URL=

# Vercel Blob Storage for image uploads – get one here: https://vercel.com/docs/storage/blob/quickstart
BLOB_READ_WRITE_TOKEN=

# GitHub OAuth for auth & login – get one here: https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
GITHUB_ID=
GITHUB_SECRET=

# Twitter Auth Bearer token (for static tweets)
TWITTER_AUTH_TOKEN=

# Secret key (generate one here: https://generate-secret.vercel.app/32)
# Secret key for NextAuth (generate one here: https://generate-secret.vercel.app/32)
NEXTAUTH_SECRET=

# https://vercel.com/account/tokens
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
**.env
**.DS_Store
**.vercel
.vscode
**.git
.vercel
4 changes: 0 additions & 4 deletions .vscode/settings.json

This file was deleted.

12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

## Introduction

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

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

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

- **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/).
- **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.
- **Uploading custom images**: Allow your customers to upload custom thumbnail images with our Cloudinary integration.
- **Uploading custom images**: Allow your customers to upload custom thumbnail images with [Vercel Blob](https://vercel.com/docs/storage/vercel-blob).
- **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).

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

### 1. Content creation platforms

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

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

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

No-code site builders with customizable pages.
No-code site builders with customizable pages.

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.

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

Multi-tenant authentication, login, and access controls.

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

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

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).


## Caveats

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


## Contributing

- [Start a discussion](https://github.com/vercel/platforms/discussions) with a question, piece of feedback, or idea you want to share with the team.
Expand Down
82 changes: 82 additions & 0 deletions app/[domain]/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable @next/next/no-img-element */

import { truncate } from "@/lib/utils";
import { ImageResponse } from "next/server";
import { sql } from "@vercel/postgres";

export const runtime = "edge";

export default async function PostOG({
params,
}: {
params: { domain: string; slug: string };
}) {
const { domain, slug } = params;

const subdomain = domain.endsWith(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`)
? domain.replace(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`, "")
: null;

const response = await sql`
SELECT post.title, post.description, post.image, "user".name as "authorName", "user".image as "authorImage"
FROM "Post" AS post
INNER JOIN "Site" AS site ON post."siteId" = site.id
INNER JOIN "User" AS "user" ON site."userId" = "user".id
WHERE
(
site.subdomain = ${subdomain}
OR site."customDomain" = ${domain}
)
AND post.slug = ${slug}
LIMIT 1;
`;

const data = response.rows[0];

if (!data) {
return new Response("Not found", { status: 404 });
}

const clashData = await fetch(
new URL("@/styles/CalSans-SemiBold.otf", import.meta.url)
).then((res) => res.arrayBuffer());

return new ImageResponse(
(
<div tw="flex flex-col items-center w-full h-full bg-white">
<div tw="flex flex-col items-center justify-center mt-8">
<h1 tw="text-6xl font-bold text-gray-900 leading-none tracking-tight">
{data.title}
</h1>
<p tw="mt-4 text-xl text-gray-600 max-w-xl text-center">
{truncate(data.description, 120)}
</p>
<div tw="flex items-center justify-center">
<img
tw="w-12 h-12 rounded-full mr-4"
src={data.authorImage}
alt={data.authorName}
/>
<p tw="text-xl font-medium text-gray-900">by {data.authorName}</p>
</div>
<img
tw="mt-4 w-5/6 rounded-2xl border border-gray-200 shadow-md"
src={data.image}
alt={data.title}
/>
</div>
</div>
),
{
width: 1200,
height: 600,
fonts: [
{
name: "Clash",
data: clashData,
},
],
emoji: "blobmoji",
}
);
}
131 changes: 131 additions & 0 deletions app/[domain]/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { notFound } from "next/navigation";
import { getPostData } from "@/lib/fetchers";
import BlogCard from "@/components/blog-card";
import BlurImage from "@/components/blur-image";
import MDX from "@/components/mdx";
import { placeholderBlurhash, toDateString } from "@/lib/utils";

export async function generateMetadata({
params,
}: {
params: { domain: string; slug: string };
}) {
const { domain, slug } = params;
const data = await getPostData(domain, slug);
if (!data) {
notFound();
}
const { title, description } = data;

return {
title,
description,
openGraph: {
title,
description,
},
twitter: {
card: "summary_large_image",
title,
description,
creator: "@vercel",
},
};
}

export default async function SitePostPage({
params,
}: {
params: { domain: string; slug: string };
}) {
const { domain, slug } = params;
const data = await getPostData(domain, slug);

if (!data) {
notFound();
}

return (
<>
<div className="flex flex-col justify-center items-center">
<div className="text-center w-full md:w-7/12 m-auto">
<p className="text-sm md:text-base font-light text-gray-500 w-10/12 m-auto my-5">
{toDateString(data.createdAt)}
</p>
<h1 className="font-bold text-3xl font-title md:text-6xl mb-10 text-gray-800">
{data.title}
</h1>
<p className="text-md md:text-lg text-gray-600 w-10/12 m-auto">
{data.description}
</p>
</div>
<a
// if you are using Github OAuth, you can get rid of the Twitter option
href={
data.site?.user?.username
? `https://twitter.com/${data.site.user.username}`
: `https://github.com/${data.site?.user?.gh_username}`
}
rel="noreferrer"
target="_blank"
>
<div className="my-8">
<div className="relative w-8 h-8 md:w-12 md:h-12 rounded-full overflow-hidden inline-block align-middle">
{data.site?.user?.image ? (
<BlurImage
alt={data.site?.user?.name ?? "User Avatar"}
height={80}
src={data.site.user.image}
width={80}
/>
) : (
<div className="absolute flex items-center justify-center w-full h-full bg-gray-100 text-gray-500 text-4xl select-none">
?
</div>
)}
</div>
<div className="inline-block text-md md:text-lg align-middle ml-3">
by <span className="font-semibold">{data.site?.user?.name}</span>
</div>
</div>
</a>
</div>
<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">
<BlurImage
alt={data.title ?? "Post image"}
width={1200}
height={630}
className="w-full h-full object-cover"
placeholder="blur"
blurDataURL={data.imageBlurhash ?? placeholderBlurhash}
src={data.image ?? "/placeholder.png"}
/>
</div>

<MDX source={data.mdxSource} />

{data.adjacentPosts.length > 0 && (
<div className="relative mt-10 sm:mt-20 mb-20">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="px-2 bg-white text-sm text-gray-500">
Continue Reading
</span>
</div>
</div>
)}
{data.adjacentPosts && (
<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">
{data.adjacentPosts.map((data, index) => (
<BlogCard key={index} data={data} />
))}
</div>
)}
</>
);
}
Loading

0 comments on commit 4aecadb

Please sign in to comment.