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:
@@ -44,7 +44,7 @@ const getData = cache(
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
revalidate: 900, // 15 minutes
|
||||
revalidate: 300, // 5 minutes
|
||||
tags: ["hits"],
|
||||
}
|
||||
);
|
||||
|
@@ -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: [
|
||||
{
|
||||
|
@@ -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>
|
||||
|
@@ -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 = () => {
|
||||
|
@@ -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: [
|
||||
{
|
||||
|
@@ -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}`,
|
||||
|
@@ -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: [
|
||||
{
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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({
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
@@ -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
144
app/projects/github.ts
Normal 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 [];
|
||||
}
|
||||
};
|
@@ -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 {
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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 = () => {
|
||||
|
Reference in New Issue
Block a user