Abdessamad Ely Logo

Nextjs Open Graph

Source code for the Nextjs Open Graph project.

Abdessamad Ely
Abdessamad Ely
Software Engineer

OpenGraph.tsx

export type OGProperties = {
  locale?: "en_US" | "fr_FR";
  url: string;
  title: string;
  type: "article" | "website";
  description: string;
  site_name: string;
  image: {
    alt: string;
    type: string;
    url: string;
    width: string;
    height: string;
  } | null;
  author?: string;
  section?: string;
  modified_time?: string;
  published_time?: string;
  card: "summary" | "summary_large_image" | "app" | "player";
};

const OpenGraph = ({ properties }: { properties: OGProperties }) => {
  const {
    locale,
    url,
    site_name,
    title,
    type,
    description,
    author,
    section,
    image,
    modified_time,
    published_time,
    card,
  } = properties;

  return (
    <>
      <meta property="og:locale" content={locale || "en_US"} />
      <meta property="og:title" content={title} />
      <meta property="og:type" content={type} />
      <meta property="og:description" content={description || ""} />
      <meta property="og:url" content={url} />
      <meta property="og:site_name" content={site_name} />
      {type === "article" && (
        <>
          <meta property="article:author" content={author} />
          <meta property="article:section" content={section} />
          <meta property="article:modified_time" content={modified_time} />
          <meta property="article:published_time" content={published_time} />
        </>
      )}
      {image && (
        <>
          <meta property="og:image" content={image.url} />
          <meta
            property="og:image:secure_url"
            content={image.url.replace("http://", "https://")}
          />
          <meta property="og:image:width" content={image.width} />
          <meta property="og:image:height" content={image.height} />
          <meta property="og:image:alt" content={image.alt} />
          <meta property="og:image:type" content={image.type} />
          <meta name="twitter:image" content={image.url} />
        </>
      )}
      <meta name="twitter:card" content={card} />
      <meta name="twitter:url" content={url} />
      <meta name="twitter:domain" content="codersteps.com" />
      <meta name="twitter:title" content={title} />
      <meta name="twitter:description" content={description || ""} />
      <meta name="twitter:site" content="@codersteps" />
      <meta name="twitter:creator" content="@abdessamadely" />
    </>
  );
};

export default OpenGraph;

helpers.ts

export const absUrl = (path: string): string => {
  path = path.trim();
  if (path.startsWith("http")) {
    return path;
  }
  if (path.indexOf("/") === 0) {
    path = path.substring(1);
  }

  const appUrl =
    process.env.APP_URL ||
    process.env.NEXT_PUBLIC_APP_URL ||
    "http://localhost:3000";

  return `${appUrl}/${path}`;
};

posts.json

[
  {
    "id": 1,
    "title": "Post 1",
    "thumbnail": "/images/og-posts-image.jpg",
    "description": "Description of post 1",
    "content": "Content of post 1"
  },
  {
    "id": 2,
    "title": "Post 2",
    "thumbnail": "/images/og-posts-image.jpg",
    "description": "Description of post 2",
    "content": "Content of post 2"
  },
  {
    "id": 3,
    "title": "Post 3",
    "thumbnail": "/images/og-posts-image.jpg",
    "description": "Description of post 3",
    "content": "Content of post 3"
  }
]

useOpenGraph.ts

import { useMemo } from "react";
import { absUrl } from "../core/helpers";
import { OGProperties } from "../components/common/OpenGraph";

type OGImage = {
  alt: string;
  type: string;
  url: string;
  width?: string;
  height?: string;
} | null;

type PageOgData = Omit<OGProperties, "image" | "card" | "site_name"> & {
  card?: OGProperties["card"];
  image: OGImage;
};

export const useOpenGraph = (data: PageOgData) => {
  const ogProperties = useMemo<OGProperties>(() => {
    return {
      url: data.url,
      title: data.title,
      type: data.type,
      author: data.author,
      site_name: "OpenGraph Article",
      description: data.description,
      image: data.image
        ? {
            type: data.image.type,
            url: absUrl(data.image.url),
            alt: data.image.alt || "",
            height: data.image.height || "720",
            width: data.image.width || "420",
          }
        : null,
      card: data.card || data.image ? "summary_large_image" : "summary",
      section: data.section,
      modified_time: data.modified_time,
      published_time: data.published_time,
    };
  }, [data]);

  return ogProperties;
};

export default useOpenGraph;

hello.ts

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

[id].tsx

import Head from "next/head";
import Image from "next/image";
import { GetServerSideProps, NextPage } from "next";
import OpenGraph from "../../components/common/OpenGraph";
import useOpenGraph from "../../hooks/useOpenGraph";
import { absUrl } from "../../core/helpers";

type Post = {
  id: number;
  title: string;
  thumbnail: string;
  description: string;
  content: string;
};

interface ServerSideProps {
  post: Post;
}

const ShowPost: NextPage<ServerSideProps> = ({ post }) => {
  const ogProperties = useOpenGraph({
    url: absUrl(`/posts/${post.id}`),
    title: post.title,
    image: {
      // The post thumbnail
      type: "image/jpeg", // replace it with a dynamic thumbnail mimetype
      url: post.thumbnail,
      alt: post.title,
    },
    description: post.description,
    type: "article",
    author: "Article Author",
    section: "Article Category",
    modified_time: "Post Updated Time",
    published_time: "Post Published Time",
  });

  return (
    <>
      <Head>
        <title>{post.title}</title>

        <OpenGraph properties={ogProperties} />
      </Head>

      <main>
        <article>
          <h1>{post.title}</h1>

          <Image
            width={720}
            height={420}
            src={post.thumbnail}
            alt={post.title}
          />

          <div dangerouslySetInnerHTML={{ __html: post.content }}></div>
        </article>
      </main>
    </>
  );
};

export const getServerSideProps: GetServerSideProps<ServerSideProps> = async ({
  params,
}) => {
  if (!params || typeof params.id !== "string" || isNaN(+params.id)) {
    return { notFound: true };
  }

  const posts: Post[] = require("../../data/posts.json");

  const postId = +params.id;
  const post = posts.find((p) => p.id === postId);
  if (!post) {
    return { notFound: true };
  }

  return {
    props: {
      post,
    },
  };
};

export default ShowPost;

_app.tsx

import '../styles/globals.css'
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp

index.tsx

import type { NextPage } from "next";
import Head from "next/head";
import OpenGraph from "../components/common/OpenGraph";
import { useOpenGraph } from "../hooks/useOpenGraph";
import { absUrl } from "../core/helpers";

const Home: NextPage = () => {
  const ogProperties = useOpenGraph({
    url: absUrl("/"),
    title: "OpenGraph Article", // Add you homepage title
    image: {
      // some default image preview for your website
      type: "image/jpeg",
      url: "/assets/images/ogImage.jpg",
      alt: "Image Alt",
    },
    description: "Your website description",
    type: "website",
  });

  return (
    <div>
      <Head>
        <title>OpenGraph with Next.js</title>
        <meta
          name="description"
          content="Article about OpenGraph with Next.js"
        />

        <OpenGraph properties={ogProperties} />
      </Head>

      <main>
        <h1>OpenGraph with Next.js</h1>
        <div>An empty page is a good page</div>
      </main>
    </div>
  );
};

export default Home;

globals.css

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

.eslintrc.json

{
  "extends": "next/core-web-vitals"
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
}

module.exports = nextConfig

package.json

{
  "name": "open_graph",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.1.6",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@types/node": "18.0.0",
    "@types/react": "18.0.14",
    "@types/react-dom": "18.0.5",
    "eslint": "8.18.0",
    "eslint-config-next": "12.1.6",
    "typescript": "4.7.4"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}