1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-10-18 10:04:25 -04:00

attempt to fix metadata merging

This commit is contained in:
2025-04-22 22:17:32 -04:00
parent fe055116d5
commit 6c3cd0afe9
25 changed files with 411 additions and 413 deletions

View File

@@ -44,7 +44,7 @@ const getData = cache(
},
undefined,
{
revalidate: 900, // 15 minutes
revalidate: 300, // 5 minutes
tags: ["hits"],
}
);

View File

@@ -2,19 +2,17 @@ import { env } from "../../lib/env";
import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Video from "../../components/Video";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import mp4 from "./birthday.mp4";
import webm from "./birthday.webm";
import thumbnail from "./thumbnail.png";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
description: "The origin of my hatred for the Happy Birthday song.",
alternates: {
canonical: "/birthday",
},
canonical: "/birthday",
openGraph: {
videos: [
{

View File

@@ -1,12 +1,10 @@
import PageTitle from "../../components/PageTitle";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "CLI",
description: "AKA, the most useless Node module ever published, in history, by anyone, ever.",
alternates: {
canonical: "/cli",
},
canonical: "/cli",
});
<PageTitle canonical="/cli">CLI</PageTitle>

View File

@@ -1,16 +1,14 @@
import { LockIcon } from "lucide-react";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import ContactForm from "./form";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "Contact Me",
description: "Fill out this quick form and I'll get back to you as soon as I can.",
alternates: {
canonical: "/contact",
},
canonical: "/contact",
});
const Page = () => {

View File

@@ -3,7 +3,7 @@ import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import webm from "./convention.webm";
@@ -11,12 +11,10 @@ import mp4 from "./convention.mp4";
import subtitles from "./subs.en.vtt";
import thumbnail from "./thumbnail.png";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "My Brief Apperance in Hillary Clinton's DNC Video",
description: "My brief apperance in one of Hillary Clinton's 2016 DNC convention videos on substance abuse.",
alternates: {
canonical: "/hillary",
},
canonical: "/hillary",
openGraph: {
videos: [
{

View File

@@ -9,16 +9,15 @@ import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
import { defaultMetadata } from "../lib/helpers/metadata";
import * as config from "../lib/config";
import { MAX_WIDTH } from "../lib/config/constants";
import type { Metadata } from "next";
import type { Person, WebSite } from "schema-dts";
import { GeistMono, GeistSans } from "./fonts";
import "./global.css";
import "./globals.css";
import "./themes.css";
import styles from "./layout.module.css";
export const metadata: Metadata = defaultMetadata;
export const metadata = defaultMetadata;
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
@@ -35,7 +34,7 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
url: env.NEXT_PUBLIC_BASE_URL,
image: [`${env.NEXT_PUBLIC_BASE_URL}/opengraph-image.jpg`],
sameAs: [
env.NEXT_PUBLIC_BASE_URL!,
env.NEXT_PUBLIC_BASE_URL,
`https://${config.authorSocial?.mastodon}`,
`https://github.com/${config.authorSocial?.github}`,
`https://bsky.app/profile/${config.authorSocial?.bluesky}`,

View File

@@ -3,7 +3,7 @@ import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import mp4 from "./leo.mp4";
@@ -11,12 +11,10 @@ import webm from "./leo.webm";
import subtitles from "./subs.en.vtt";
import thumbnail from "./thumbnail.png";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: 'Facebook App on "The Lab with Leo Laporte"',
description: "Powncer app featured in Leo Laporte's TechTV show.",
alternates: {
canonical: "/leo",
},
canonical: "/leo",
openGraph: {
videos: [
{

View File

@@ -1,11 +1,10 @@
import PageTitle from "../../components/PageTitle";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "License",
alternates: {
canonical: "/license",
},
description: "This site's content is licensed under CC-BY-4.0.",
canonical: "/license",
});
<PageTitle canonical="/license">License</PageTitle>

View File

@@ -9,7 +9,7 @@ import Comments from "../../../components/Comments";
import Loading from "../../../components/Loading";
import HitCounter from "./counter";
import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import { addMetadata } from "../../../lib/helpers/metadata";
import { createMetadata } from "../../../lib/helpers/metadata";
import * as config from "../../../lib/config";
import { POSTS_DIR } from "../../../lib/config/constants";
import { size as ogImageSize } from "./opengraph-image";
@@ -37,9 +37,10 @@ export const generateMetadata = async ({ params }: { params: Promise<{ slug: str
const { slug } = await params;
const frontmatter = await getFrontMatter(slug);
return addMetadata({
return createMetadata({
title: frontmatter!.title,
description: frontmatter!.description,
canonical: `/${POSTS_DIR}/${slug}`,
openGraph: {
type: "article",
authors: [config.authorName],
@@ -50,9 +51,6 @@ export const generateMetadata = async ({ params }: { params: Promise<{ slug: str
twitter: {
card: "summary_large_image",
},
alternates: {
canonical: `/${POSTS_DIR}/${slug}`,
},
});
};
@@ -63,7 +61,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
const { default: MDXContent } = await import(`../../../${POSTS_DIR}/${slug}/index.mdx`);
return (
<>
<article>
<JsonLd<BlogPosting>
item={{
"@context": "https://schema.org",
@@ -155,7 +153,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
</Suspense>
</div>
)}
</>
</article>
);
};

View File

@@ -1,7 +1,7 @@
import Link from "../../components/Link";
import Time from "../../components/Time";
import { getFrontMatter } from "../../lib/helpers/posts";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import * as config from "../../lib/config";
import { POSTS_DIR } from "../../lib/config/constants";
import type { ReactElement } from "react";
@@ -9,12 +9,10 @@ import type { FrontMatter } from "../../lib/helpers/posts";
import styles from "./page.module.css";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "Notes",
description: `Recent posts by ${config.authorName}.`,
alternates: {
canonical: `/${POSTS_DIR}`,
},
canonical: `/${POSTS_DIR}`,
});
const Page = async () => {

View File

@@ -1,13 +1,11 @@
import { Comic_Neue as ComicNeueLoader } from "next/font/google";
import PageTitle from "../../components/PageTitle";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "Previously on...",
description: "An incredibly embarrassing and somewhat painful trip down this site's memory lane...",
alternates: {
canonical: "/previously",
},
canonical: "/previously",
});
export const ComicNeue = ComicNeueLoader({

View File

@@ -1,11 +1,10 @@
import PageTitle from "../../components/PageTitle";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "Privacy",
alternates: {
canonical: "/privacy",
},
description: "This website's extremely simple privacy policy.",
canonical: "/privacy",
});
<PageTitle canonical="/privacy">Privacy</PageTitle>

View File

@@ -1,33 +0,0 @@
.calendar {
--activity-0: #ebedf0;
--activity-1: #9be9a8;
--activity-2: #40c463;
--activity-3: #30a14e;
--activity-4: #216e39;
}
[data-theme="dark"] .calendar {
--activity-0: #252525;
--activity-1: #033a16;
--activity-2: #196c2e;
--activity-3: #2ea043;
--activity-4: #56d364;
}
.calendar :global(.react-activity-calendar) {
margin: 1em auto;
}
.calendar :global(.react-activity-calendar__count),
.calendar :global(.react-activity-calendar__legend-month) {
color: var(--colors-medium);
}
.calendar :global(.react-activity-calendar__legend-colors) {
color: var(--colors-medium-light);
}
.tooltip {
background-color: var(--colors-background-header);
color: var(--colors-text);
}

View File

@@ -3,22 +3,20 @@
import { cloneElement } from "react";
import { ActivityCalendar } from "react-activity-calendar";
import { Tooltip } from "react-tooltip";
import clsx from "clsx";
import { format } from "date-fns";
import type { ComponentPropsWithoutRef } from "react";
import type { Activity } from "react-activity-calendar";
import styles from "./calendar.module.css";
import "react-tooltip/dist/react-tooltip.css";
export type CalendarProps = ComponentPropsWithoutRef<"div"> & {
data: Activity[];
};
const Calendar = ({ data, className, ...rest }: CalendarProps) => {
const Calendar = ({ data, ...rest }: CalendarProps) => {
// heavily inspired by https://github.com/grubersjoe/react-github-calendar
return (
<div className={clsx(styles.calendar, className)} {...rest}>
<div {...rest}>
<ActivityCalendar
data={data}
colorScheme="dark"
@@ -45,7 +43,7 @@ const Calendar = ({ data, className, ...rest }: CalendarProps) => {
fontSize={13}
/>
<Tooltip id="activity-tooltip" className={styles.tooltip} />
<Tooltip id="activity-tooltip" />
</div>
);
};

144
app/projects/github.ts Normal file
View File

@@ -0,0 +1,144 @@
// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
import "server-only";
import { env } from "../../lib/env";
import { graphql } from "@octokit/graphql";
import * as cheerio from "cheerio";
import type { Repository, User } from "@octokit/graphql-schema";
export const getContributions = async (): Promise<
Array<{
date: string;
count: number;
level: number;
}>
> => {
// thanks @grubersjoe! :) https://github.com/grubersjoe/github-contributions-api/blob/main/src/scrape.ts
try {
const response = await fetch(`https://github.com/users/${env.NEXT_PUBLIC_GITHUB_USERNAME}/contributions`, {
headers: {
referer: `https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`,
"x-requested-with": "XMLHttpRequest",
},
cache: "force-cache",
next: {
revalidate: 3600, // 1 hour
tags: ["github-contributions"],
},
});
const $ = cheerio.load(await response.text());
const days = $(".js-calendar-graph-table .ContributionCalendar-day")
.get()
.sort((a, b) => {
const dateA = a.attribs["data-date"] ?? "";
const dateB = b.attribs["data-date"] ?? "";
return dateA.localeCompare(dateB, "en");
});
const dayTooltips = $(".js-calendar-graph tool-tip")
.toArray()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.reduce<Record<string, any>>((map, elem) => {
map[elem.attribs["for"]] = elem;
return map;
}, {});
return days.map((day) => {
const attr = {
id: day.attribs["id"],
date: day.attribs["data-date"],
level: day.attribs["data-level"],
};
let count = 0;
if (dayTooltips[attr.id]) {
const text = dayTooltips[attr.id].firstChild;
if (text) {
const countMatch = text.data.trim().match(/^\d+/);
if (countMatch) {
count = parseInt(countMatch[0]);
}
}
}
const level = parseInt(attr.level);
return {
date: attr.date,
count,
level,
};
});
} catch (error) {
console.error("[/projects] Failed to fetch contributions:", error);
return [];
}
};
export const getRepos = async (): Promise<Repository[] | undefined> => {
try {
// https://docs.github.com/en/graphql/reference/objects#repository
const { user } = await graphql<{ user: User }>(
`
query ($username: String!, $sort: RepositoryOrderField!, $limit: Int) {
user(login: $username) {
repositories(
first: $limit
isLocked: false
isFork: false
ownerAffiliations: OWNER
privacy: PUBLIC
orderBy: { field: $sort, direction: DESC }
) {
edges {
node {
name
url
description
pushedAt
stargazerCount
forkCount
primaryLanguage {
name
color
}
}
}
}
}
}
`,
{
username: env.NEXT_PUBLIC_GITHUB_USERNAME,
sort: "STARGAZERS",
limit: 12,
headers: {
accept: "application/vnd.github.v3+json",
authorization: `token ${env.GITHUB_TOKEN}`,
},
request: {
// override fetch() to use next's extension to cache the response
// https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options
fetch: (url: string | URL | Request, options?: RequestInit) => {
return fetch(url, {
...options,
cache: "force-cache",
next: {
revalidate: 3600, // 1 hour
tags: ["github-repos"],
},
});
},
},
}
);
return user.repositories.edges?.map((edge) => edge!.node as Repository);
} catch (error) {
console.error("[/projects] Failed to fetch repositories:", error);
return [];
}
};

View File

@@ -1,6 +1,35 @@
.heading {
font-weight: 400;
font-size: 1.4em;
font-weight: 400;
}
.calendar {
--activity-0: #ebedf0;
--activity-1: #9be9a8;
--activity-2: #40c463;
--activity-3: #30a14e;
--activity-4: #216e39;
}
[data-theme="dark"] .calendar {
--activity-0: #252525;
--activity-1: #033a16;
--activity-2: #196c2e;
--activity-3: #2ea043;
--activity-4: #56d364;
}
.calendar :global(.react-activity-calendar) {
margin: 1em auto 2em;
}
.calendar :global(.react-activity-calendar__count),
.calendar :global(.react-activity-calendar__legend-month) {
color: var(--colors-medium);
}
.calendar :global(.react-activity-calendar__legend-colors) {
color: var(--colors-medium-light);
}
.grid {

View File

@@ -1,176 +1,27 @@
import { env } from "../../lib/env";
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { graphql } from "@octokit/graphql";
import * as cheerio from "cheerio";
import { GitForkIcon, StarIcon } from "lucide-react";
import Calendar from "./calendar";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import { addMetadata } from "../../lib/helpers/metadata";
import * as config from "../../lib/config";
import type { Repository, User } from "@octokit/graphql-schema";
import { createMetadata } from "../../lib/helpers/metadata";
import { getContributions, getRepos } from "./github";
import styles from "./page.module.css";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "Projects",
description: `Most-starred repositories by @${config.authorSocial?.github} on GitHub`,
alternates: {
canonical: "/projects",
},
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
canonical: "/projects",
});
const getContributions = async (): Promise<
Array<{
date: string;
count: number;
level: number;
}>
> => {
// thanks @grubersjoe! :) https://github.com/grubersjoe/github-contributions-api/blob/main/src/scrape.ts
try {
const response = await fetch(`https://github.com/users/${config.authorSocial.github}/contributions`, {
headers: {
referer: `https://github.com/${config.authorSocial.github}`,
"x-requested-with": "XMLHttpRequest",
},
cache: "force-cache",
next: {
revalidate: 43200, // 12 hours
tags: ["github-contributions"],
},
});
const $ = cheerio.load(await response.text());
const days = $(".js-calendar-graph-table .ContributionCalendar-day")
.get()
.sort((a, b) => {
const dateA = a.attribs["data-date"] ?? "";
const dateB = b.attribs["data-date"] ?? "";
return dateA.localeCompare(dateB, "en");
});
const dayTooltips = $(".js-calendar-graph tool-tip")
.toArray()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.reduce<Record<string, any>>((map, elem) => {
map[elem.attribs["for"]] = elem;
return map;
}, {});
return days.map((day) => {
const attr = {
id: day.attribs["id"],
date: day.attribs["data-date"],
level: day.attribs["data-level"],
};
let count = 0;
if (dayTooltips[attr.id]) {
const text = dayTooltips[attr.id].firstChild;
if (text) {
const countMatch = text.data.trim().match(/^\d+/);
if (countMatch) {
count = parseInt(countMatch[0]);
}
}
}
const level = parseInt(attr.level);
return {
date: attr.date,
count,
level,
};
});
} catch (error) {
console.error("[/projects] Failed to fetch contributions:", error);
return [];
}
};
const getRepos = async (): Promise<Repository[] | undefined> => {
try {
// https://docs.github.com/en/graphql/reference/objects#repository
const { user } = await graphql<{ user: User }>(
`
query ($username: String!, $sort: RepositoryOrderField!, $limit: Int) {
user(login: $username) {
repositories(
first: $limit
isLocked: false
isFork: false
ownerAffiliations: OWNER
privacy: PUBLIC
orderBy: { field: $sort, direction: DESC }
) {
edges {
node {
name
url
description
pushedAt
stargazerCount
forkCount
primaryLanguage {
name
color
}
}
}
}
}
}
`,
{
username: config.authorSocial.github,
sort: "STARGAZERS",
limit: 12,
headers: {
accept: "application/vnd.github.v3+json",
authorization: `token ${env.GITHUB_TOKEN}`,
},
request: {
// override fetch() to use next's extension to cache the response
// https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options
fetch: (url: string | URL | Request, options?: RequestInit) => {
return fetch(url, {
...options,
cache: "force-cache",
next: {
revalidate: 1800, // 30 minutes
tags: ["github-repos"],
},
});
},
},
}
);
return user.repositories.edges?.map((edge) => edge!.node as Repository);
} catch (error) {
console.error("[/projects] Failed to fetch repositories:", error);
return [];
}
};
const Page = async () => {
// don't fail the entire site build if the required config for this page is missing, just return a 404 since this page
// would be blank anyways
// would be mostly blank anyways.
if (!env.GITHUB_TOKEN) {
console.warn("[/projects] I can't fetch anything from GitHub without 'GITHUB_TOKEN' set!");
notFound();
}
if (!config.authorSocial?.github) {
console.warn(
"[/projects] I can't fetch anything from GitHub without 'authorSocial.github' set in lib/config/index.ts."
);
console.error("[/projects] I can't fetch anything from GitHub without 'GITHUB_TOKEN' set!");
notFound();
}
@@ -182,18 +33,18 @@ const Page = async () => {
<PageTitle canonical="/projects">Projects</PageTitle>
<h2 className={styles.heading}>
<Link href={`https://github.com/${config.authorSocial.github}`} style={{ color: "inherit" }} plain>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain>
Contribution activity
</Link>
</h2>
<Suspense fallback={null}>
<Calendar data={contributions} style={{ marginBottom: "2em" }} />
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
<Calendar data={contributions} className={styles.calendar} />
</Suspense>
<h2 className={styles.heading}>
<Link
href={`https://github.com/${config.authorSocial.github}?tab=repositories&sort=stargazers`}
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
style={{ color: "inherit" }}
plain
>
@@ -276,7 +127,7 @@ const Page = async () => {
fontWeight: 500,
}}
>
<Link href={`https://github.com/${config.authorSocial.github}`}>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
View more on{" "}
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -4,36 +4,50 @@ import glob from "fast-glob";
import { getFrontMatter } from "../lib/helpers/posts";
import type { MetadataRoute } from "next";
// routes in /app (in other words, directories containing a page.tsx/mdx file) are automatically included; add a route
// here to exclude it.
const excludedRoutes = [
// homepage is already included manually
"./",
// other excluded pages
"./license",
"./privacy",
];
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
// start with manual routes
const routes: MetadataRoute.Sitemap = [
{
// homepage
url: `${env.NEXT_PUBLIC_BASE_URL}`,
url: env.NEXT_PUBLIC_BASE_URL,
priority: 1.0,
lastModified: new Date(),
},
];
// add each directory in the app folder as a route (excluding special routes)
(
await glob("**/page.{tsx,mdx}", {
const [staticRoutes, frontmatter] = await Promise.all([
// static routes in app directory
glob("**/page.{tsx,mdx}", {
cwd: path.join(process.cwd(), "app"),
ignore: [
// homepage is already included manually above
"./page.tsx",
...excludedRoutes.map((route) => `${route}/page.{tsx,mdx}`),
// don't include dynamic routes
"**/\\[*\\]/page.tsx",
"**/\\[*\\]/page.{tsx,mdx}",
],
})
).forEach((route) => {
}),
// blog posts
getFrontMatter(),
]);
// add each directory in the app folder as a route (excluding special routes)
staticRoutes.forEach((route) => {
routes.push({
// remove matching page.(tsx|mdx) file and make all URLs absolute
url: `${env.NEXT_PUBLIC_BASE_URL}/${route.replace(/\/page\.(tsx|mdx)$/, "")}`,
});
});
const frontmatter = await getFrontMatter();
frontmatter.forEach((post) => {
routes.push({
url: post.permalink,
@@ -45,7 +59,7 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
// sort alphabetically by URL, sometimes fast-glob returns results in a different order
routes.sort((a, b) => (a.url < b.url ? -1 : 1));
return [...routes];
return routes;
};
export default sitemap;

View File

@@ -1,12 +1,10 @@
import PageTitle from "../../components/PageTitle";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "/uses",
description: "Things I use daily.",
alternates: {
canonical: "/uses",
},
canonical: "/uses",
});
<PageTitle canonical="/uses">Uses</PageTitle>

View File

@@ -1,14 +1,12 @@
import Link from "../../components/Link";
import { addMetadata } from "../../lib/helpers/metadata";
import { createMetadata } from "../../lib/helpers/metadata";
import backgroundImg from "./sundar.jpg";
export const metadata = addMetadata({
export const metadata = createMetadata({
title: "fuckyougoogle.zip",
description: "This is a horrible idea.",
alternates: {
canonical: "/zip",
},
canonical: "/zip",
});
const Page = () => {