Skip to content

Commit 8c986fa

Browse files
committed
Add opengraph and other metatags
1 parent 5b5a98a commit 8c986fa

24 files changed

+353
-47
lines changed

example.env

+4
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
# Application urls
12
NEXT_PUBLIC_BACKEND_URL=https://backend.commitrocket.com
3+
NEXT_PUBLIC_FRONTEND_URL=https://www.commitrocket.com
4+
5+
# Google analytics
26
NEXT_PUBLIC_GOOGLE_ANALYTICS_TAG_ID= [[ SOME GOOGLE ANALYTICS TAG ID]]

next.config.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ const bundleAnalyzer = withBundleAnalyzer({
1010
/** @type {import('next').NextConfig} */
1111
const nextConfig = {
1212
reactStrictMode: true,
13-
compress: true,
14-
output: "export"
13+
compress: true
1514
};
1615

1716
export default withPlugins([bundleAnalyzer], nextConfig);

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "next dev",
7-
"build": "next build",
7+
"build": "next build && next export",
88
"analyze:build": "cross-env ANALYZE=true npm run build",
99
"analyze:dev": "cross-env ANALYZE=true npm run dev",
1010
"script:gen-articles-index": "node ./scripts/generateArticlesIndex.js"

src/assets/images/icons/discord.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { SVGProps, forwardRef, ForwardedRef } from "react";
1+
import { SVGProps, forwardRef, ForwardedRef } from "react";
22

33
const DiscordIcon = forwardRef((props: SVGProps<SVGSVGElement>, ref: ForwardedRef<SVGSVGElement>) => (
44
<svg

src/assets/state/articles/1.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default {
1010
thumbnail,
1111
thumbnailAlt: "A placeholder thumbnail",
1212

13+
vertical: "technology",
14+
1315
slug: "1",
1416
tags: [
1517
tags.news,

src/assets/state/articles/2.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export default {
1111
thumbnail,
1212
thumbnailAlt: "A placeholder thumbnail",
1313

14+
vertical: "technology",
15+
1416
slug: "2",
1517
tags: [
1618
tags.git

src/assets/state/articles/article.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ type IArticle = {
66
title: string;
77
thumbnail: StaticImageData,
88
thumbnailAlt: string;
9+
910
tags: string[];
11+
12+
/**
13+
* A high level overview of the article E.g. Technology
14+
*/
15+
vertical: string;
16+
1017
slug: string;
1118

19+
20+
1221
author: IMember;
1322

1423
teaser: string;

src/assets/state/contactMethods.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import { ReactNode, RefAttributes, SVGProps } from "react";
2+
13
import NewsIcon from "@heroicons/react/24/solid/NewspaperIcon";
24
import EnvelopeIcon from "@heroicons/react/24/solid/EnvelopeIcon";
35
import PencilSquareIcon from "@heroicons/react/24/solid/PencilSquareIcon";
46

57
import DiscordIcon from "@/assets/images/icons/discord";
68

79

8-
import { ReactNode } from "react";
910

1011
interface IContactMethod {
1112
title: ReactNode;
1213
href?: string;
13-
icon: React.ForwardRefExoticComponent<any>;
14+
icon: React.ForwardRefExoticComponent<any> | React.FC<SVGProps<SVGSVGElement> & RefAttributes<SVGSVGElement>>;
1415
iconAlt: string;
1516
}
1617

src/assets/state/team.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { StaticImageData } from "next/image";
22
import RikPicture from "@/assets/images/people/rik.webp";
33

44
export interface IMember {
5-
name: string;
5+
fullName: string;
6+
firstName?: string;
7+
lastName?: string;
8+
gender?: string; // Open Graph only accepts "male" & "female", but put in here whatever you want
69
title: string;
710
image: StaticImageData;
811
links: {
@@ -14,7 +17,7 @@ export interface IMember {
1417
export const people = {
1518
"rik": {
1619
image: RikPicture,
17-
name: "Rik den Breejen",
20+
fullName: "Rik den Breejen",
1821
title: "Lead Developer & Founder",
1922
links: [
2023
{

src/components/head/KeywordsMeta.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface KeywordTagsProps {
2+
tags: readonly string[];
3+
}
4+
5+
const KeywordsMeta = ({ tags }: KeywordTagsProps) => (
6+
<meta
7+
name="keywords"
8+
content={tags.map((tag) => tag.replace(",", "")).join(", ")}
9+
/>
10+
);
11+
12+
export default KeywordsMeta;

src/components/head/OgArticleMeta.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import OgProfileMeta, { OgProfile } from "./OgProfileMeta";
3+
import toOpenGraph from "./utils/toOpenGraph";
4+
5+
export interface OgArticle {
6+
publishedTime: Date;
7+
modifiedTime?: Date;
8+
expirationTime?: Date;
9+
section?: string;
10+
tag?: string | string[];
11+
author?: OgProfile | OgProfile[];
12+
}
13+
14+
export interface OgArticleMetaProps extends OgArticle {
15+
}
16+
17+
const OgArticleMeta = ({ author, ...props }: OgArticleMetaProps) => {
18+
19+
const computedAuthors = !author ? [] : Array.isArray(author) ? author : [author];
20+
21+
return (
22+
<>
23+
{toOpenGraph({ props, prefix: "article" })}
24+
{computedAuthors.map((author, i) => <OgProfileMeta key={i} {...author} />)}
25+
</>
26+
);
27+
};
28+
29+
export default OgArticleMeta;

src/components/head/OgImageMeta.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
import toOpenGraph from "./utils/toOpenGraph";
3+
4+
export interface OgImage {
5+
url?: string;
6+
secureUrl?: string;
7+
path?: string;
8+
securePath?: string;
9+
type?: string;
10+
width?: number;
11+
height?: number;
12+
alt?: string;
13+
}
14+
15+
export interface OgImageMetaProps extends OgImage {
16+
17+
}
18+
19+
const OgImageMeta = ({ url, path, securePath, secureUrl, ...props }: OgImageMetaProps) => {
20+
const computedUrl = path ? `${process.env.NEXT_PUBLIC_FRONTEND_URL}${path}` : url;
21+
const computedSecureUrl = computedUrl?.startsWith("https") ? computedUrl : securePath ? `${process.env.NEXT_PUBLIC_FRONTEND_URL}${securePath}` : secureUrl;
22+
23+
return (
24+
<>
25+
{toOpenGraph({ prefix: "og:image", props })}
26+
{toOpenGraph({
27+
prefix: "og:image",
28+
props: {
29+
url: computedUrl,
30+
secureUrl: computedSecureUrl
31+
}
32+
})}
33+
</>
34+
);
35+
};
36+
37+
export default OgImageMeta;

src/components/head/OgMeta.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useRouter } from "next/router";
2+
import toOpenGraph from "./utils/toOpenGraph";
3+
4+
5+
export interface OgBase {
6+
title: string;
7+
siteName?: string;
8+
description?: string;
9+
url: string;
10+
path?: string;
11+
type: "article" | "profile" | "website";
12+
image?: string;
13+
14+
locale?: string;
15+
localeAlternate?: string | string[];
16+
17+
audio?: string;
18+
video?: string;
19+
}
20+
21+
export interface OgMetaProps extends Partial<OgBase> {
22+
23+
}
24+
25+
const DEFAULT_OG = {
26+
title: "Commit Rocket",
27+
description: "Commit Rocket, the next-gen git client",
28+
url: `${process.env.NEXT_PUBLIC_FRONTEND_URL}`,
29+
type: "website",
30+
locale: "en_US"
31+
} as OgBase;
32+
33+
const OgMeta = ({ localeAlternate, path, url, image, ...props }: OgMetaProps = DEFAULT_OG) => {
34+
const computedUrl = path ? `${process.env.NEXT_PUBLIC_FRONTEND_URL}${path}` : url;
35+
const computedImage = !image ? undefined : image.startsWith("http") ? image : `${process.env.NEXT_PUBLIC_FRONTEND_URL}${image}`;
36+
37+
return (
38+
<>
39+
{props.title && <title>{props.title}</title>}
40+
{props.description && <meta name="description" content={props.description} />}
41+
{toOpenGraph({ props, prefix: "og" })}
42+
{toOpenGraph({ props: { url: computedUrl, image: computedImage }, prefix: "og" })}
43+
{toOpenGraph({ props: { localeAlternate }, prefix: "og:locale" })}
44+
</>
45+
);
46+
};
47+
48+
export default OgMeta;

src/components/head/OgProfileMeta.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export interface OgProfile {
2+
userName: string;
3+
firstName?: string;
4+
lastName?: string;
5+
gender?: string;
6+
}
7+
8+
export interface OgProfileMetaProps extends OgProfile {
9+
10+
}
11+
12+
const OgProfileMeta = ({ userName, firstName, lastName, gender }: OgProfileMetaProps) => {
13+
return (
14+
<>
15+
<meta property="profile:username" content={userName} />
16+
{firstName && <meta property="profile:first_name" content={firstName} />}
17+
{lastName && <meta property="profile:last_name" content={lastName} />}
18+
{gender && <meta property="profile:gender" content={gender} />}
19+
</>
20+
);
21+
};
22+
23+
export default OgProfileMeta;
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
3+
interface Options {
4+
props: Record<string, string | string[] | Date | number | undefined>;
5+
prefix: "og" | "profile" | "article" | "book" | "og:image" | "og:audio" | "og:video" | "og:locale";
6+
}
7+
8+
const toSnakeCase = (key: string) => {
9+
return key.replace(/[A-Z]/g, (upper) => "_" + upper.toLowerCase());
10+
};
11+
12+
const toOpenGraphValue = (val: string | number | Date) => {
13+
if (typeof val === "string" || typeof val === "number") return String(val);
14+
if (val instanceof Date) return val.toISOString();
15+
};
16+
17+
const toOpenGraph = ({ props, prefix }: Options) => {
18+
return Object.keys(props).map((key, i) => {
19+
const givenValue = props[key];
20+
if (!givenValue) return null;
21+
const values = Array.isArray(givenValue) ? givenValue : [givenValue];
22+
const propertyName = toSnakeCase(key);
23+
24+
return values.map((value, j) => {
25+
return <meta
26+
key={`${i}${j}${key}${prefix}`}
27+
property={`${prefix}:${propertyName}`}
28+
content={toOpenGraphValue(value)}
29+
/>;
30+
});
31+
}).flatMap((val) => val);
32+
33+
};
34+
35+
export default toOpenGraph;

src/components/pages/about/Member.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IMember } from "@/assets/state/team";
22
import LinkButton from "@/components/controls/LinkButton";
33

4-
const Member = ({ image, name, title, links }: IMember) => (
4+
const Member = ({ image, fullName, title, links }: IMember) => (
55
<li
66
className="flex flex-col items-center w-full max-w-full gap-2 p-4 border-2 rounded-md border-primary-light motion-safe:transition-all sm:p-6 md:w-fit"
77
aria-label="Member"
@@ -16,7 +16,7 @@ const Member = ({ image, name, title, links }: IMember) => (
1616
height={image.height}
1717
/>
1818
<div className="flex flex-col max-w-full gap-2 py-4 text-center w-72">
19-
<p className="text-2xl font-semibold text-secondary" aria-label="Name">{name}</p>
19+
<p className="text-2xl font-semibold text-secondary" aria-label="Name">{fullName}</p>
2020
<p className="font-semibold text-secondary" aria-label="Title / Role">{title}</p>
2121
</div>
2222
</div>

src/components/pages/blog/ArticleBrief.tsx

+3-12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import Link from "@/components/navigation/Link";
99
import useSSGSafe from "@/hooks/useSSGSafe";
1010

1111
import IArticle from "@/assets/state/articles/article";
12+
import makeTagUrl from "./utils/makeTagUrl";
13+
import { readtimeFormatter } from "@/utils/readtime";
1214

1315
const listAnim = {
1416
in: {
@@ -41,17 +43,6 @@ export interface ArticleBriefProps extends IArticleBrief {
4143

4244
}
4345

44-
const readtimeFormatter = Intl.NumberFormat(undefined, {
45-
style: "unit",
46-
unit: "minute"
47-
});
48-
49-
const makeTagUrl = (tag: string) => {
50-
const url = new URL("/blog", "https://example.com");
51-
url.searchParams.set("tag", tag);
52-
return url.href.replace(url.origin, "");
53-
};
54-
5546
const ArticleBrief = ({ title, thumbnail, thumbnailAlt, readtime, teaser, author, date, url, tags }: ArticleBriefProps) => {
5647
const safeToRender = useSSGSafe();
5748

@@ -113,7 +104,7 @@ const ArticleBrief = ({ title, thumbnail, thumbnailAlt, readtime, teaser, author
113104
/>
114105
</div>
115106
<div className="flex flex-col">
116-
<div>{author.name}</div>
107+
<div>{author.fullName}</div>
117108
<div className="text-sm">{author.title}</div>
118109
</div>
119110
</AuthorTag>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const makeTagUrl = (tag: string) => {
2+
const url = new URL("/blog", "https://example.com");
3+
url.searchParams.set("tag", tag);
4+
return url.href.replace(url.origin, "");
5+
};
6+
7+
export default makeTagUrl;

src/pages/_app.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,10 @@ export default function App({ Component, pageProps, router }: AppProps) {
5656
});
5757
}, 1);
5858
}, []);
59+
console.log(router);
5960

6061
return (
6162
<MotionConfig reducedMotion="user">
62-
<Head>
63-
<title>Commit Rocket</title>
64-
</Head>
6563
<GoogleAnalytics />
6664
<div className="flex flex-col font-sans">
6765
<Header />
@@ -76,7 +74,7 @@ export default function App({ Component, pageProps, router }: AppProps) {
7674
variants={reduceMotion ? {} : pageAnimation}
7775
>
7876
<Component
79-
className=""
77+
pathname={new URL(router.asPath, "http://example.com").pathname}
8078
initialLoad={initialLoad}
8179
reduceMotion={reduceMotion}
8280
{...pageProps}

0 commit comments

Comments
 (0)