NextJS Demo App

First Post

This is the first post

/app/posts/[id]/page.tsx

import { Suspense } from "react";
import { Metadata } from "next";

import { Code } from "@/components/code";
import { Row } from "@/components/row";

import { getPost, getPosts } from "@/lib/actions";

// Surround Post Component with Suspense to show a loading state
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <Suspense fallback={<div>Loading post...</div>}>
      <Post id={id} />
    </Suspense>
  );
}

export async function generateStaticParams() {
  const { data: posts } = await getPosts();
  if (!posts) return [];
  return posts.map((post) => ({ id: post.id.toString() }));
}

// The component that fetches and renders the post
async function Post({ id }: { id: string }) {
  const post = await getPost(id);

  if (!post) {
    return <div>Post not found</div>;
  }
  return (
    <Row>
      <div className="flex flex-col gap-4">
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </div>
      <div>
        <Code path="/app/posts/[id]/page.tsx" />
        <Code path="/lib/actions.ts" sub={[38, 43]} />
        <Code path="/app/posts/[id]/opengraph-image.tsx" />
      </div>
    </Row>
  );
}

// Metadata for SEO
export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>;
}): Promise<Metadata> {
  const { id } = await params;

  const post = await getPost(id);

  return {
    title: post?.title,
  };
}

/lib/actions.ts (lines 39-44)

interface ActionResponse<T> {
  data?: T;
  error?: Error;
}

/app/posts/[id]/opengraph-image.tsx

import { getPost } from "@/lib/actions";
import { ImageResponse } from "next/og";

// Image metadata
export const size = {
  width: 1200,
  height: 630,
};

export const contentType = "image/png";

// Image generation
export default async function Image({ params }: { params: { id: string } }) {
  const { id } = params;

  const post = await getPost(id);

  return new ImageResponse(
    (
      <div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: "#fff",
          fontSize: 32,
          fontWeight: 600,
        }}
      >
        <svg
          width="75"
          viewBox="0 0 75 65"
          fill="#000"
          style={{ margin: "0 75px" }}
        >
          <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
        </svg>
        <div style={{ marginTop: 40 }}>{post?.title}</div>
      </div>
    )
  );
}