diff --git a/components/Shell.tsx b/components/Shell.tsx index 312219c3..26b88ccc 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -3,7 +3,7 @@ import React, { Fragment, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { signOut, useSession } from "next-auth/client"; import { Menu, Transition } from "@headlessui/react"; -import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../lib/telemetry"; +import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { SelectorIcon } from "@heroicons/react/outline"; import { CalendarIcon, @@ -20,6 +20,7 @@ import classNames from "@lib/classNames"; import { Toaster } from "react-hot-toast"; import Avatar from "@components/Avatar"; import { User } from "@prisma/client"; +import { HeadSeo } from "@components/seo/head-seo"; export default function Shell(props) { const router = useRouter(); @@ -70,8 +71,18 @@ export default function Shell(props) { router.replace("/auth/login"); } + const pageTitle = typeof props.heading === "string" ? props.heading : props.title; + return session ? ( <> +
diff --git a/components/seo/head-seo.tsx b/components/seo/head-seo.tsx new file mode 100644 index 00000000..5155e704 --- /dev/null +++ b/components/seo/head-seo.tsx @@ -0,0 +1,101 @@ +import { NextSeo, NextSeoProps } from "next-seo"; +import React from "react"; +import { getBrowserInfo } from "@lib/core/browser/browser.utils"; +import { getSeoImage, seoConfig } from "@lib/config/next-seo.config"; +import merge from "lodash.merge"; + +export type HeadSeoProps = { + title: string; + description: string; + siteName?: string; + name?: string; + avatar?: string; + url?: string; + canonical?: string; + nextSeoProps?: NextSeoProps; +}; + +/** + * Build full seo tags from title, desc, canonical and url + */ +const buildSeoMeta = (pageProps: { + title: string; + description: string; + image: string; + siteName?: string; + url?: string; + canonical?: string; +}): NextSeoProps => { + const { title, description, image, canonical, siteName = seoConfig.headSeo.siteName } = pageProps; + return { + title: title, + canonical: canonical, + openGraph: { + site_name: siteName, + type: "website", + title: title, + description: description, + images: [ + { + url: image, + //width: 1077, + //height: 565, + //alt: "Alt image" + }, + ], + }, + additionalMetaTags: [ + { + property: "name", + content: title, + }, + { + property: "description", + content: description, + }, + { + name: "description", + content: description, + }, + { + property: "image", + content: image, + }, + ], + }; +}; + +const constructImage = (name: string, avatar: string, description: string): string => { + return ( + encodeURIComponent("Meet **" + name + "**
" + description).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(avatar) + ); +}; + +export const HeadSeo: React.FC = (props) => { + const defaultUrl = getBrowserInfo()?.url; + const image = getSeoImage("default"); + + const { + title, + description, + name = null, + avatar = null, + siteName, + canonical = defaultUrl, + nextSeoProps = {}, + } = props; + + const pageTitle = title + " | Calendso"; + let seoObject = buildSeoMeta({ title: pageTitle, image, description, canonical, siteName }); + + if (name && avatar) { + const pageImage = getSeoImage("ogImage") + constructImage(name, avatar, description); + seoObject = buildSeoMeta({ title: pageTitle, description, image: pageImage, canonical, siteName }); + } + + const seoProps: NextSeoProps = merge(nextSeoProps, seoObject); + + return ; +}; diff --git a/lib/config/next-seo.config.ts b/lib/config/next-seo.config.ts new file mode 100644 index 00000000..5689521a --- /dev/null +++ b/lib/config/next-seo.config.ts @@ -0,0 +1,27 @@ +import { DefaultSeoProps } from "next-seo"; +import { HeadSeoProps } from "@components/seo/head-seo"; + +const seoImages = { + default: "https://calendso.com/og-image.png", + ogImage: "https://og-image-one-pi.vercel.app/", +}; + +export const getSeoImage = (key: keyof typeof seoImages): string => { + return seoImages[key]; +}; + +export const seoConfig: { + headSeo: Required>; + defaultNextSeo: DefaultSeoProps; +} = { + headSeo: { + siteName: "Calendso", + }, + defaultNextSeo: { + twitter: { + handle: "@calendso", + site: "@Calendso", + cardType: "summary_large_image", + }, + }, +} as const; diff --git a/lib/core/browser/browser.utils.ts b/lib/core/browser/browser.utils.ts new file mode 100644 index 00000000..828c6ca1 --- /dev/null +++ b/lib/core/browser/browser.utils.ts @@ -0,0 +1,22 @@ +export const isBrowser = () => typeof window !== "undefined"; + +type BrowserInfo = { + url: string; + path: string; + referrer: string; + title: string; + query: string; +}; + +export const getBrowserInfo = (): Partial => { + if (!isBrowser()) { + return {}; + } + return { + url: window.document.location?.href ?? undefined, + path: window.document.location?.pathname ?? undefined, + referrer: window.document?.referrer ?? undefined, + title: window.document.title ?? undefined, + query: window.document.location?.search, + }; +}; diff --git a/package.json b/package.json index f6124e05..91a33e25 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "lodash.throttle": "^4.1.1", "next": "^10.2.3", "next-auth": "^3.28.0", + "next-seo": "^4.26.0", "next-transpile-modules": "^8.0.0", "nodemailer": "^6.6.3", "react": "17.0.2", diff --git a/pages/404.tsx b/pages/404.tsx index 2e339168..7de90082 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -3,7 +3,7 @@ import { BookOpenIcon, CheckIcon, CodeIcon, DocumentTextIcon } from "@heroicons/ import { useRouter } from "next/router"; import React from "react"; import Link from "next/link"; -import Head from "next/head"; +import { HeadSeo } from "@components/seo/head-seo"; const links = [ { @@ -32,9 +32,14 @@ export default function Custom404() { return ( <> - - 404: This page could not be found. - +
diff --git a/pages/[user].tsx b/pages/[user].tsx index f7a418d7..6773abba 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,5 +1,5 @@ import { GetServerSideProps } from "next"; -import Head from "next/head"; +import { HeadSeo } from "@components/seo/head-seo"; import Link from "next/link"; import prisma, { whereAndSelect } from "@lib/prisma"; import Avatar from "@components/Avatar"; @@ -48,59 +48,12 @@ export default function User(props): User { )); return ( <> - - {props.user.name || props.user.username} | Calendso - - - - - - - - - - ").replace( - /'/g, - "%27" - ) + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - - - - - - ").replace( - /'/g, - "%27" - ) + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - + {isReady && (
diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index e4988b18..b5b3df7a 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -6,16 +6,16 @@ import prisma from "@lib/prisma"; import * as Collapsible from "@radix-ui/react-collapsible"; import dayjs, { Dayjs } from "dayjs"; import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; -import Head from "next/head"; +import { HeadSeo } from "@components/seo/head-seo"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import Avatar from "@components/Avatar"; -import AvailableTimes from "../../components/booking/AvailableTimes"; -import DatePicker from "../../components/booking/DatePicker"; -import TimeOptions from "../../components/booking/TimeOptions"; -import PoweredByCalendso from "../../components/ui/PoweredByCalendso"; -import { timeZone } from "../../lib/clock"; -import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry"; +import AvailableTimes from "@components/booking/AvailableTimes"; +import DatePicker from "@components/booking/DatePicker"; +import TimeOptions from "@components/booking/TimeOptions"; +import PoweredByCalendso from "@components/ui/PoweredByCalendso"; +import { timeZone } from "@lib/clock"; +import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { asStringOrNull } from "@lib/asStringOrNull"; export default function Type(props: InferGetServerSidePropsType) { @@ -82,53 +82,14 @@ export default function Type(props: InferGetServerSidePropsType - - - {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} | - Calendso - - - - - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - - + {isReady && (
- - - {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} - {props.user.name || props.user.username} | Calendso - - - +
diff --git a/pages/_app.tsx b/pages/_app.tsx index a78372e6..beb8fda8 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,7 +1,8 @@ import "../styles/globals.css"; import AppProviders from "@lib/app-providers"; import type { AppProps as NextAppProps } from "next/app"; -import Head from "next/head"; +import { DefaultSeo } from "next-seo"; +import { seoConfig } from "@lib/config/next-seo.config"; // Workaround for https://github.com/zeit/next.js/issues/8592 export type AppProps = NextAppProps & { @@ -12,9 +13,7 @@ export type AppProps = NextAppProps & { function MyApp({ Component, pageProps, err }: AppProps) { return ( - - - + ); diff --git a/pages/auth/error.tsx b/pages/auth/error.tsx index cbd57cfe..2dc8028c 100644 --- a/pages/auth/error.tsx +++ b/pages/auth/error.tsx @@ -1,6 +1,6 @@ import { useRouter } from "next/router"; import { XIcon } from "@heroicons/react/outline"; -import Head from "next/head"; +import { HeadSeo } from "@components/seo/head-seo"; import Link from "next/link"; export default function Error() { @@ -13,10 +13,7 @@ export default function Error() { aria-labelledby="modal-title" role="dialog" aria-modal="true"> - - {error} - Calendso - - +