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

attempt to fix metadata merging

This commit is contained in:
Jake Jarvis 2025-04-22 22:17:32 -04:00
parent fe055116d5
commit 6c3cd0afe9
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
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 = () => {

View File

@ -19,6 +19,7 @@ export const env = createEnv({
* @see https://vercel.com/marketplace/upstash
*/
KV_REST_API_TOKEN: v.string(),
/**
* Required. Redis storage credentials for hit counter's [server component](../app/notes/[slug]/counter.tsx) and API
* endpoint. Currently set automatically by Vercel's Upstash integration.
@ -36,6 +37,7 @@ export const env = createEnv({
* @see https://vercel.com/integrations/resend
*/
RESEND_API_KEY: v.pipe(v.string(), v.startsWith("re_")),
/**
* Optional, but will throw a warning if unset. Use an approved domain (or subdomain) on the Resend account to send
* submissions from. Sender's real email is passed via a Reply-To header, so setting this makes zero difference to
@ -43,10 +45,9 @@ export const env = createEnv({
*
* @see https://resend.com/domains
*/
RESEND_FROM_EMAIL: v.optional(v.pipe(v.string(), v.email()), "onboarding@resend.dev"),
/**
* Required. The destination email for contact form submissions.
*/
RESEND_FROM_EMAIL: v.fallback(v.pipe(v.string(), v.email()), "onboarding@resend.dev"),
/** Required. The destination email for contact form submissions. */
RESEND_TO_EMAIL: v.pipe(v.string(), v.email()),
/**
@ -58,11 +59,38 @@ export const env = createEnv({
},
client: {
/**
* Optional. Overrides the most appropriate default URL for the current deployment.
* Optional. We try to make an educated guess for the most appropriate URL based on the current deployment's
* environment and platform (if Vercel or Netlify), but you can override it by simply setting this manually. Must be
* a fully-qualified URL if set beginning with `http(s)://`, and should not end with a trailing slash.
*
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata#default-value
*/
NEXT_PUBLIC_BASE_URL: v.optional(v.pipe(v.string(), v.url())),
NEXT_PUBLIC_BASE_URL: v.fallback(
v.pipe(v.string(), v.url()),
() =>
// Vercel: https://vercel.com/docs/environment-variables/system-environment-variables
(process.env.VERCEL
? process.env.VERCEL_ENV === "production"
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: process.env.VERCEL_ENV === "preview"
? `https://${process.env.VERCEL_BRANCH_URL}`
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: undefined
: undefined) ||
// Netlify: https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables
(process.env.NETLIFY
? process.env.CONTEXT === "production"
? `${process.env.URL}`
: process.env.DEPLOY_PRIME_URL
? `${process.env.DEPLOY_PRIME_URL}`
: process.env.DEPLOY_URL
? `${process.env.DEPLOY_URL}`
: undefined
: undefined) ||
// next dev
`http://localhost:${process.env.PORT || 3000}`
),
/**
* Optional. Enables comments on blog posts via GitHub discussions.
@ -70,6 +98,7 @@ export const env = createEnv({
* @see https://giscus.app/
*/
NEXT_PUBLIC_GISCUS_CATEGORY_ID: v.optional(v.string()),
/**
* Optional. Enables comments on blog posts via GitHub discussions.
*
@ -77,10 +106,11 @@ export const env = createEnv({
*/
NEXT_PUBLIC_GISCUS_REPO_ID: v.optional(v.string()),
/**
* Required. GitHub repository for the site in the format of `{username}/{repo}`.
*/
NEXT_PUBLIC_GITHUB_REPO: v.string(),
/** Required. GitHub repository for the site in the format of `{username}/{repo}`. */
NEXT_PUBLIC_GITHUB_REPO: v.pipe(v.string(), v.includes("/")),
/** Required. GitHub username of the author, used to generate [/projects](../app/projects/page.tsx). */
NEXT_PUBLIC_GITHUB_USERNAME: v.string(),
/**
* Optional. Sets an `Onion-Location` header in responses to advertise a URL for the same page but hosted on a
@ -115,33 +145,11 @@ export const env = createEnv({
NEXT_PUBLIC_TURNSTILE_SITE_KEY: v.optional(v.string(), "1x00000000000000000000AA"),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_BASE_URL:
process.env.NEXT_PUBLIC_BASE_URL ||
// Vercel: https://vercel.com/docs/environment-variables/system-environment-variables
(process.env.VERCEL
? process.env.VERCEL_ENV === "production"
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: process.env.VERCEL_ENV === "preview"
? `https://${process.env.VERCEL_BRANCH_URL}`
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: undefined
: undefined) ||
// Netlify: https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables
(process.env.NETLIFY
? process.env.CONTEXT === "production"
? `${process.env.URL}`
: process.env.DEPLOY_PRIME_URL
? `${process.env.DEPLOY_PRIME_URL}`
: process.env.DEPLOY_URL
? `${process.env.DEPLOY_URL}`
: undefined
: undefined) ||
// next dev
`http://localhost:${process.env.PORT || 3000}`,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
NEXT_PUBLIC_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_GISCUS_REPO_ID,
NEXT_PUBLIC_GITHUB_REPO: process.env.NEXT_PUBLIC_GITHUB_REPO,
NEXT_PUBLIC_GITHUB_USERNAME: process.env.NEXT_PUBLIC_GITHUB_USERNAME,
NEXT_PUBLIC_ONION_DOMAIN: process.env.NEXT_PUBLIC_ONION_DOMAIN,
NEXT_PUBLIC_SITE_LOCALE: process.env.NEXT_PUBLIC_SITE_LOCALE,
NEXT_PUBLIC_SITE_TZ: process.env.NEXT_PUBLIC_SITE_TZ,

View File

@ -3,7 +3,7 @@ import * as config from "../config";
import type { Metadata } from "next";
export const defaultMetadata: Metadata = {
metadataBase: env.NEXT_PUBLIC_BASE_URL ? new URL(env.NEXT_PUBLIC_BASE_URL) : undefined,
metadataBase: new URL(env.NEXT_PUBLIC_BASE_URL),
title: {
template: `%s ${config.siteName}`,
default: `${config.siteName} ${config.tagline}`,
@ -48,15 +48,15 @@ export const defaultMetadata: Metadata = {
* Helper function to deep merge a page's metadata into the default site metadata
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
*/
export const addMetadata = (metadata: Metadata): Metadata => {
export const createMetadata = (metadata: Metadata & { canonical: string }): Metadata => {
return {
...defaultMetadata,
...metadata,
openGraph: {
...defaultMetadata.openGraph,
title: metadata.title as string,
description: metadata.description as string,
url: metadata.alternates?.canonical as string,
title: metadata.title!,
description: metadata.description!,
url: metadata.canonical,
...metadata.openGraph,
},
twitter: {
@ -65,6 +65,7 @@ export const addMetadata = (metadata: Metadata): Metadata => {
},
alternates: {
...defaultMetadata.alternates,
canonical: metadata.canonical,
...metadata.alternates,
},
other: {

View File

@ -24,8 +24,8 @@
"@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "15.4.0-canary.3",
"@next/mdx": "15.4.0-canary.3",
"@next/bundle-analyzer": "15.4.0-canary.4",
"@next/mdx": "15.4.0-canary.4",
"@octokit/graphql": "^8.2.2",
"@octokit/graphql-schema": "^15.26.0",
"@t3-oss/env-nextjs": "^0.12.0",
@ -39,8 +39,8 @@
"feed": "^4.2.2",
"geist": "^1.3.1",
"html-entities": "^2.6.0",
"lucide-react": "0.501.0",
"next": "15.4.0-canary.3",
"lucide-react": "0.503.0",
"next": "15.4.0-canary.4",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"react": "19.1.0",
@ -66,7 +66,8 @@
"remark-mdx-frontmatter": "^5.1.0",
"remark-parse": "^11.0.0",
"remark-smartypants": "^3.0.2",
"resend": "^4.3.0",
"resend": "^4.4.0",
"server-only": "0.0.1",
"shiki": "^3.3.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
@ -82,10 +83,10 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-is": "^19.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411",
"babel-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417",
"cross-env": "^7.0.3",
"eslint": "^9.25.1",
"eslint-config-next": "15.4.0-canary.3",
"eslint-config-next": "15.4.0-canary.4",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.31.0",
@ -93,7 +94,7 @@
"eslint-plugin-mdx": "^3.4.1",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411",
"eslint-plugin-react-compiler": "19.0.0-beta-af1b7da-20250417",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
@ -111,7 +112,7 @@
"engines": {
"node": ">=20.x"
},
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677",
"packageManager": "pnpm@10.9.0+sha512.0486e394640d3c1fb3c9d43d49cf92879ff74f8516959c235308f5a8f62e2e19528a65cdc2a3058f587cde71eba3d5b56327c8c33a97e4c4051ca48a10ca2d5f",
"cacheDirectories": [
"node_modules",
".next/cache"

182
pnpm-lock.yaml generated
View File

@ -27,11 +27,11 @@ importers:
specifier: ^3.1.0
version: 3.1.0(@types/react@19.1.2)(react@19.1.0)
'@next/bundle-analyzer':
specifier: 15.4.0-canary.3
version: 15.4.0-canary.3
specifier: 15.4.0-canary.4
version: 15.4.0-canary.4
'@next/mdx':
specifier: 15.4.0-canary.3
version: 15.4.0-canary.3(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))
specifier: 15.4.0-canary.4
version: 15.4.0-canary.4(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))
'@octokit/graphql':
specifier: ^8.2.2
version: 8.2.2
@ -46,7 +46,7 @@ importers:
version: 1.34.8
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
version: 1.5.0(next@15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
cheerio:
specifier: ^1.0.0
version: 1.0.0
@ -67,16 +67,16 @@ importers:
version: 4.2.2
geist:
specifier: ^1.3.1
version: 1.3.1(next@15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
version: 1.3.1(next@15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
html-entities:
specifier: ^2.6.0
version: 2.6.0
lucide-react:
specifier: 0.501.0
version: 0.501.0(react@19.1.0)
specifier: 0.503.0
version: 0.503.0(react@19.1.0)
next:
specifier: 15.4.0-canary.3
version: 15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: 15.4.0-canary.4
version: 15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
polished:
specifier: ^4.3.1
version: 4.3.1
@ -153,8 +153,11 @@ importers:
specifier: ^3.0.2
version: 3.0.2
resend:
specifier: ^4.3.0
version: 4.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^4.4.0
version: 4.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
server-only:
specifier: 0.0.1
version: 0.0.1
shiki:
specifier: ^3.3.0
version: 3.3.0
@ -196,8 +199,8 @@ importers:
specifier: ^19.0.0
version: 19.0.0
babel-plugin-react-compiler:
specifier: 19.0.0-beta-ebf51a3-20250411
version: 19.0.0-beta-ebf51a3-20250411
specifier: 19.0.0-beta-af1b7da-20250417
version: 19.0.0-beta-af1b7da-20250417
cross-env:
specifier: ^7.0.3
version: 7.0.3
@ -205,8 +208,8 @@ importers:
specifier: ^9.25.1
version: 9.25.1
eslint-config-next:
specifier: 15.4.0-canary.3
version: 15.4.0-canary.3(eslint@9.25.1)(typescript@5.8.3)
specifier: 15.4.0-canary.4
version: 15.4.0-canary.4(eslint@9.25.1)(typescript@5.8.3)
eslint-config-prettier:
specifier: ^10.1.2
version: 10.1.2(eslint@9.25.1)
@ -229,8 +232,8 @@ importers:
specifier: ^7.37.5
version: 7.37.5(eslint@9.25.1)
eslint-plugin-react-compiler:
specifier: 19.0.0-beta-ebf51a3-20250411
version: 19.0.0-beta-ebf51a3-20250411(eslint@9.25.1)
specifier: 19.0.0-beta-af1b7da-20250417
version: 19.0.0-beta-af1b7da-20250417(eslint@9.25.1)
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.25.1)
@ -665,17 +668,17 @@ packages:
'@napi-rs/wasm-runtime@0.2.9':
resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==}
'@next/bundle-analyzer@15.4.0-canary.3':
resolution: {integrity: sha512-O04r/vDajPOay6h7OtAuGR7hyW5PiQe1daN6nRfXMuvwJpsDsd45m5EbhnLFLiV7O8h6YTksK8iVCP3yz/DssQ==}
'@next/bundle-analyzer@15.4.0-canary.4':
resolution: {integrity: sha512-9wLOrH/gMKUit6MBqrzCTtJbXLPXOcLOAH0Jp2vMbx4rnOW+1GBtqvdeZk9NVEVmQ9BVKySrDtkCIsxGWoJoNw==}
'@next/env@15.4.0-canary.3':
resolution: {integrity: sha512-lu4pB2e3Z/d+B0rxEm9YuQMb57Hd96iJUBZgVlcRNemlIryr0GByu17kvN6nBk3JjbWL8h+MW90stpGzGdhbqg==}
'@next/env@15.4.0-canary.4':
resolution: {integrity: sha512-bAQK2bwWe5qek16e9cu0r0wlmK8K07nFTPZuBVqyuHAAPYLq44mNG8L0KZ/wV5el6u9ctsZpbfF/U7gC5jGN4Q==}
'@next/eslint-plugin-next@15.4.0-canary.3':
resolution: {integrity: sha512-O2aNDzw1OQUf1FEuOCH/T8l1sCemNDdmQGhzkrZPqqUfX4rNFZLEddoMC9cqTT8Y8PaLsqMi3po1ehAENyILlA==}
'@next/eslint-plugin-next@15.4.0-canary.4':
resolution: {integrity: sha512-cnFwPJTk2OED7ZGtSG70hoGqtRVPgGvnQ86FYghKOA9JLtEWyDwBJWwba+7R6B12lfT+aDtLNfWhhQWNCzDiRw==}
'@next/mdx@15.4.0-canary.3':
resolution: {integrity: sha512-HOOlt+PV86X3ONSFF1JySTqfODfYUGV+TwhehkHXniDmfD8YWZIjNOE2qRkxA0vN2F32VjK21rOohR8UAGrURA==}
'@next/mdx@15.4.0-canary.4':
resolution: {integrity: sha512-Alr0TgvJNRxa3lXU5hGXjqrfBllvf3g7gJhozZrlA3CyC+vqgSVt1pBzppUmedjmnvZJ2m5olp3fxQuCds9pWA==}
peerDependencies:
'@mdx-js/loader': '>=0.15.0'
'@mdx-js/react': '>=0.15.0'
@ -685,50 +688,50 @@ packages:
'@mdx-js/react':
optional: true
'@next/swc-darwin-arm64@15.4.0-canary.3':
resolution: {integrity: sha512-w9u8IpwLb/JS7HzHLt24smP4FxIYMgciOtYNUCognO1xh1XZfqqjDIrRAXDuuYDPKrc1i2EvI24R5eDTz7EYMQ==}
'@next/swc-darwin-arm64@15.4.0-canary.4':
resolution: {integrity: sha512-98K8VoRkzmg+a68SA8bJlaEKUdv1T+k5+NxTOn0OHUvVFAK3IB44v7GGfsrHX/Sauh+C8AOa4eD3RM/BrqZvAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.4.0-canary.3':
resolution: {integrity: sha512-5pL1hBRw8h1XeArzWYjCDERtRFIfrMAz1Nq9m1np8FrTuHclE7xitKKfOJqqmBbO9dWtnZIfA8lZl9bdlNEUZg==}
'@next/swc-darwin-x64@15.4.0-canary.4':
resolution: {integrity: sha512-LdrgqdqIZR2PtQkxb4w+DW4DSgHsj5XYQt/MmepYu5IJ313a2FC7RLP21rIotLbloprnunrIW8t+S1EXM85Bgw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.4.0-canary.3':
resolution: {integrity: sha512-vx6cU4jKoecF2QZw3CQqJrzb+D0WhNzHHoWUN8O+YKPnX0oG4wEtAQWSWisxKjNrU1U4TiraOql0nOQBUOKwaQ==}
'@next/swc-linux-arm64-gnu@15.4.0-canary.4':
resolution: {integrity: sha512-ILQIt/nz8XeoVugHYuxDAiz+YqOkzUfYaqBG+kZGIuVtndQTCKAvUu+ZPIqav+4lsZNw9zLlw9w0UgryraL/oA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.4.0-canary.3':
resolution: {integrity: sha512-7ig1sQHRRgTrj4QHt5l8OT1z2SJnEAHbnEY9SDP2HilwQIfgOAOxveFDBR+f/8AMdAKhCTSeMyrZsivpC0xTUA==}
'@next/swc-linux-arm64-musl@15.4.0-canary.4':
resolution: {integrity: sha512-Obp+gG8ZnC4syrvn58ZdeT0ZXStTfgS/spLLQprhgCcblPLyxKDZm6i5T5eDhDHTFOdC+ThqVxVSaw7kU20uyA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.4.0-canary.3':
resolution: {integrity: sha512-fML6pzNX9i3DlrCOdE6A1TbVL0aIQkIDDCjrbn/f37hOn88god1OrVd/d4J4w1YqLKQWpmJPnUn6Bkn8qXqbRw==}
'@next/swc-linux-x64-gnu@15.4.0-canary.4':
resolution: {integrity: sha512-AsTTKgxXEJ3EwS3SSZhqUmJf4Hb5trazeJ+Yy9VO+nPsZWU6mdnRbIloOUV+7Gc/Hn3FOJQuZvAYCCApzVyeRA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.4.0-canary.3':
resolution: {integrity: sha512-87/JPkbr3fgvASdWW2qBVuaXwcjSxgy+CTllj2DgYB7e7BEzT7QJEdj0HJZljBjVbN5oT1FOKwhaVRgRWuwYLQ==}
'@next/swc-linux-x64-musl@15.4.0-canary.4':
resolution: {integrity: sha512-75KSJDWe3irbsja2GZjx9euV+SWfbXL7G/o8vdE3mU3iG1bWNIPeJDJlnDSYeqMb0NrqYT1VfqOy0TCQOrWa5w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.4.0-canary.3':
resolution: {integrity: sha512-cTZh72h3ZX8z0lhdVs5m38uyy83mW5r0jz6hKagysPT06uTdOAypK6CRqG5CJSN7RM0n7CkfcO6ExjDqhkDhRA==}
'@next/swc-win32-arm64-msvc@15.4.0-canary.4':
resolution: {integrity: sha512-k1HDU4ipztIiHM0iHqxHz4sVtiHumAVCYfMhOOYdB3WcuSLZfNUnSRYzjZ0oT7+O/7a/sXsM9PIRV47OaRMuCA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.4.0-canary.3':
resolution: {integrity: sha512-8oZKOKRGad4EVZ94L5Sz2EP59khHIeKGKg+/z8r5mCbBtupLPTXmWjrXoi1R55hHRXJjbW2D5NwcPfJn/ltZ3Q==}
'@next/swc-win32-x64-msvc@15.4.0-canary.4':
resolution: {integrity: sha512-1C+r+7C+hrLNuN6KCx9lALkq4d9w6iw75V4bJGyIMj/ZhNoRJzAxALHP99peG5Qe6othTPC/ev3oCcnq2FZwsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -1227,8 +1230,8 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411:
resolution: {integrity: sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g==}
babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417:
resolution: {integrity: sha512-UyTCRmzpxa4H1EqJk8fWeUOzHdEA12NQZ5DrF5hyhCs+Y6f7B4pg1fkul49sRn9GPPGFgkrH4IxOtnQJ7tNXIA==}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@ -1626,8 +1629,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-next@15.4.0-canary.3:
resolution: {integrity: sha512-hgxAzqZuG4uKeHv24uW2ZwkTlD+B3kJKzVaY4Fq3j8ewEueO7Pncl8RQjCqgLIHQoxxvclHZgsrwn9Tv3irv2g==}
eslint-config-next@15.4.0-canary.4:
resolution: {integrity: sha512-H0+7XKwmESYF+SCBEINypc7/j+F0lOxkj6ZridB7MRFk90P98KRmz3dmTn/TLbHm3P9rat83ay95W76MXMB24g==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@ -1730,8 +1733,8 @@ packages:
eslint-config-prettier:
optional: true
eslint-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411:
resolution: {integrity: sha512-R7ncuwbCPFAoeMlS56DGGSJFxmRtlWafYH/iWyep5Ks0RaPqTCL4k5gA87axUBBcITsaIgUGkbqAxDxl8Xfm5A==}
eslint-plugin-react-compiler@19.0.0-beta-af1b7da-20250417:
resolution: {integrity: sha512-m3eVzHqtXFtu6rViWx/kBhv9Jcpj+Q4pkfRWyF47TIBa+Jo8DAB90OoeDh0JR896rtG1OXogLKKd+b6lzFup0A==}
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
peerDependencies:
eslint: '>=7'
@ -2484,8 +2487,8 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.501.0:
resolution: {integrity: sha512-E2KoyhW59fCb/yUbR3rbDer83fqn7a8NG91ZhIot2yWaPHjPyGzzsNKh40N//GezYShAuycf3TcQksRQznIsRw==}
lucide-react@0.503.0:
resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
@ -2724,8 +2727,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.4.0-canary.3:
resolution: {integrity: sha512-OkwxAFNQeuE0vNL7tTwU+jm3nf3x3D5DHSmjRlFktsedGtxZiILZTq6UNExNaFBjttR+2Y6oGqRsFWXC4ob1Wg==}
next@15.4.0-canary.4:
resolution: {integrity: sha512-ZdzHJLXVyzbWHHsQuCk9vdTcxcpgVVZWSZ7pjW7h6sBUXUyQnKAjZkdxixcD0Ge0OzWb1b6sfINqTsDQqE+Qzw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -3181,8 +3184,8 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resend@4.3.0:
resolution: {integrity: sha512-4OBHeusMVSl0vcba2J3AaGzdZ1SXAAhX/Wkcwobe16AHmlW9h3li8wG62Fhvlsc61e+wlQoxcwJZP6WrBTbghQ==}
resend@4.4.0:
resolution: {integrity: sha512-SmVI3JCpgPNt4/m3Uy403LjoSeeleUE2X+KwPYQZcw+jiBCFsqL6vdf1r/XuQ7yOjvxYmlI8GD/oIWonFF9t9w==}
engines: {node: '>=18'}
resolve-from@4.0.0:
@ -3278,6 +3281,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@ -4292,48 +4298,48 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next/bundle-analyzer@15.4.0-canary.3':
'@next/bundle-analyzer@15.4.0-canary.4':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.4.0-canary.3': {}
'@next/env@15.4.0-canary.4': {}
'@next/eslint-plugin-next@15.4.0-canary.3':
'@next/eslint-plugin-next@15.4.0-canary.4':
dependencies:
fast-glob: 3.3.1
'@next/mdx@15.4.0-canary.3(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))':
'@next/mdx@15.4.0-canary.4(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))':
dependencies:
source-map: 0.7.4
optionalDependencies:
'@mdx-js/loader': 3.1.0(acorn@8.14.1)
'@mdx-js/react': 3.1.0(@types/react@19.1.2)(react@19.1.0)
'@next/swc-darwin-arm64@15.4.0-canary.3':
'@next/swc-darwin-arm64@15.4.0-canary.4':
optional: true
'@next/swc-darwin-x64@15.4.0-canary.3':
'@next/swc-darwin-x64@15.4.0-canary.4':
optional: true
'@next/swc-linux-arm64-gnu@15.4.0-canary.3':
'@next/swc-linux-arm64-gnu@15.4.0-canary.4':
optional: true
'@next/swc-linux-arm64-musl@15.4.0-canary.3':
'@next/swc-linux-arm64-musl@15.4.0-canary.4':
optional: true
'@next/swc-linux-x64-gnu@15.4.0-canary.3':
'@next/swc-linux-x64-gnu@15.4.0-canary.4':
optional: true
'@next/swc-linux-x64-musl@15.4.0-canary.3':
'@next/swc-linux-x64-musl@15.4.0-canary.4':
optional: true
'@next/swc-win32-arm64-msvc@15.4.0-canary.3':
'@next/swc-win32-arm64-msvc@15.4.0-canary.4':
optional: true
'@next/swc-win32-x64-msvc@15.4.0-canary.3':
'@next/swc-win32-x64-msvc@15.4.0-canary.4':
optional: true
'@nodelib/fs.scandir@2.1.5':
@ -4717,9 +4723,9 @@ snapshots:
dependencies:
crypto-js: 4.2.0
'@vercel/analytics@1.5.0(next@15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
'@vercel/analytics@1.5.0(next@15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
optionalDependencies:
next: 15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next: 15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
abbrev@2.0.0: {}
@ -4851,7 +4857,7 @@ snapshots:
axobject-query@4.1.0: {}
babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411:
babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417:
dependencies:
'@babel/types': 7.27.0
@ -5312,9 +5318,9 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@15.4.0-canary.3(eslint@9.25.1)(typescript@5.8.3):
eslint-config-next@15.4.0-canary.4(eslint@9.25.1)(typescript@5.8.3):
dependencies:
'@next/eslint-plugin-next': 15.4.0-canary.3
'@next/eslint-plugin-next': 15.4.0-canary.4
'@rushstack/eslint-patch': 1.11.0
'@typescript-eslint/eslint-plugin': 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3)
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1)(typescript@5.8.3)
@ -5473,7 +5479,7 @@ snapshots:
optionalDependencies:
eslint-config-prettier: 10.1.2(eslint@9.25.1)
eslint-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411(eslint@9.25.1):
eslint-plugin-react-compiler@19.0.0-beta-af1b7da-20250417(eslint@9.25.1):
dependencies:
'@babel/core': 7.26.10
'@babel/parser': 7.27.0
@ -5733,9 +5739,9 @@ snapshots:
functions-have-names@1.2.3: {}
geist@1.3.1(next@15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
geist@1.3.1(next@15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
dependencies:
next: 15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next: 15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
gensync@1.0.0-beta.2: {}
@ -6378,7 +6384,7 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@0.501.0(react@19.1.0):
lucide-react@0.503.0(react@19.1.0):
dependencies:
react: 19.1.0
@ -6877,9 +6883,9 @@ snapshots:
natural-compare@1.4.0: {}
next@15.4.0-canary.3(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
next@15.4.0-canary.4(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-af1b7da-20250417)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.4.0-canary.3
'@next/env': 15.4.0-canary.4
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@ -6889,15 +6895,15 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.4.0-canary.3
'@next/swc-darwin-x64': 15.4.0-canary.3
'@next/swc-linux-arm64-gnu': 15.4.0-canary.3
'@next/swc-linux-arm64-musl': 15.4.0-canary.3
'@next/swc-linux-x64-gnu': 15.4.0-canary.3
'@next/swc-linux-x64-musl': 15.4.0-canary.3
'@next/swc-win32-arm64-msvc': 15.4.0-canary.3
'@next/swc-win32-x64-msvc': 15.4.0-canary.3
babel-plugin-react-compiler: 19.0.0-beta-ebf51a3-20250411
'@next/swc-darwin-arm64': 15.4.0-canary.4
'@next/swc-darwin-x64': 15.4.0-canary.4
'@next/swc-linux-arm64-gnu': 15.4.0-canary.4
'@next/swc-linux-arm64-musl': 15.4.0-canary.4
'@next/swc-linux-x64-gnu': 15.4.0-canary.4
'@next/swc-linux-x64-musl': 15.4.0-canary.4
'@next/swc-win32-arm64-msvc': 15.4.0-canary.4
'@next/swc-win32-x64-msvc': 15.4.0-canary.4
babel-plugin-react-compiler: 19.0.0-beta-af1b7da-20250417
sharp: 0.34.1
transitivePeerDependencies:
- '@babel/core'
@ -7455,7 +7461,7 @@ snapshots:
require-from-string@2.0.2: {}
resend@4.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
resend@4.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@react-email/render': 1.0.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
transitivePeerDependencies:
@ -7561,6 +7567,8 @@ snapshots:
semver@7.7.1: {}
server-only@0.0.1: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4