1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 04:45:22 -04:00

pre-render optimizations

This commit is contained in:
Jake Jarvis 2025-03-14 12:51:18 -04:00
parent e162d6a46c
commit 3932660acc
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
57 changed files with 305 additions and 318 deletions

View File

@ -7,7 +7,8 @@ const Analytics = () => {
return (
<Script
src="/_stream/u/script.js" // see next.config.ts rewrite
src="/_stream/u/script.js" // see middleware rewrite
id="umami-js"
strategy="afterInteractive"
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
data-domains={process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@ -11,7 +11,6 @@ export const metadata: Metadata = {
openGraph: {
...defaultMetadata.openGraph,
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
images: [thumbnail.src],
url: "/birthday",
},
alternates: {

BIN
app/cli/opengraph-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -1,15 +1,12 @@
import PageTitle from "../../components/PageTitle";
import { metadata as defaultMetadata } from "../layout";
import featuredImage from "./screenshot.png";
export const metadata = {
title: "CLI",
description: "AKA, the most useless Node module ever published, in history, by anyone, ever.",
openGraph: {
...defaultMetadata.openGraph,
title: "CLI",
images: [featuredImage.src],
url: "/cli",
},
alternates: {

View File

@ -3,7 +3,7 @@
import { headers } from "next/headers";
import { z } from "zod";
import { Resend } from "resend";
import config from "../../lib/config";
import * as config from "../../lib/config";
const schema = z.object({
name: z.string().min(1, { message: "Name is required" }),

View File

@ -3,7 +3,7 @@
padding: 0.8em;
margin: 0.6em 0;
border: 2px solid var(--colors-light);
border-radius: var(--radii-corner);
border-radius: 0.6em;
color: var(--colors-text);
background-color: var(--colors-superDuperLight);
}
@ -36,7 +36,7 @@
padding: 1em 1.25em;
margin-right: 1.5em;
border: 0;
border-radius: var(--radii-corner);
border-radius: 0.6em;
cursor: pointer;
user-select: none;
font-weight: 500;

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -12,7 +12,6 @@ export const metadata: Metadata = {
openGraph: {
...defaultMetadata.openGraph,
title: "My Brief Apperance in Hillary Clinton's DNC Video",
images: [thumbnail.src],
url: "/hillary",
},
alternates: {

View File

@ -13,7 +13,6 @@
}
.container {
max-width: var(--sizes-maxLayoutWidth);
margin: 0 auto;
display: block;
}

View File

@ -4,7 +4,8 @@ import { ThemeProvider } from "../contexts/ThemeContext";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { SkipToContentLink, SkipToContentTarget } from "../components/SkipToContent";
import config from "../lib/config";
import * as config from "../lib/config";
import { BASE_URL, MAX_WIDTH } from "../lib/config/constants";
import type { Metadata } from "next";
import type { Person, WithContext } from "schema-dts";
@ -15,10 +16,10 @@ import "./global.css";
import styles from "./layout.module.css";
import meJpg from "../public/static/me.jpg";
import ogImage from "./opengraph-image.jpg";
export const metadata: Metadata = {
metadataBase: new URL(config.baseUrl),
metadataBase: new URL(BASE_URL),
title: {
template: `%s ${config.siteName}`,
default: `${config.siteName} ${config.shortDescription}`,
@ -64,10 +65,10 @@ const jsonLd: WithContext<Person> = {
"@context": "https://schema.org",
"@type": "Person",
name: config.authorName,
url: config.baseUrl,
image: `${config.baseUrl}${meJpg.src}`,
url: BASE_URL,
image: `${BASE_URL}${ogImage.src}`,
sameAs: [
config.baseUrl,
BASE_URL,
`https://github.com/${config.authorSocial?.github}`,
`https://keybase.io/${config.authorSocial?.keybase}`,
`https://twitter.com/${config.authorSocial?.twitter}`,
@ -96,7 +97,9 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
<main className={styles.default}>
<SkipToContentTarget />
<div className={styles.container}>{children}</div>
<div className={styles.container} style={{ maxWidth: MAX_WIDTH }}>
{children}
</div>
</main>
<Footer />

BIN
app/leo/opengraph-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -12,7 +12,6 @@ export const metadata: Metadata = {
openGraph: {
...defaultMetadata.openGraph,
title: 'Facebook App on "The Lab with Leo Laporte"',
images: [thumbnail.src],
url: "/leo",
},
alternates: {

View File

@ -1,4 +1,4 @@
import config from "../lib/config";
import * as config from "../lib/config";
import type { MetadataRoute } from "next";
const manifest = (): MetadataRoute.Manifest => {

View File

@ -3,7 +3,7 @@ import Video from "../components/Video";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "404 Not Found",
title: "Page Not Found",
description: null,
openGraph: {},
alternates: {
@ -23,7 +23,7 @@ const Page = () => {
}}
/>
<h1>404: Page Not Found 😢</h1>
<h1 style={{ margin: "0.2em auto" }}>Page Not Found 😢</h1>
<Link href="/">Go home?</Link>
</div>

View File

@ -1,13 +1,11 @@
/* eslint-disable jsx-a11y/alt-text */
import { ImageResponse } from "next/og";
import { notFound } from "next/navigation";
import path from "path";
import fs from "fs/promises";
import glob from "fast-glob";
import { join } from "path";
import { existsSync } from "fs";
import { readFile } from "fs/promises";
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import { POSTS_DIR, AVATAR_PATH } from "../../../lib/config/constants";
export const dynamicParams = false;
export const contentType = "image/png";
export const size = {
// https://developers.facebook.com/docs/sharing/webmasters/images/
@ -15,6 +13,10 @@ export const size = {
height: 630,
};
// generate and cache these images at build-time for each slug, since doing this on-demand is mega slow...
export const dynamic = "force-static";
export const dynamicParams = false;
export const generateStaticParams = async () => {
const slugs = await getPostSlugs();
@ -24,29 +26,35 @@ export const generateStaticParams = async () => {
}));
};
const getLocalImage = async (src: string) => {
const imagePath = await glob(src);
if (imagePath.length > 0) {
const imageData = await fs.readFile(path.join(process.cwd(), imagePath[0]));
return Uint8Array.from(imageData).buffer;
}
const getLocalImage = async (src: string): Promise<ArrayBuffer | string> => {
// https://stackoverflow.com/questions/5775469/whats-the-valid-way-to-include-an-image-with-no-src/14115340#14115340
const NO_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
// image doesn't exist
return null;
const imagePath = join(process.cwd(), src);
try {
if (!existsSync(imagePath)) {
console.error(`[og-image] couldn't find an image file located at "${imagePath}"`);
// return a 1x1 transparent gif if the image doesn't exist instead of crashing
return NO_IMAGE;
}
// return the raw image data as a buffer
return Uint8Array.from(await readFile(imagePath)).buffer;
} catch (error) {
// fail silently and return a 1x1 transparent gif instead of crashing
console.error(`[og-image] found "${imagePath}" but couldn't read it:`, error);
return NO_IMAGE;
}
};
const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
try {
const { slug } = await params;
// get the note's title and image filename from its frontmatter
const { title, image } = await getFrontMatter(slug);
// load the image specified in the note's frontmatter from its directory
const imageSrc = await getLocalImage(`notes/${slug}/${image}`);
// load the author avatar
const avatarSrc = await getLocalImage("public/static/me.jpg");
// get the post's title and image filename from its frontmatter
const { title, image: imagePath } = await getFrontMatter(slug);
return new ImageResponse(
(
@ -59,7 +67,7 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
background: "linear-gradient(0deg, hsla(197, 14%, 57%, 1) 0%, hsla(192, 17%, 94%, 1) 100%)",
}}
>
{imageSrc && (
{imagePath && (
<div
style={{
display: "flex",
@ -67,14 +75,15 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
width: "100%",
}}
>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img
// @ts-expect-error
src={imageSrc}
src={await getLocalImage(`${POSTS_DIR}/${slug}/${imagePath}`)}
style={{ objectFit: "cover", height: "100%", width: "100%" }}
/>
</div>
)}
{avatarSrc && (
{AVATAR_PATH && (
<div
style={{
display: "flex",
@ -83,9 +92,10 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
top: 42,
}}
>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img
// @ts-expect-error
src={avatarSrc}
src={await getLocalImage(AVATAR_PATH)}
style={{ height: 96, width: 96, borderRadius: "100%" }}
/>
</div>
@ -117,9 +127,8 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
{
name: "Geist",
// load the Geist font directly from its npm package
data: await fs.readFile(
path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")
),
// IMPORTANT: include this exact path in next.config.ts under "outputFileTracingIncludes"
data: await readFile(join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")),
style: "normal",
weight: 600,
},
@ -127,7 +136,7 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
}
);
} catch (error) {
console.error("[og-image] Error generating image:", error);
console.error("[og-image] error generating image:", error);
notFound();
}
};

View File

@ -8,7 +8,8 @@ import Loading from "../../../components/Loading";
import HitCounter from "./counter";
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import { metadata as defaultMetadata } from "../../layout";
import config from "../../../lib/config";
import * as config from "../../../lib/config";
import { BASE_URL } from "../../../lib/config/constants";
import type { Metadata, Route } from "next";
import type { Article, WithContext } from "schema-dts";
@ -72,7 +73,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
author: {
"@type": "Person",
name: config.authorName,
url: config.baseUrl,
url: BASE_URL,
},
};

View File

@ -1,7 +1,7 @@
import Link from "../../components/Link";
import Time from "../../components/Time";
import { getAllPosts } from "../../lib/helpers/posts";
import config from "../../lib/config";
import * as config from "../../lib/config";
import { metadata as defaultMetadata } from "../layout";
import type { ReactElement } from "react";
import type { Metadata, Route } from "next";

BIN
app/opengraph-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -2,27 +2,11 @@ import hash from "@emotion/hash";
import { rgba } from "polished";
import { LockIcon } from "lucide-react";
import UnstyledLink from "../components/Link";
import { metadata as defaultMetadata } from "./layout";
import type { ComponentPropsWithoutRef } from "react";
import type { Metadata, Route } from "next";
import type { Route } from "next";
import styles from "./page.module.css";
import meJpg from "../public/static/me.jpg";
export const metadata: Metadata = {
openGraph: {
...defaultMetadata.openGraph,
images: [
{
url: meJpg.src,
width: meJpg.width,
height: meJpg.height,
},
],
},
};
const Link = ({
lightColor,
darkColor,

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -24,7 +24,6 @@ body:has(.wackyWrapper) a {
display: block;
margin: 0.6em 0;
border: 2px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
}
.screenshot,

View File

@ -13,7 +13,7 @@
width: 370px;
padding: 1.2em 1.2em 0.8em;
border: 1px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
border-radius: 1em;
font-size: 0.9em;
color: var(--colors-mediumDark);
}

View File

@ -5,7 +5,7 @@ import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import { metadata as defaultMetadata } from "../layout";
import config from "../../lib/config";
import * as config from "../../lib/config";
import type { Metadata } from "next";
import type { User, Repository } from "@octokit/graphql-schema";

View File

@ -1,4 +1,4 @@
import config from "../lib/config";
import { BASE_URL } from "../lib/config/constants";
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
@ -74,7 +74,7 @@ const robots = (): MetadataRoute.Robots => {
disallow: "/",
},
],
sitemap: `${config.baseUrl}/sitemap.xml`,
sitemap: `${BASE_URL}/sitemap.xml`,
};
};

View File

@ -1,7 +1,7 @@
import path from "path";
import glob from "fast-glob";
import { getAllPosts } from "../lib/helpers/posts";
import config from "../lib/config";
import { BASE_URL } from "../lib/config/constants";
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
@ -11,13 +11,13 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
const routes: MetadataRoute.Sitemap = [
{
// homepage
url: config.baseUrl,
url: BASE_URL,
priority: 1.0,
lastModified: new Date(process.env.RELEASE_DATE || Date.now()), // timestamp frozen when a new build is deployed
},
{ url: `${config.baseUrl}/stats` },
{ url: `${config.baseUrl}/tweets` },
{ url: `${config.baseUrl}/y2k` },
{ url: `${BASE_URL}/stats` },
{ url: `${BASE_URL}/tweets` },
{ url: `${BASE_URL}/y2k` },
];
// add each directory in the app folder as a route (excluding special routes)
@ -35,7 +35,7 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
).forEach((route) => {
routes.push({
// remove matching page.(tsx|mdx) file and make all URLs absolute
url: `${config.baseUrl}/${route.replace(/\/page\.(tsx|mdx)$/, "")}`,
url: `${BASE_URL}/${route.replace(/\/page\.(tsx|mdx)$/, "")}`,
});
});

View File

@ -26,8 +26,6 @@
--colors-codeVariable: #d88200;
--colors-codeAddition: #44a248;
--colors-codeDeletion: #ff1b1b;
--sizes-maxLayoutWidth: 865px;
--radii-corner: 0.6rem;
}
[data-theme="dark"] {

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

View File

@ -1,15 +1,12 @@
import PageTitle from "../../components/PageTitle";
import { metadata as defaultMetadata } from "../layout";
import featuredImage from "./desktop.png";
export const metadata = {
title: "/uses",
description: "Things I use daily.",
openGraph: {
...defaultMetadata.openGraph,
title: "/uses",
images: [featuredImage.src],
url: "/uses",
},
alternates: {

BIN
app/zip/opengraph-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

View File

@ -26,7 +26,7 @@ const Page = () => {
backgroundImage: `url(${backgroundImg.src})`,
backgroundRepeat: "repeat",
backgroundPosition: "center",
borderRadius: "var(--radii-corner)",
borderRadius: "0.6em",
}}
>
<CodeBlock

View File

@ -13,7 +13,7 @@
tab-size: 2px;
background-color: var(--colors-codeBackground);
border: 1px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
border-radius: 0.6em;
}
.codeBlock :global(.line-number)::before {
@ -106,8 +106,8 @@
width: 3em;
color: var(--colors-mediumDark);
border: 1px solid var(--colors-kindaLight);
border-top-right-radius: var(--radii-corner);
border-bottom-left-radius: var(--radii-corner);
border-top-right-radius: 0.6em;
border-bottom-left-radius: 0.6em;
background-color: var(--colors-backgroundHeader);
backdrop-filter: saturate(180%) blur(5px);
}

View File

@ -4,5 +4,5 @@
page-break-inside: avoid;
background-color: var(--colors-codeBackground);
border: 1px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
border-radius: 0.6em;
}

View File

@ -1,7 +1,7 @@
"use client";
import Giscus from "@giscus/react";
import config from "../../lib/config";
import * as config from "../../lib/config";
import type { GiscusProps } from "@giscus/react";
export type CommentsProps = {

View File

@ -10,7 +10,6 @@
.row {
display: flex;
width: 100%;
max-width: var(--sizes-maxLayoutWidth);
margin: 0 auto;
justify-content: space-between;
font-size: 0.8em;

View File

@ -1,7 +1,8 @@
import clsx from "clsx";
import { HeartIcon } from "lucide-react";
import Link from "../Link";
import config from "../../lib/config";
import * as config from "../../lib/config";
import { MAX_WIDTH } from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Footer.module.css";
@ -11,7 +12,7 @@ export type FooterProps = ComponentPropsWithoutRef<"footer">;
const Footer = ({ className, ...rest }: FooterProps) => {
return (
<footer className={clsx(styles.footer, className)} {...rest}>
<div className={styles.row}>
<div className={styles.row} style={{ maxWidth: MAX_WIDTH }}>
<div>
Content{" "}
<Link href="/license" title={config.license} plain className={styles.link}>

View File

@ -14,22 +14,22 @@
backdrop-filter: saturate(180%) blur(5px);
}
.selfieImage {
.homeImage {
width: 50px;
height: 50px;
border: 1px solid var(--colors-light);
border-radius: 50%;
}
.selfieLink {
.homeLink {
display: inline-flex;
flex-shrink: 0;
align-items: center;
color: var(--colors-mediumDark) !important;
}
.selfieLink:hover,
.selfieLink:focus-visible {
.homeLink:hover,
.homeLink:focus-visible {
color: var(--colors-link) !important;
}
@ -46,7 +46,6 @@
align-items: center;
justify-content: space-between;
width: 100%;
max-width: var(--sizes-maxLayoutWidth);
margin: 0 auto;
}
@ -56,14 +55,14 @@
height: 5.9em;
}
.selfieImage {
.homeImage {
width: 70px;
height: 70px;
border-width: 2px;
}
.selfieLink:hover .selfieImage,
.selfieLink:focus-visible .selfieImage {
.homeLink:hover .homeImage,
.homeLink:focus-visible .homeImage {
border-color: var(--colors-linkUnderline);
}

View File

@ -2,7 +2,8 @@ import clsx from "clsx";
import Link from "../Link";
import Image from "../Image";
import Menu from "../Menu";
import config from "../../lib/config";
import * as config from "../../lib/config";
import { MAX_WIDTH } from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Header.module.css";
@ -14,12 +15,12 @@ export type HeaderProps = ComponentPropsWithoutRef<"header">;
const Header = ({ className, ...rest }: HeaderProps) => {
return (
<header className={clsx(styles.header, className)} {...rest}>
<nav className={styles.nav}>
<Link href="/" rel="author" title={config.authorName} plain className={styles.selfieLink}>
<nav className={styles.nav} style={{ maxWidth: MAX_WIDTH }}>
<Link href="/" rel="author" title={config.authorName} plain className={styles.homeLink}>
<Image
src={selfieJpg}
alt={`Photo of ${config.authorName}`}
className={styles.selfieImage}
className={styles.homeImage}
width={70}
height={70}
quality={60}

View File

@ -9,7 +9,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
const HeadingAnchor = ({ id, title, ...rest }: HeadingAnchorProps) => {
return (
<Link href={`#${id}`} title={`Jump to "${title}"`} aria-hidden plain {...rest}>
<Link href={`#${id}`} title={`Jump to "${title}"`} plain {...rest}>
<LinkIcon size="0.8em" />
</Link>
);

View File

@ -1,5 +0,0 @@
.image {
height: auto;
max-width: 100%;
border-radius: var(--radii-corner);
}

View File

@ -1,15 +1,11 @@
import NextImage from "next/image";
import clsx from "clsx";
import { MAX_WIDTH } from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react";
import type { StaticImageData } from "next/image";
import styles from "./Image.module.css";
const MAX_WIDTH = 865;
export type ImageProps = ComponentPropsWithoutRef<typeof NextImage>;
const Image = ({ src, height, width, quality, placeholder, className, ...rest }: ImageProps) => {
const Image = ({ src, height, width, quality, placeholder, style, ...rest }: ImageProps) => {
const constrainWidth = (width?: number | `${number}`) => {
if (!width) return MAX_WIDTH;
@ -22,10 +18,14 @@ const Image = ({ src, height, width, quality, placeholder, className, ...rest }:
width: constrainWidth(width || (src as StaticImageData).width),
quality: quality || 75,
placeholder: placeholder || (typeof src === "string" ? "empty" : "blur"),
style: {
height: "auto",
...style,
},
...rest,
};
return <NextImage className={clsx(styles.image, className)} {...imageProps} />;
return <NextImage {...imageProps} />;
};
export default Image;

View File

@ -1,7 +1,7 @@
import NextLink from "next/link";
import clsx from "clsx";
import objStr from "obj-str";
import config from "../../lib/config";
import { BASE_URL } from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css";
@ -14,7 +14,7 @@ export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
const Link = ({ href, rel, target, prefetch = false, plain, openInNewTab, className, ...rest }: LinkProps) => {
// This component auto-detects whether or not this link should open in the same window (the default for internal
// links) or a new tab (the default for external links). Defaults can be overridden with `openInNewTab={true}`.
const isExternal = typeof href === "string" && !(["/", "#"].includes(href[0]) || href.startsWith(config.baseUrl));
const isExternal = typeof href === "string" && !(["/", "#"].includes(href[0]) || href.startsWith(BASE_URL));
if (openInNewTab || isExternal) {
return (

View File

@ -1,7 +1,7 @@
/* accessible invisibility stuff pulled from @reach/skip-nav:
https://github.com/reach/reach-ui/blob/main/packages/skip-nav/styles.css */
.hiddenLink {
.skipNav {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
@ -12,7 +12,7 @@ https://github.com/reach/reach-ui/blob/main/packages/skip-nav/styles.css */
position: absolute;
}
.hiddenLink:focus {
.skipNav:focus {
padding: 1rem;
position: fixed;
top: 10px;
@ -24,6 +24,5 @@ https://github.com/reach/reach-ui/blob/main/packages/skip-nav/styles.css */
background: var(--colors-superDuperLight);
color: var(--colors-link);
border: 2px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
text-decoration: underline;
}

View File

@ -4,7 +4,7 @@ const skipNavId = "skip-nav";
export const SkipToContentLink = () => {
return (
<a href={`#${skipNavId}`} tabIndex={0} className={styles.hiddenLink}>
<a href={`#${skipNavId}`} tabIndex={0} className={styles.skipNav}>
Skip to content
</a>
);

View File

@ -11,3 +11,15 @@
.toggle:focus-visible {
color: var(--colors-warning);
}
/* hacky way to avoid flashing icon for a few milliseconds on initial render */
.toggle > .sun,
[data-theme="dark"] .toggle > .moon {
display: inline-block;
}
/* stylelint-disable-next-line no-descending-specificity */
.toggle > .moon,
[data-theme="dark"] .toggle > .sun {
display: none;
}

View File

@ -1,7 +1,8 @@
"use client";
import { useHasMounted, useTheme } from "../../hooks";
import { EllipsisIcon, MoonIcon, SunIcon } from "lucide-react";
import clsx from "clsx";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "../../hooks";
import type { ComponentPropsWithoutRef } from "react";
import type { LucideIcon } from "lucide-react";
@ -9,27 +10,17 @@ import styles from "./ThemeToggle.module.css";
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
const ThemeToggle = ({ ...rest }: ThemeToggleProps) => {
const hasMounted = useHasMounted();
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
const { theme, setTheme } = useTheme();
// render a placeholder icon to avoid layout shifting until we're fully mounted and self-aware
if (!hasMounted) {
return (
<div className={styles.toggle}>
<EllipsisIcon style={{ stroke: "var(--colors-mediumLight)" }} {...rest} />
</div>
);
}
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
aria-label="Toggle Theme"
className={styles.toggle}
title={theme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
aria-label={theme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
>
{theme === "light" ? <SunIcon {...rest} /> : <MoonIcon {...rest} />}
<SunIcon className={clsx(styles.sun, className)} {...rest} />
<MoonIcon className={clsx(styles.moon, className)} {...rest} />
</button>
);
};

View File

@ -2,10 +2,6 @@
margin: 0 auto;
}
.player video {
border-radius: var(--radii-corner);
}
.wrapper.responsive {
position: relative;
padding-top: 56.25%; /* ratio of 1280x720 */

17
lib/config/constants.ts Normal file
View File

@ -0,0 +1,17 @@
// path to directory with .mdx files, relative to project root
export const POSTS_DIR = "notes";
// path to an image used in various places to represent the site, relative to project root
// IMPORTANT: must be included in next.config.ts under "outputFileTracingIncludes"
export const AVATAR_PATH = "app/opengraph-image.jpg";
// maximum width of content wrapper (e.g. for images) in pixels
export const MAX_WIDTH = 865;
// same logic as metadataBase: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#default-value
export const BASE_URL =
process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}`
: process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" && process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: `http://localhost:${process.env.PORT || 3000}`;

View File

@ -1,39 +1,28 @@
const config = {
// Site info
siteName: "Jake Jarvis",
siteLocale: "en-US",
baseUrl:
// same logic as metadataBase: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#default-value
process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}`
: process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" && process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: `http://localhost:${process.env.PORT || 3000}`,
timeZone: "America/New_York", // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
onionDomain: "http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion",
shortDescription: "Front-End Web Developer in Boston, MA",
longDescription:
"Hi there! I'm a frontend web developer based in Boston, Massachusetts specializing in the JAMstack, modern JavaScript frameworks, and progressive web apps.",
license: "Creative Commons Attribution 4.0 International",
licenseAbbr: "CC-BY-4.0",
licenseUrl: "https://creativecommons.org/licenses/by/4.0/",
copyrightYearStart: 2001,
githubRepo: "jakejarvis/jarv.is",
// Site info
export const siteName = "Jake Jarvis";
export const siteLocale = "en-US";
export const timeZone = "America/New_York"; // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
export const onionDomain = "jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion";
export const shortDescription = "Front-End Web Developer in Boston; MA";
export const longDescription =
"Hi there! I'm a frontend web developer based in Boston, Massachusetts specializing in the JAMstack, modern JavaScript frameworks, and progressive web apps.";
export const license = "Creative Commons Attribution 4.0 International";
export const licenseAbbr = "CC-BY-4.0";
export const licenseUrl = "https://creativecommons.org/licenses/by/4.0/";
export const copyrightYearStart = 2001;
export const githubRepo = "jakejarvis/jarv.is";
// Me info
authorName: "Jake Jarvis",
authorEmail: "jake@jarv.is",
authorSocial: {
github: "jakejarvis",
twitter: "jakejarvis",
facebook: "jakejarvis",
keybase: "jakejarvis",
medium: "jakejarvis",
linkedin: "jakejarvis",
instagram: "jakejarvis",
mastodon: "fediverse.jarv.is/@jake",
bluesky: "jarv.is",
},
// Me info
export const authorName = "Jake Jarvis";
export const authorEmail = "jake@jarv.is";
export const authorSocial = {
github: "jakejarvis",
twitter: "jakejarvis",
facebook: "jakejarvis",
keybase: "jakejarvis",
medium: "jakejarvis",
linkedin: "jakejarvis",
instagram: "jakejarvis",
mastodon: "fediverse.jarv.is/@jake",
bluesky: "jarv.is",
};
export default config;

View File

@ -1,26 +1,27 @@
import { Feed } from "feed";
import { getAllPosts } from "./posts";
import config from "../config";
import * as config from "../config";
import { BASE_URL } from "../config/constants";
import meJpg from "../../public/static/me.jpg";
import ogImage from "../../app/opengraph-image.jpg";
export const buildFeed = async (): Promise<Feed> => {
// https://github.com/jpmonette/feed#example
const feed = new Feed({
id: config.baseUrl,
link: config.baseUrl,
id: BASE_URL,
link: BASE_URL,
title: config.siteName,
description: config.longDescription,
copyright: config.licenseUrl,
updated: new Date(process.env.RELEASE_DATE || Date.now()),
image: `${config.baseUrl}${meJpg.src}`,
image: `${BASE_URL}${ogImage.src}`,
feedLinks: {
rss: `${config.baseUrl}/feed.xml`,
atom: `${config.baseUrl}/feed.atom`,
rss: `${BASE_URL}/feed.xml`,
atom: `${BASE_URL}/feed.atom`,
},
author: {
name: config.authorName,
link: config.baseUrl,
link: BASE_URL,
email: config.authorEmail,
},
});
@ -35,7 +36,7 @@ export const buildFeed = async (): Promise<Feed> => {
author: [
{
name: config.authorName,
link: config.baseUrl,
link: BASE_URL,
},
],
date: new Date(post.date),

View File

@ -5,7 +5,7 @@ import dayjsRelativeTime from "dayjs/plugin/relativeTime";
import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat";
import dayjsAdvancedFormat from "dayjs/plugin/advancedFormat";
import "dayjs/locale/en";
import config from "../config";
import * as config from "../config";
const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => {
// plugins

View File

@ -3,10 +3,7 @@ import glob from "fast-glob";
import pMap from "p-map";
import pMemoize from "p-memoize";
import { formatDate } from "./format-date";
import config from "../config";
// path to directory with .mdx files, relative to project root
const POSTS_DIR = "notes";
import { BASE_URL, POSTS_DIR } from "../config/constants";
export type FrontMatter = {
slug: string;
@ -65,7 +62,7 @@ export const getFrontMatter = async (slug: string): Promise<FrontMatter> => {
htmlTitle,
slug,
date: formatDate(frontmatter.date), // validate/normalize the date string provided from front matter
permalink: `${config.baseUrl}/${POSTS_DIR}/${slug}`,
permalink: `${BASE_URL}/${POSTS_DIR}/${slug}`,
};
};

View File

@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import siteConfig from "./lib/config";
import * as siteConfig from "./lib/config";
// assign "short codes" to approved reverse proxy destinations. for example:
// ["abc", "https://jakejarvis.github.io"] => /_stream/abc/123.html -> https://jakejarvis.github.io/123.html
@ -16,7 +16,12 @@ export const middleware = (request: NextRequest) => {
// https://gitweb.torproject.org/tor-browser-spec.git/tree/proposals/100-onion-location-header.txt
if (siteConfig.onionDomain) {
headers.set("Onion-Location", `${siteConfig.onionDomain}${request.nextUrl.pathname}${request.nextUrl.search}`);
const onionUrl = request.nextUrl.clone();
onionUrl.hostname = siteConfig.onionDomain;
onionUrl.protocol = "http";
onionUrl.port = "";
headers.set("onion-location", onionUrl.toString());
}
// debugging 🥛

View File

@ -22,7 +22,7 @@ const nextConfig: NextConfig = {
outputFileTracingIncludes: {
"/notes/[slug]/opengraph-image": [
"./notes/**/*",
"./public/static/me.jpg",
"./app/opengraph-image.jpg",
"./node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf",
],
},

View File

@ -22,9 +22,9 @@
"@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "15.3.0-canary.6",
"@next/mdx": "15.3.0-canary.6",
"@next/third-parties": "15.3.0-canary.6",
"@next/bundle-analyzer": "15.3.0-canary.8",
"@next/mdx": "15.3.0-canary.8",
"@next/third-parties": "15.3.0-canary.8",
"@octokit/graphql": "^8.2.1",
"@octokit/graphql-schema": "^15.26.0",
"@prisma/client": "^6.5.0",
@ -35,9 +35,9 @@
"fast-glob": "^3.3.3",
"feed": "^4.2.2",
"geist": "^1.3.1",
"lucide-react": "0.479.0",
"lucide-react": "0.481.0",
"modern-normalize": "^3.0.1",
"next": "15.3.0-canary.6",
"next": "15.3.0-canary.8",
"obj-str": "^1.1.0",
"p-map": "^7.0.3",
"p-memoize": "^7.1.1",
@ -78,7 +78,7 @@
"@types/react-is": "^19.0.0",
"cross-env": "^7.0.3",
"eslint": "^9.22.0",
"eslint-config-next": "15.3.0-canary.6",
"eslint-config-next": "15.3.0-canary.8",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.31.0",
@ -92,7 +92,7 @@
"prisma": "^6.5.0",
"schema-dts": "^1.1.5",
"simple-git-hooks": "^2.11.1",
"stylelint": "^16.15.0",
"stylelint": "^16.16.0",
"stylelint-config-css-modules": "^4.4.0",
"stylelint-config-recommended": "^15.0.0",
"stylelint-config-standard": "^37.0.0",
@ -104,7 +104,7 @@
"engines": {
"node": ">=20.x"
},
"packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b",
"packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6",
"cacheDirectories": [
"node_modules",
".next/cache"

216
pnpm-lock.yaml generated
View File

@ -21,14 +21,14 @@ importers:
specifier: ^3.1.0
version: 3.1.0(@types/react@19.0.10)(react@19.0.0)
'@next/bundle-analyzer':
specifier: 15.3.0-canary.6
version: 15.3.0-canary.6
specifier: 15.3.0-canary.8
version: 15.3.0-canary.8
'@next/mdx':
specifier: 15.3.0-canary.6
version: 15.3.0-canary.6(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.0.10)(react@19.0.0))
specifier: 15.3.0-canary.8
version: 15.3.0-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.0.10)(react@19.0.0))
'@next/third-parties':
specifier: 15.3.0-canary.6
version: 15.3.0-canary.6(next@15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
specifier: 15.3.0-canary.8
version: 15.3.0-canary.8(next@15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
'@octokit/graphql':
specifier: ^8.2.1
version: 8.2.1
@ -58,16 +58,16 @@ importers:
version: 4.2.2
geist:
specifier: ^1.3.1
version: 1.3.1(next@15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
version: 1.3.1(next@15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
lucide-react:
specifier: 0.479.0
version: 0.479.0(react@19.0.0)
specifier: 0.481.0
version: 0.481.0(react@19.0.0)
modern-normalize:
specifier: ^3.0.1
version: 3.0.1
next:
specifier: 15.3.0-canary.6
version: 15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: 15.3.0-canary.8
version: 15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
obj-str:
specifier: ^1.1.0
version: 1.1.0
@ -184,8 +184,8 @@ importers:
specifier: ^9.22.0
version: 9.22.0
eslint-config-next:
specifier: 15.3.0-canary.6
version: 15.3.0-canary.6(eslint@9.22.0)(typescript@5.8.2)
specifier: 15.3.0-canary.8
version: 15.3.0-canary.8(eslint@9.22.0)(typescript@5.8.2)
eslint-config-prettier:
specifier: ^10.1.1
version: 10.1.1(eslint@9.22.0)
@ -194,7 +194,7 @@ importers:
version: 2.12.0(eslint@9.22.0)
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0)
version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0)
eslint-plugin-jsx-a11y:
specifier: ^6.10.2
version: 6.10.2(eslint@9.22.0)
@ -226,17 +226,17 @@ importers:
specifier: ^2.11.1
version: 2.11.1
stylelint:
specifier: ^16.15.0
version: 16.15.0(typescript@5.8.2)
specifier: ^16.16.0
version: 16.16.0(typescript@5.8.2)
stylelint-config-css-modules:
specifier: ^4.4.0
version: 4.4.0(stylelint@16.15.0(typescript@5.8.2))
version: 4.4.0(stylelint@16.16.0(typescript@5.8.2))
stylelint-config-recommended:
specifier: ^15.0.0
version: 15.0.0(stylelint@16.15.0(typescript@5.8.2))
version: 15.0.0(stylelint@16.16.0(typescript@5.8.2))
stylelint-config-standard:
specifier: ^37.0.0
version: 37.0.0(stylelint@16.15.0(typescript@5.8.2))
version: 37.0.0(stylelint@16.16.0(typescript@5.8.2))
typescript:
specifier: 5.8.2
version: 5.8.2
@ -649,17 +649,17 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@next/bundle-analyzer@15.3.0-canary.6':
resolution: {integrity: sha512-M6eaBkxo60WbGYv5pY5/FcpfMT8I9R7qepcoEpIxP0N7cMuo2IURXMhdt2rgmrZmBs+8xsqCotVRDt2rO4tA0w==}
'@next/bundle-analyzer@15.3.0-canary.8':
resolution: {integrity: sha512-K3PLM+bxciuah4Q/coJQvxEVWhkptKAWous0fjjbajSgNY8h+DqK77ZZZc00u+0EOiLYsTUntoXb8VDNGyChNw==}
'@next/env@15.3.0-canary.6':
resolution: {integrity: sha512-ZaiOaNef86GzehX2W7ZbKf3F9XXwrzcKQBnVvmKjHokumQkixoRb4cmSpro5wuEZ761nu15Z5cQeIudvEad4QA==}
'@next/env@15.3.0-canary.8':
resolution: {integrity: sha512-/cGLuOWycBJnskYxETUtVH8XdwfYNiWJ7Db17qnZd76nLDLfulnIKSZ4QiHTm3cm5uWj9HhkXhm1Pkbj2LObuA==}
'@next/eslint-plugin-next@15.3.0-canary.6':
resolution: {integrity: sha512-s2M/oWJR5dDP3nzzdHpsC83YZgz/QF0Wlt7Kus0dxSTgur8WpyO59QvNFqeYWxHwcrk1tM6XHkaGGBBdodYX7g==}
'@next/eslint-plugin-next@15.3.0-canary.8':
resolution: {integrity: sha512-WIa24zXGlI/8xpAOvWrdiHA9ypj8eTh2KHowNMHoU2u4GC9igV+GOOjghhBx/PhAkYyiXpG2iJ3EFnvf7sBMQA==}
'@next/mdx@15.3.0-canary.6':
resolution: {integrity: sha512-opVhy5uHLs6nuSxSxeScKLrHzNMzM6Vy3EGet7vvm6pV1o5oFWjDTpd8QcBcUovbPRZst2UQJ4R39SCj4wsSww==}
'@next/mdx@15.3.0-canary.8':
resolution: {integrity: sha512-VaKbWHy32z72JyCNT5kqDALy253aPlJsavNvRC+Wm8d3diXUdOc+ztou4KGy0iRC8+o6flE9sncy/6bzzBV0VA==}
peerDependencies:
'@mdx-js/loader': '>=0.15.0'
'@mdx-js/react': '>=0.15.0'
@ -669,56 +669,56 @@ packages:
'@mdx-js/react':
optional: true
'@next/swc-darwin-arm64@15.3.0-canary.6':
resolution: {integrity: sha512-Ha7GrbUmF5sdMYGHwcTP1+eFtsl/1Bobxc2yEzBONF4ZGsDwTJtJ0mg6/duyDBH6vB4Qi5mvO9ohe8AKcubFwA==}
'@next/swc-darwin-arm64@15.3.0-canary.8':
resolution: {integrity: sha512-ItDJSApzYkYLKxjXvI2WRYeb8FkCww6/Wou0Tsdunixh5XeUp/UE7cNP88YGgmp3bI3FgDpVYnP+rUsPToJBUg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.3.0-canary.6':
resolution: {integrity: sha512-SwEH5UZ1D7BhUThx5qu0ONhSXYaIAziC3o021lfWMzPtsZlylxP6W5CLRnyf16geTNEsn9/PcipaI9pRvmVaEQ==}
'@next/swc-darwin-x64@15.3.0-canary.8':
resolution: {integrity: sha512-kh55C9939RM0unsZ7700LlHeqUNwnv2Vmd8iz40HQ+G8aaS/Dkd5M7kdzMXkGcY3D79TFL1dxGSL6s1te5R4dg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.3.0-canary.6':
resolution: {integrity: sha512-IqRHfFOaICHC8Pfb/8NFGn1jzSb3pbR9IE6ea/DjQp4dv5NguMQ3iPB3SEMEojh9mvpUhmXT7FoSF9xZO24E5w==}
'@next/swc-linux-arm64-gnu@15.3.0-canary.8':
resolution: {integrity: sha512-YVyuFoF0GAssBsCTnv1w4rJCxPApVe3UQyNRBjrTL/szRm5kZc6lPz+bBTc/QfgsOZz7iGV/6z4CuR8L+NSm0w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.3.0-canary.6':
resolution: {integrity: sha512-jyplseHznib0tfe/fx5l2lV4f/ZpDHSJBnq8d5dOQZwa9kqGD/plcfqOqdwR7XmwYrDoXXyDiWArk6F1XCMWDQ==}
'@next/swc-linux-arm64-musl@15.3.0-canary.8':
resolution: {integrity: sha512-YMZxUsvNz7LtVd8q9pXlp+AJoN73lvycUMLB+Or2Zc207KvnPxIspkmeOj4AiiNC6q5AIxc0W3YxWH7NmFvJJg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.3.0-canary.6':
resolution: {integrity: sha512-vMPd9dTR1eRYFGYGb2J/5WYuBYiUXy/hNFhnXbBq5hVf2kZ8wlJblJuNC/yyXr9lvSf4jsXDuWeXNYPaRl85hA==}
'@next/swc-linux-x64-gnu@15.3.0-canary.8':
resolution: {integrity: sha512-Cbij+lmUlqj0FSv64jAU5j8iTAGSb2BAfXpE0Crz8pymddlny1h+mYJtiYo3fOACQPqQnExBXyzXRY5RveiH7Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.3.0-canary.6':
resolution: {integrity: sha512-va7/O3oqgWUMIzNLAuQXbKFw6DqTS6S45FneyGWGorDlttq1K11cBYKOQhz3If7Lw/ftrLLOLPdlYZGcciCdeg==}
'@next/swc-linux-x64-musl@15.3.0-canary.8':
resolution: {integrity: sha512-zJyaG6tiYFHhLsge8MULXwOhaWqAj2OLWSL7yjWGLMSm+c5n06S4tfppQiY4E/4fJiqtpo7Ty9s7AjG3TbTWSw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.3.0-canary.6':
resolution: {integrity: sha512-SeUU5mQSIZXGwMxl5Bn94CdWF0R3YIWCxWq9Lpo1KW2B2S6F61WCmjeZnZZ9TOGMi4D7BAP96ptMCNiNOZUi0Q==}
'@next/swc-win32-arm64-msvc@15.3.0-canary.8':
resolution: {integrity: sha512-qhV+9EkjyRlxvS54aC1e+wDqe2JgP2vXCo00VkyVPJqiVXIvpSIR/7DXg7bDuSvxr7t1sufm7P7v4jhUWTmg1w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.3.0-canary.6':
resolution: {integrity: sha512-P+O19Iet4806xsdVFwbVfzxeTRb50xImjK5NT9Ge5gXCSuBw+DdRdJhENijRYa9lS/RnQVrXfII5gLdLy4X3sQ==}
'@next/swc-win32-x64-msvc@15.3.0-canary.8':
resolution: {integrity: sha512-3lE7Qz7XmAswjJ+WczXsiLjdQ9TpXNX9yJ33cEAsaCBP6+5dsmYeyviguvKDza1VbmQXlxRHqSeZvMFr9jXrBQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@next/third-parties@15.3.0-canary.6':
resolution: {integrity: sha512-6M5iSbY21atKOYfw4Kx4xWgPv0aoYKrNrz/3PIgcDROnkgT05LkIJMQecZ06+2r+ff93NuKYqtEz+NvZMHPcXQ==}
'@next/third-parties@15.3.0-canary.8':
resolution: {integrity: sha512-ELJ6bR4AFgsDboH0pC856jauB16U+574uGM6r2JsQz2IM/iI+d3TJvUa6C942sbGNsbnPd4eo5ycORZZV0PS7Q==}
peerDependencies:
next: ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@ -1476,8 +1476,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-next@15.3.0-canary.6:
resolution: {integrity: sha512-NEefwyiCUtXGkpJN5CwCJSYra2+8+YyMYo6TtG59b+jsrZxa9r12emTWYJcWMzNgxDZvRM4xYO6pyXs1ie2FyA==}
eslint-config-next@15.3.0-canary.8:
resolution: {integrity: sha512-3kcG6U4jii1+Vur5XQbUUYZrDHa4pvHa0Me2GdWAa2JhevJuNOzIVmWLHrKlPtULBbVmxWjX09wUesQIkoYb4Q==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@ -1494,8 +1494,8 @@ packages:
eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
eslint-import-resolver-typescript@3.8.6:
resolution: {integrity: sha512-d9UjvYpj/REmUoZvOtDEmayPlwyP4zOwwMBgtC6RtrpZta8u1AIVmxgZBYJIcCKKXwAcLs+DX2yn2LeMaTqKcQ==}
eslint-import-resolver-typescript@3.8.7:
resolution: {integrity: sha512-U7k84gOzrfl09c33qrIbD3TkWTWu3nt3dK5sDajHSekfoLlYGusIwSdPlPzVeA6TFpi0Wpj+ZdBD8hX4hxPoww==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@ -1937,8 +1937,8 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
hookified@1.7.1:
resolution: {integrity: sha512-OXcdHsXeOiD7OJ5zvWj8Oy/6RCdLwntAX+wUrfemNcMGn6sux4xbEHi2QXwqePYhjQ/yvxxq2MvCRirdlHscBw==}
hookified@1.8.1:
resolution: {integrity: sha512-GrO2l93P8xCWBSTBX9l2BxI78VU/MAAYag+pG8curS3aBGy0++ZlxrQ7PdUOUVMbn5BwkGb6+eRrnf43ipnFEA==}
hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
@ -2302,8 +2302,8 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lucide-react@0.479.0:
resolution: {integrity: sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==}
lucide-react@0.481.0:
resolution: {integrity: sha512-NrvUDNFwgLIvHiwTEq9boa5Kiz1KdUT8RJ+wmNijwxdn9U737Fw42c43sRxJTMqhL+ySHpGRVCWpwiF+abrEjw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
@ -2544,8 +2544,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.3.0-canary.6:
resolution: {integrity: sha512-2ud5m+f6q4iCwRGJSCODhJHTlSXi1ueY4tMx38EYp8obLCT1VBpCqLm70ppu0RYU+D79Vkqgf1VaQ+tu3i0nuw==}
next@15.3.0-canary.8:
resolution: {integrity: sha512-RuXoaWX13+VhfK0lD/r8XqcyZFpyER4/KaomW/wZYaEyoiHVEFH6ndpD6m4b9+a60w1bua3CoRn2Ih1419FZ8Q==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -3275,8 +3275,8 @@ packages:
peerDependencies:
stylelint: ^16.0.2
stylelint@16.15.0:
resolution: {integrity: sha512-OK6Rs7EPdcdmjqiDycadZY4fw3f5/TC1X6/tGjnF3OosbwCeNs7nG+79MCAtjEg7ckwqTJTsku08e0Rmaz5nUw==}
stylelint@16.16.0:
resolution: {integrity: sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg==}
engines: {node: '>=18.12.0'}
hasBin: true
@ -3910,53 +3910,53 @@ snapshots:
'@types/react': 19.0.10
react: 19.0.0
'@next/bundle-analyzer@15.3.0-canary.6':
'@next/bundle-analyzer@15.3.0-canary.8':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.3.0-canary.6': {}
'@next/env@15.3.0-canary.8': {}
'@next/eslint-plugin-next@15.3.0-canary.6':
'@next/eslint-plugin-next@15.3.0-canary.8':
dependencies:
fast-glob: 3.3.1
'@next/mdx@15.3.0-canary.6(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.0.10)(react@19.0.0))':
'@next/mdx@15.3.0-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.0.10)(react@19.0.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.0.10)(react@19.0.0)
'@next/swc-darwin-arm64@15.3.0-canary.6':
'@next/swc-darwin-arm64@15.3.0-canary.8':
optional: true
'@next/swc-darwin-x64@15.3.0-canary.6':
'@next/swc-darwin-x64@15.3.0-canary.8':
optional: true
'@next/swc-linux-arm64-gnu@15.3.0-canary.6':
'@next/swc-linux-arm64-gnu@15.3.0-canary.8':
optional: true
'@next/swc-linux-arm64-musl@15.3.0-canary.6':
'@next/swc-linux-arm64-musl@15.3.0-canary.8':
optional: true
'@next/swc-linux-x64-gnu@15.3.0-canary.6':
'@next/swc-linux-x64-gnu@15.3.0-canary.8':
optional: true
'@next/swc-linux-x64-musl@15.3.0-canary.6':
'@next/swc-linux-x64-musl@15.3.0-canary.8':
optional: true
'@next/swc-win32-arm64-msvc@15.3.0-canary.6':
'@next/swc-win32-arm64-msvc@15.3.0-canary.8':
optional: true
'@next/swc-win32-x64-msvc@15.3.0-canary.6':
'@next/swc-win32-x64-msvc@15.3.0-canary.8':
optional: true
'@next/third-parties@15.3.0-canary.6(next@15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
'@next/third-parties@15.3.0-canary.8(next@15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
dependencies:
next: 15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
third-party-capital: 1.0.20
@ -4440,7 +4440,7 @@ snapshots:
cacheable@1.8.9:
dependencies:
hookified: 1.7.1
hookified: 1.8.1
keyv: 5.3.1
call-bind-apply-helpers@1.0.2:
@ -4858,16 +4858,16 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@15.3.0-canary.6(eslint@9.22.0)(typescript@5.8.2):
eslint-config-next@15.3.0-canary.8(eslint@9.22.0)(typescript@5.8.2):
dependencies:
'@next/eslint-plugin-next': 15.3.0-canary.6
'@next/eslint-plugin-next': 15.3.0-canary.8
'@rushstack/eslint-patch': 1.11.0
'@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
'@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2)
eslint: 9.22.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.6(eslint-plugin-import@2.31.0)(eslint@9.22.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0)
eslint-import-resolver-typescript: 3.8.7(eslint-plugin-import@2.31.0)(eslint@9.22.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0)
eslint-plugin-react: 7.37.4(eslint@9.22.0)
eslint-plugin-react-hooks: 5.2.0(eslint@9.22.0)
@ -4890,7 +4890,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.8.6(eslint-plugin-import@2.31.0)(eslint@9.22.0):
eslint-import-resolver-typescript@3.8.7(eslint-plugin-import@2.31.0)(eslint@9.22.0):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
@ -4901,7 +4901,7 @@ snapshots:
stable-hash: 0.0.4
tinyglobby: 0.2.12
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0)
transitivePeerDependencies:
- supports-color
@ -4926,14 +4926,14 @@ snapshots:
- bluebird
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2)
eslint: 9.22.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.6(eslint-plugin-import@2.31.0)(eslint@9.22.0)
eslint-import-resolver-typescript: 3.8.7(eslint-plugin-import@2.31.0)(eslint@9.22.0)
transitivePeerDependencies:
- supports-color
@ -4943,7 +4943,7 @@ snapshots:
gonzales-pe: 4.3.0
lodash: 4.17.21
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@ -4954,7 +4954,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.22.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.6)(eslint@9.22.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.7)(eslint@9.22.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -5238,7 +5238,7 @@ snapshots:
dependencies:
cacheable: 1.8.9
flatted: 3.3.3
hookified: 1.7.1
hookified: 1.8.1
flatted@3.3.3: {}
@ -5267,9 +5267,9 @@ snapshots:
functions-have-names@1.2.3: {}
geist@1.3.1(next@15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
geist@1.3.1(next@15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
dependencies:
next: 15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
get-east-asian-width@1.3.0: {}
@ -5536,7 +5536,7 @@ snapshots:
property-information: 7.0.0
space-separated-tokens: 2.0.2
hookified@1.7.1: {}
hookified@1.8.1: {}
hosted-git-info@7.0.2:
dependencies:
@ -5903,7 +5903,7 @@ snapshots:
lru-cache@10.4.3: {}
lucide-react@0.479.0(react@19.0.0):
lucide-react@0.481.0(react@19.0.0):
dependencies:
react: 19.0.0
@ -6392,9 +6392,9 @@ snapshots:
natural-compare@1.4.0: {}
next@15.3.0-canary.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
next@15.3.0-canary.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.3.0-canary.6
'@next/env': 15.3.0-canary.8
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@ -6404,14 +6404,14 @@ snapshots:
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.3.0-canary.6
'@next/swc-darwin-x64': 15.3.0-canary.6
'@next/swc-linux-arm64-gnu': 15.3.0-canary.6
'@next/swc-linux-arm64-musl': 15.3.0-canary.6
'@next/swc-linux-x64-gnu': 15.3.0-canary.6
'@next/swc-linux-x64-musl': 15.3.0-canary.6
'@next/swc-win32-arm64-msvc': 15.3.0-canary.6
'@next/swc-win32-x64-msvc': 15.3.0-canary.6
'@next/swc-darwin-arm64': 15.3.0-canary.8
'@next/swc-darwin-x64': 15.3.0-canary.8
'@next/swc-linux-arm64-gnu': 15.3.0-canary.8
'@next/swc-linux-arm64-musl': 15.3.0-canary.8
'@next/swc-linux-x64-gnu': 15.3.0-canary.8
'@next/swc-linux-x64-musl': 15.3.0-canary.8
'@next/swc-win32-arm64-msvc': 15.3.0-canary.8
'@next/swc-win32-x64-msvc': 15.3.0-canary.8
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
@ -7277,22 +7277,22 @@ snapshots:
client-only: 0.0.1
react: 19.0.0
stylelint-config-css-modules@4.4.0(stylelint@16.15.0(typescript@5.8.2)):
stylelint-config-css-modules@4.4.0(stylelint@16.16.0(typescript@5.8.2)):
dependencies:
stylelint: 16.15.0(typescript@5.8.2)
stylelint: 16.16.0(typescript@5.8.2)
optionalDependencies:
stylelint-scss: 6.11.1(stylelint@16.15.0(typescript@5.8.2))
stylelint-scss: 6.11.1(stylelint@16.16.0(typescript@5.8.2))
stylelint-config-recommended@15.0.0(stylelint@16.15.0(typescript@5.8.2)):
stylelint-config-recommended@15.0.0(stylelint@16.16.0(typescript@5.8.2)):
dependencies:
stylelint: 16.15.0(typescript@5.8.2)
stylelint: 16.16.0(typescript@5.8.2)
stylelint-config-standard@37.0.0(stylelint@16.15.0(typescript@5.8.2)):
stylelint-config-standard@37.0.0(stylelint@16.16.0(typescript@5.8.2)):
dependencies:
stylelint: 16.15.0(typescript@5.8.2)
stylelint-config-recommended: 15.0.0(stylelint@16.15.0(typescript@5.8.2))
stylelint: 16.16.0(typescript@5.8.2)
stylelint-config-recommended: 15.0.0(stylelint@16.16.0(typescript@5.8.2))
stylelint-scss@6.11.1(stylelint@16.15.0(typescript@5.8.2)):
stylelint-scss@6.11.1(stylelint@16.16.0(typescript@5.8.2)):
dependencies:
css-tree: 3.1.0
is-plain-object: 5.0.0
@ -7302,10 +7302,10 @@ snapshots:
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.0
postcss-value-parser: 4.2.0
stylelint: 16.15.0(typescript@5.8.2)
stylelint: 16.16.0(typescript@5.8.2)
optional: true
stylelint@16.15.0(typescript@5.8.2):
stylelint@16.16.0(typescript@5.8.2):
dependencies:
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
'@csstools/css-tokenizer': 3.0.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB