1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-25 17:55:23 -04:00

offload videos to vercel blob

This commit is contained in:
Jake Jarvis 2025-04-23 09:34:28 -04:00
parent 6c3cd0afe9
commit 430789c25d
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
42 changed files with 189 additions and 551 deletions

2
.nvmrc
View File

@ -1 +1 @@
22.14.0
22.15.0

Binary file not shown.

Binary file not shown.

View File

@ -5,8 +5,6 @@ import Video from "../../components/Video";
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 = createMetadata({
@ -16,7 +14,7 @@ export const metadata = createMetadata({
openGraph: {
videos: [
{
url: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
url: "https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/birthday-pavk1LBK4H6xF8ZWeR0oTcaabGuQ8T.webm",
type: "video/webm",
},
],
@ -32,7 +30,8 @@ const Page = () => {
"@type": "VideoObject",
name: metadata.title as string,
description: metadata.description as string,
contentUrl: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
contentUrl:
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/birthday-pavk1LBK4H6xF8ZWeR0oTcaabGuQ8T.webm",
thumbnailUrl: `${env.NEXT_PUBLIC_BASE_URL}${thumbnail.src}`,
embedUrl: `${env.NEXT_PUBLIC_BASE_URL}/birthday`,
uploadDate: "1996-02-06T00:00:00Z",
@ -42,7 +41,13 @@ const Page = () => {
<PageTitle canonical="/birthday">1996.mov</PageTitle>
<Video src={[webm, mp4]} poster={thumbnail.src} />
<Video
src={[
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/birthday-pavk1LBK4H6xF8ZWeR0oTcaabGuQ8T.webm",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/birthday-EkbYbrKY8reheQ4UPcP22ipzpMZ2MC.mp4",
]}
poster={thumbnail.src}
/>
</>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Binary file not shown.

View File

@ -6,9 +6,6 @@ import Video from "../../components/Video";
import { createMetadata } from "../../lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import webm from "./convention.webm";
import mp4 from "./convention.mp4";
import subtitles from "./subs.en.vtt";
import thumbnail from "./thumbnail.png";
export const metadata = createMetadata({
@ -18,7 +15,7 @@ export const metadata = createMetadata({
openGraph: {
videos: [
{
url: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
url: "https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention-ZTUBLwMcmOE8EJ4tNAhpCli4NAHKcG.webm",
type: "video/webm",
},
],
@ -34,7 +31,8 @@ const Page = () => {
"@type": "VideoObject",
name: metadata.title as string,
description: metadata.description as string,
contentUrl: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
contentUrl:
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention-ZTUBLwMcmOE8EJ4tNAhpCli4NAHKcG.webm",
thumbnailUrl: `${env.NEXT_PUBLIC_BASE_URL}${thumbnail.src}`,
embedUrl: `${env.NEXT_PUBLIC_BASE_URL}/hillary`,
uploadDate: "2016-07-25T00:00:00Z",
@ -44,7 +42,14 @@ const Page = () => {
<PageTitle canonical="/hillary">HRC.mov</PageTitle>
<Video src={[webm, mp4, subtitles]} poster={thumbnail.src} />
<Video
src={[
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention-ZTUBLwMcmOE8EJ4tNAhpCli4NAHKcG.webm",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention-T6klrrArGL0IO4QPaloIiIH164UqUC.mp4",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention.en-uHnecgVCrT9xA8EkzdEaeIwB0rHFC9.vtt",
]}
poster={thumbnail.src}
/>
<p
style={{

View File

@ -1,108 +0,0 @@
WEBVTT
Kind: captions
Language: en
00:00:00.030 --> 00:00:03.210
All right, well let's listen
here to the video at the Democratic
00:00:03.210 --> 00:00:04.880
National Convention on this issue.
00:00:04.880 --> 00:00:08.340
You know, when I came here to New Hampshire
00:00:08.340 --> 00:00:14.269
the first time in this campaign and
heard about the heroin epidemic
00:00:14.269 --> 00:00:16.660
The growing drug problem in our area...
00:00:16.780 --> 00:00:19.604
We're all losing fathers, brothers, sons, mothers...
00:00:19.604 --> 00:00:22.040
The numbers of people who are being affected
00:00:22.040 --> 00:00:27.000
If you have been impacted by your own or
someone else's use of substances, would
00:00:27.000 --> 00:00:28.510
you just raise your hand?
00:00:28.510 --> 00:00:31.470
I didn't even know what was wrong with me until I
00:00:31.470 --> 00:00:35.400
found out that I was I was an addict.
00:00:35.400 --> 00:00:38.480
This problem touches everybody...
00:00:38.480 --> 00:00:41.484
My children suffered, my family suffered...
00:00:41.484 --> 00:00:44.280
Meeting grandmothers raising their grandchildren...
00:00:44.280 --> 00:00:48.800
All of my friends are raising
their grandchildren because of addiction
00:00:48.800 --> 00:00:53.700
I think you're very brave and
very loving to take on this responsibility.
00:00:53.700 --> 00:00:56.720
It is plaguing families.
I don't want to
00:00:56.730 --> 00:01:00.180
go to more funerals. I'm going to one this weekend.
I didn't know if I wanted
00:01:00.180 --> 00:01:04.980
to do better. Addiction had full control
over my heart and soul.
00:01:04.980 --> 00:01:10.880
I wouldn't have necessarily known that if I hadn't been
sitting in a little cafe in Keene
00:01:10.880 --> 00:01:16.140
listening to people tell me about what
was breaking their hearts and what they
00:01:16.140 --> 00:01:19.660
wanted their president to know.
00:01:19.660 --> 00:01:21.299
We need the voice of those who are using, that is
00:01:21.299 --> 00:01:24.480
the way in which we can make an impact
on this disease.
00:01:24.480 --> 00:01:26.860
We can't walk away from these stories.
00:01:26.860 --> 00:01:29.900
These are our children, these are our friends, our neighbors,
00:01:29.900 --> 00:01:34.160
our loved ones. This is not something we can
just brush under the rug and wish it would
00:01:34.160 --> 00:01:39.180
go away. We need to drag it out of the
shadows, we need to hold it up to the light
00:01:39.180 --> 00:01:41.740
Everybody should feel that they are
00:01:41.740 --> 00:01:50.960
valued, they are cared about, and they
have a president who sees them

Binary file not shown.

Binary file not shown.

View File

@ -6,9 +6,6 @@ import Video from "../../components/Video";
import { createMetadata } from "../../lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import mp4 from "./leo.mp4";
import webm from "./leo.webm";
import subtitles from "./subs.en.vtt";
import thumbnail from "./thumbnail.png";
export const metadata = createMetadata({
@ -18,7 +15,7 @@ export const metadata = createMetadata({
openGraph: {
videos: [
{
url: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
url: "https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/leo-uoCXHS9gViyRnQhr8CEGXFvj4VGh5Y.webm",
type: "video/webm",
},
],
@ -34,7 +31,7 @@ const Page = () => {
"@type": "VideoObject",
name: metadata.title as string,
description: metadata.description as string,
contentUrl: `${env.NEXT_PUBLIC_BASE_URL}${webm}`,
contentUrl: "https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/leo-uoCXHS9gViyRnQhr8CEGXFvj4VGh5Y.webm",
thumbnailUrl: `${env.NEXT_PUBLIC_BASE_URL}${thumbnail.src}`,
embedUrl: `${env.NEXT_PUBLIC_BASE_URL}/leo`,
uploadDate: "2007-05-10T00:00:00Z",
@ -44,7 +41,14 @@ const Page = () => {
<PageTitle canonical="/leo">TheLab.mov</PageTitle>
<Video src={[webm, mp4, subtitles]} poster={thumbnail.src} />
<Video
src={[
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/leo-uoCXHS9gViyRnQhr8CEGXFvj4VGh5Y.webm",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/leo-Blp1bsf872vuY05LuSw7fjZBHURWT1.mp4",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/leo.en-TsoyI7XMA10Uaj8EFZV1bQ65At35gz.vtt",
]}
poster={thumbnail.src}
/>
<p
style={{

View File

@ -1,111 +0,0 @@
WEBVTT
00:00:00.000 --> 00:00:05.029
["The Lab" Theme Music]
00:00:05.029 --> 00:00:08.760
Welcome back to "The Lab with Leo," I'm Kate Abraham and now it's time for our
00:00:08.760 --> 00:00:13.000
Facebook app of the day. And in honor of
the lovely Amber wasn't it great to see Amber?
00:00:13.040 --> 00:00:16.470
Oh, it was so nice to see her.
She's looking so tan!
00:00:16.470 --> 00:00:21.140
Toronto weather.
Yeah, really? I think she was at Prince Edward Island for a few days
00:00:21.140 --> 00:00:26.380
for Canada Day.
So she's out there like this, tanning in the ocean. Good for her.
00:00:26.380 --> 00:00:28.060
She knows how to enjoy herself.
[laughs]
00:00:28.060 --> 00:00:30.740
So in honor of Amber, our Facebook app of the day is
00:00:30.740 --> 00:00:35.360
Powncer, or Poncer or Punser
00:00:35.360 --> 00:00:39.000
However you want to say it, I say Powncer.
So there's your Pownce page.
00:00:39.000 --> 00:00:44.520
So what you do is, basically, it literally updates your most recent post on Pownce.
So literally I've just put here
00:00:44.520 --> 00:00:49.200
"Playing with Pownce," then if I just post this, and what will happen is it'll automatically
00:00:49.200 --> 00:00:52.460
I bet you've done this in advance, it'll
automatically go through to my actual
00:00:52.460 --> 00:00:56.420
site on Facebook and as you can see, you can see exactly what I'm doing
00:00:56.420 --> 00:00:59.000
And then can they click that and takes
it to your Pownce page?
Yeah, then you can
00:00:59.000 --> 00:01:02.860
reply to me. Oh that's neat!
Then what'll happen, you can basically send me a reply.
00:01:02.860 --> 00:01:06.460
You know who wrote that? I don't know.
Jake Jarvis wrote that.
00:01:06.479 --> 00:01:10.560
And I know who Jake Jarvis is, he's on my
Facebook friends list.
00:01:10.560 --> 00:01:14.250
He's a young guy, I think he's a high school kid, the son of Jeff Jarvis, who's
00:01:14.250 --> 00:01:18.450
a very well known media critic.
These kids freak me out. Isn't it neat?
00:01:18.450 --> 00:01:22.590
Jake's written some great applications
How old is he?
I don't know, I think 16 or 17.
00:01:22.590 --> 00:01:26.100
That scares me.
He's written a lot, he's one of the best Facebook developers out there.
00:01:26.100 --> 00:01:29.310
Did a nice job with that.
That's fantastic.
Isn't that great? I love it, that's so good.
00:01:29.310 --> 00:01:31.860
But for more details, obviously Facebook
apps
00:01:31.860 --> 00:01:37.080
I said facebook.com/apps.
There's lots of them, there's thousands.
So many on there.
00:01:37.080 --> 00:01:41.100
There's a lot of bad ones but some really good ones.
Well that's what you're here for, to tell us which ones are good.
00:01:41.100 --> 00:01:44.540
Exactly, this was a good one, I like it.
She installs them all so that you can
00:01:44.540 --> 00:01:48.220
tell, you got like a page this long!
You just keep scrolling, scrolling, scrolling...
[laughs]

Binary file not shown.

View File

@ -2,8 +2,6 @@ import Video from "../components/Video";
import Link from "../components/Link";
import type { Metadata } from "next";
import notFoundVideo from "./not-found.mp4";
export const metadata: Metadata = {
title: "Page Not Found",
description: null,
@ -16,7 +14,11 @@ export const metadata: Metadata = {
const Page = () => {
return (
<>
<Video src={notFoundVideo} autoPlay style={{ maxWidth: 480, aspectRatio: "16/11" }} />
<Video
src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/not-found-SAtLyNyc7gVhveYxr6o1ITd9CSXo5X.mp4"
autoPlay
style={{ maxWidth: 480, aspectRatio: "16/11" }}
/>
<div style={{ textAlign: "center", marginTop: "1.5em" }}>
<h1 style={{ margin: "0.5em 0", fontSize: "2.2em", fontWeight: 500, lineHeight: 1 }}>Page Not Found</h1>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -85,7 +85,7 @@ _Previously on the [Cringey Chronicles&trade;](https://web.archive.org/web/20010
<iframe
src="https://jakejarvis.github.io/my-first-website/"
title="My Terrible, Horrible, No Good, Very Bad First Website"
style={{ height: "500px", width: "100%", marginBottom: "-0.4em" }}
style={{ height: "500px", width: "100%", border: "1px solid var(--colors-kinda-light)", marginBottom: "-0.4em" }}
/>
_[November 2001](https://jakejarvis.github.io/my-first-website/) ([view
source](https://github.com/jakejarvis/my-first-website))_

View File

@ -2,8 +2,8 @@
import "server-only";
import { env } from "../../lib/env";
import { graphql } from "@octokit/graphql";
import * as cheerio from "cheerio";
import { graphql } from "@octokit/graphql";
import type { Repository, User } from "@octokit/graphql-schema";
export const getContributions = async (): Promise<

View File

@ -10,8 +10,8 @@ const excludedRoutes = [
// homepage is already included manually
"./",
// other excluded pages
"./license",
"./privacy",
// "./license",
// "./privacy",
];
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {

View File

@ -24,6 +24,7 @@ const Video = ({ src, autoPlay, className, children, ...rest }: VideoProps) => {
preload: "metadata",
controls: true,
})}
crossOrigin="anonymous"
className={clsx(styles.player, className)}
{...rest}
>

View File

@ -22,7 +22,6 @@ export const useMDXComponents = (components: MDXComponents): MDXComponents => {
a: Link,
code: Code,
blockquote: Blockquote,
video: Video,
hr: HorizontalRule,
h1: Heading.H1,
h2: Heading.H2,
@ -35,6 +34,7 @@ export const useMDXComponents = (components: MDXComponents): MDXComponents => {
li: List.ListItem,
// third-party embeds:
Video,
Tweet,
YouTube,
Gist,

View File

@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/no-require-imports, import/no-anonymous-default-export */
import path from "path";
import { visit } from "unist-util-visit";
import * as mdxPlugins from "./lib/helpers/remark-rehype-plugins";
import type { NextConfig } from "next";
@ -18,6 +16,17 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "ijyxfbpcm3itvdly.public.blob.vercel-storage.com",
port: "",
pathname: "/**",
search: "",
},
],
},
outputFileTracingIncludes: {
"/notes/[slug]/opengraph-image": [
"./notes/**/*",
@ -26,23 +35,7 @@ const nextConfig: NextConfig = {
"./node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf",
],
},
outputFileTracingExcludes: {
"*": ["./public/**/*", "**/*.mp4", "**/*.webm", "**/*.vtt"],
},
productionBrowserSourceMaps: true,
webpack: (config) => {
config.module.rules.push({
test: /\.(mp4|webm|vtt)$/i,
type: "asset/resource",
generator: {
// https://github.com/vercel/next.js/blob/4447ea402a50113490103abe14255e95dcc8cf69/packages/next/src/build/webpack-config.ts#L1231
// https://github.com/vercel/next.js/discussions/18852#discussioncomment-10752440
outputPath: path.relative(config.output.path, path.join(process.cwd(), ".next/")),
},
});
return config;
},
experimental: {
reactCompiler: true, // https://react.dev/learn/react-compiler
ppr: "incremental", // https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering#using-partial-prerendering
@ -129,8 +122,11 @@ const nextConfig: NextConfig = {
{ source: "/rss", destination: "/feed.xml", permanent: true },
{ source: "/blog/(.*)", destination: "/notes", permanent: true },
{ source: "/archives/(.*)", destination: "/notes", permanent: true },
{ source: "/resume", destination: "/static/resume.pdf", permanent: false },
{ source: "/resume.pdf", destination: "/static/resume.pdf", permanent: false },
{
source: "/static/daily.pdf",
destination: "https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/daily-C0dPhAyjDegAgsFDXUwOu5WAAgGdqP.pdf",
permanent: false,
},
// WordPress permalinks:
{
@ -166,23 +162,6 @@ const nextPlugins: Array<
mdxPlugins.remarkMdxFrontmatter,
mdxPlugins.remarkGfm,
mdxPlugins.remarkSmartypants,
// workaround for rehype-mdx-import-media not applying to `<video>` tags:
// https://github.com/Chailotl/remark-videos/blob/851c332993210e6f091453f7ed887be24492bcee/index.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => (tree: any) => {
visit(tree, "image", (node) => {
if (node.url.match(/\.(mp4|webm)$/i)) {
node.type = "element";
node.data = {
hName: "video",
hProperties: {
src: node.url,
// TODO: make this even hackier and pass an autoplay option in the alt text or something
},
};
}
});
},
],
rehypePlugins: [
mdxPlugins.rehypeUnwrapImages,

View File

@ -42,7 +42,7 @@ Using either feature, a volunteer starts with a search of the database for the v
Here's one of the instructional videos provided internally to volunteers:
![](./friend-to-friend.mp4)
<Video src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/friend-to-friend-uBJEsdkWEk2h00W7YQWqojhcLLsnvx.mp4" />
...and a few privacy-related questions about the friend-to-friend feature were answered by campaign staff in a separate closed webinar for volunteers this week:
@ -59,10 +59,10 @@ _BERN's API response in Chrome DevTools_
Others have noted that web-based organizing tools like BERN have been used by campaigns at all levels since President Obama's well-oiled, futuristic machine in 2007. This is also true, and I'm a big fan of the trend they started.
But the latter category of databases — like [NationBuilder](https://nationbuilder.com/) and, more notably, [NGP VAN's VoteBuilder](https://act.ngpvan.com/votebuilder) software based on the Obama campaign's inventions and now used by almost all Democratic campaigns across the United States — are secured and strictly guarded. Volunteer accounts need to be created and approved by paid campaign organizers and are locked down to provide the bare minimum amount of information necessary for one to canvass or phone bank a shortlist of voters. Every single click is also recorded in a [detailed log](/static/bernie-sanders-bern-app-data/sanders-campaign-audit.pdf) down to the millisecond. (This is how [Bernie's organizers got busted](https://time.com/4155185/bernie-sanders-hillary-clinton-data/) snooping around Hillary's VoteBuilder data last cycle, by the way.)
But the latter category of databases — like [NationBuilder](https://nationbuilder.com/) and, more notably, [NGP VAN's VoteBuilder](https://act.ngpvan.com/votebuilder) software based on the Obama campaign's inventions and now used by almost all Democratic campaigns across the United States — are secured and strictly guarded. Volunteer accounts need to be created and approved by paid campaign organizers and are locked down to provide the bare minimum amount of information necessary for one to canvass or phone bank a shortlist of voters. Every single click is also recorded in a [detailed log](https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/sanders-campaign-audit-OhdPk62Ift9VDPbJb9EYBArz1NUXIm.pdf) down to the millisecond. (This is how [Bernie's organizers got busted](https://time.com/4155185/bernie-sanders-hillary-clinton-data/) snooping around Hillary's VoteBuilder data last cycle, by the way.)
![NGP VAN's audit of the Sanders campaign's VoteBuilder activity](./votebuilder-audit.png)
_[NGP VAN's audit of the Sanders campaign's VoteBuilder activity](/static/bernie-sanders-bern-app-data/sanders-campaign-audit.pdf)_
_[NGP VAN's audit of the Sanders campaign's VoteBuilder activity](https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/sanders-campaign-audit-OhdPk62Ift9VDPbJb9EYBArz1NUXIm.pdf)_
BERN is taking this to an unprecedented level. Allowing anybody on the internet to sign up and add others' personal information to the campaign's database without their knowledge is troubling, especially when you consider the gamified "points" system they've added as an incentive to report as much information on as many people as possible.

View File

@ -13,11 +13,15 @@ tags:
image: ./covid19dashboards.png
---
import Link from "../../components/Link";
export const OctocatLink = ({ repo }) => {
return (
<Link href={`https://github.com/${repo}`} plain style={{ margin: "0 0.4em" }}>
<a
href={`https://github.com/${repo}`}
title={`${repo} on GitHub`}
target="_blank"
rel="noopener noreferrer"
style={{ margin: "0 0.4em" }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
strokeWidth="0"
@ -33,7 +37,7 @@ export const OctocatLink = ({ repo }) => {
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
</Link>
</a>
);
};

View File

@ -32,12 +32,12 @@ I've written a simple implementation below, which...
...meaning that any CSS selectors beginning with `body.dark` or `body.light` will only apply when the respective mode is active. A good place to start is by separating any color rules — your background, text, links, etc. — into a different section of your CSS. Using [SASS or SCSS](https://sass-lang.com/) makes this a whole lot [easier with nesting](https://sass-lang.com/guide#topic-3) but is not required; this was written with a [KISS](https://www.youtube-nocookie.com/embed/O58A7MJfOwU?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3) mentality.
<iframe
src="/static/dark-mode/example.html"
src="https://jakejarvis.github.io/dark-mode-example/"
title="Dark Mode Example"
style={{ height: "190px", width: "100%" }}
style={{ height: "190px", width: "100%", border: "1px solid var(--colors-kinda-light)" }}
></iframe>
A _very_ barebones example is embedded above ([view the source here](https://github.com/jakejarvis/dark-mode-example), or [open in a new window](/static/dark-mode/example.html) if your browser is blocking the frame) and you can try it out on this site by clicking the 💡 lightbulb in the upper right corner of this page. You'll notice that the dark theme sticks when refreshing this page, navigating between other pages, or if you were to return to this example weeks from now.
A _very_ barebones example is embedded above ([view the source here](https://github.com/jakejarvis/dark-mode-example), or [open in a new window](https://jakejarvis.github.io/dark-mode-example/) if your browser is blocking the frame) and you can try it out on this site by clicking the 💡 lightbulb in the upper right corner of this page. You'll notice that the dark theme sticks when refreshing this page, navigating between other pages, or if you were to return to this example weeks from now.
---

Binary file not shown.

View File

@ -31,7 +31,7 @@ Decisions made by the top folks at Dropbox gave me an increasingly sour taste in
- Explicitly [dropping support for symlinking](https://news.ycombinator.com/item?id=20844363) (aka making aliases to) files outside of the literal `~/Dropbox` folder, which was incredibly helpful for nerds — once their main audience and biggest cheerleaders — with things like [dotfiles](https://github.com/jakejarvis/dotfiles) and Git repositories.
- ...and as a bonus, making the process of canceling Dropbox Pro incredibly convoluted, annoying, and sketchy. Here's a video demonstration via [Justin Dunham](https://twitter.com/jwyattd):
![](./cancel.mp4)
<Video src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/cancel-H27HQVT7KMOQZcWlaR9UDi7q6BQNIr.mp4" />
## Seeking an alternative...

View File

@ -49,4 +49,9 @@ As [Bill Maher](https://medium.com/u/cdc04a9799f6) (an avid Bernie supporter) [s
**Update:** The campaign has included my snowy New Hampshire interaction with her in one of the DNC convention videos! See a few short seconds of my joy at 1:24.
![](./convention.mp4)
<Video
src={[
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention-T6klrrArGL0IO4QPaloIiIH164UqUC.mp4",
"https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/convention.en-uHnecgVCrT9xA8EkzdEaeIwB0rHFC9.vtt",
]}
/>

View File

@ -27,7 +27,7 @@ I'm a _huge_ sucker for Kate McKinnon's spot-on impression of Warren on Saturday
Although the designer who selected this GIF likely had _thousands_ of choices when searching "[Bernie finger wagging GIF](https://www.google.com/search?q=Bernie+finger+wagging+GIF&tbm=isch&tbs=itp:animated)," the text beside it is well-written and funny — even though we both know putting a page at [berniesanders.com/zxcliaosid](https://berniesanders.com/zxcliaosid/) probably won't be a top priority of a President Sanders.
![](./sanders.mp4)
<Video src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/sanders-C68KSJMWFGknQgKYZHFaDH55ipi3hE.mp4" />
## 3. Joe Biden — [joebiden.com](https://joebiden.com/asdfasdf404)
@ -39,13 +39,13 @@ Uncle Joe has a nice and simple 404 page. I like it, along with the Ray-Bans and
A ballsy move, considering Beto's infamous [DUI arrest](https://www.politifact.com/texas/statements/2019/mar/14/club-growth/beto-orourke-arrested-dwi-flee-scene/) in the '90s — but still a clever ask for a donation and a great use of a GIF, even if it's left over from his Senate campaign.
![](./orourke.mp4)
<Video src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/orourke-oacYVeAX9oRTetK5z1N2MHqqvfRhos.mp4" />
## 5. Kamala Harris — [kamalaharris.org](https://kamalaharris.org/asdfasdf404)
Another clean and simple page with a top-notch GIF. It injected some emotion into visiting [kamalaharris.com/alskdjf](https://kamalaharris.com/alskdjf).
![](./harris.mp4)
<Video src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/harris-WH3JQm4pcB8NHGnK0hGPEp4olLCZfc.mp4" />
## 6. Pete Buttigeg — [peteforamerica.com](https://peteforamerica.com/asdfasdf404/)

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.4",
"@next/mdx": "15.4.0-canary.4",
"@next/bundle-analyzer": "15.4.0-canary.5",
"@next/mdx": "15.4.0-canary.5",
"@octokit/graphql": "^8.2.2",
"@octokit/graphql-schema": "^15.26.0",
"@t3-oss/env-nextjs": "^0.12.0",
@ -40,7 +40,7 @@
"geist": "^1.3.1",
"html-entities": "^2.6.0",
"lucide-react": "0.503.0",
"next": "15.4.0-canary.4",
"next": "15.4.0-canary.5",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"react": "19.1.0",
@ -49,7 +49,7 @@
"react-dom": "19.1.0",
"react-innertext": "^1.1.5",
"react-is": "19.1.0",
"react-lite-youtube-embed": "^2.5.0",
"react-lite-youtube-embed": "^2.5.1",
"react-schemaorg": "^2.0.0",
"react-textarea-autosize": "^8.5.9",
"react-timeago": "^8.2.0",
@ -70,7 +70,6 @@
"server-only": "0.0.1",
"shiki": "^3.3.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"valibot": "^1.0.0"
},
"devDependencies": {
@ -86,7 +85,7 @@
"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.4",
"eslint-config-next": "15.4.0-canary.5",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.31.0",
@ -100,7 +99,7 @@
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"schema-dts": "^1.1.5",
"stylelint": "^16.18.0",
"stylelint": "^16.19.0",
"stylelint-config-css-modules": "^4.4.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-prettier": "^5.0.3",

195
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.4
version: 15.4.0-canary.4
specifier: 15.4.0-canary.5
version: 15.4.0-canary.5
'@next/mdx':
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))
specifier: 15.4.0-canary.5
version: 15.4.0-canary.5(@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.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)
version: 1.5.0(next@15.4.0-canary.5(@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,7 +67,7 @@ importers:
version: 4.2.2
geist:
specifier: ^1.3.1
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))
version: 1.3.1(next@15.4.0-canary.5(@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
@ -75,8 +75,8 @@ importers:
specifier: 0.503.0
version: 0.503.0(react@19.1.0)
next:
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)
specifier: 15.4.0-canary.5
version: 15.4.0-canary.5(@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
@ -102,8 +102,8 @@ importers:
specifier: 19.1.0
version: 19.1.0
react-lite-youtube-embed:
specifier: ^2.5.0
version: 2.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^2.5.1
version: 2.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-schemaorg:
specifier: ^2.0.0
version: 2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.3)
@ -164,9 +164,6 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
valibot:
specifier: ^1.0.0
version: 1.0.0(typescript@5.8.3)
@ -208,8 +205,8 @@ importers:
specifier: ^9.25.1
version: 9.25.1
eslint-config-next:
specifier: 15.4.0-canary.4
version: 15.4.0-canary.4(eslint@9.25.1)(typescript@5.8.3)
specifier: 15.4.0-canary.5
version: 15.4.0-canary.5(eslint@9.25.1)(typescript@5.8.3)
eslint-config-prettier:
specifier: ^10.1.2
version: 10.1.2(eslint@9.25.1)
@ -250,17 +247,17 @@ importers:
specifier: ^1.1.5
version: 1.1.5
stylelint:
specifier: ^16.18.0
version: 16.18.0(typescript@5.8.3)
specifier: ^16.19.0
version: 16.19.0(typescript@5.8.3)
stylelint-config-css-modules:
specifier: ^4.4.0
version: 4.4.0(stylelint@16.18.0(typescript@5.8.3))
version: 4.4.0(stylelint@16.19.0(typescript@5.8.3))
stylelint-config-standard:
specifier: ^38.0.0
version: 38.0.0(stylelint@16.18.0(typescript@5.8.3))
version: 38.0.0(stylelint@16.19.0(typescript@5.8.3))
stylelint-prettier:
specifier: ^5.0.3
version: 5.0.3(prettier@3.5.3)(stylelint@16.18.0(typescript@5.8.3))
version: 5.0.3(prettier@3.5.3)(stylelint@16.19.0(typescript@5.8.3))
typescript:
specifier: 5.8.3
version: 5.8.3
@ -668,17 +665,17 @@ packages:
'@napi-rs/wasm-runtime@0.2.9':
resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==}
'@next/bundle-analyzer@15.4.0-canary.4':
resolution: {integrity: sha512-9wLOrH/gMKUit6MBqrzCTtJbXLPXOcLOAH0Jp2vMbx4rnOW+1GBtqvdeZk9NVEVmQ9BVKySrDtkCIsxGWoJoNw==}
'@next/bundle-analyzer@15.4.0-canary.5':
resolution: {integrity: sha512-LZBEVgBqsa34Bplnk+pvYgQwfzVYsktTTiVAlNwbxrwtixjs6pv0V90bkuO/iYXbfx4BTnQy6GDJwOzf1JRXdg==}
'@next/env@15.4.0-canary.4':
resolution: {integrity: sha512-bAQK2bwWe5qek16e9cu0r0wlmK8K07nFTPZuBVqyuHAAPYLq44mNG8L0KZ/wV5el6u9ctsZpbfF/U7gC5jGN4Q==}
'@next/env@15.4.0-canary.5':
resolution: {integrity: sha512-4z6OcPCFze1RKmQ0LraHZ5EhA4UHvmqvzvlrAxcfT9JUDQiRZgbWDGn8CrEAG32gtIDG8jyA9/i4YprNbam1Uw==}
'@next/eslint-plugin-next@15.4.0-canary.4':
resolution: {integrity: sha512-cnFwPJTk2OED7ZGtSG70hoGqtRVPgGvnQ86FYghKOA9JLtEWyDwBJWwba+7R6B12lfT+aDtLNfWhhQWNCzDiRw==}
'@next/eslint-plugin-next@15.4.0-canary.5':
resolution: {integrity: sha512-oQnWcKD1HsJTEOiORXXnjujfUjCdMuQsqQ9JbR4EGHZ362HFiX7x0zEAG5Yiwd4VsSguOAJU0MTi+0uXgqcmjg==}
'@next/mdx@15.4.0-canary.4':
resolution: {integrity: sha512-Alr0TgvJNRxa3lXU5hGXjqrfBllvf3g7gJhozZrlA3CyC+vqgSVt1pBzppUmedjmnvZJ2m5olp3fxQuCds9pWA==}
'@next/mdx@15.4.0-canary.5':
resolution: {integrity: sha512-/YTXTIX0jJrX8/XguyU3uvBZlLv2a2s5SfG3/UvIVZ/xVmqO8vQtxjfhZz3EEAqDiejPDBvRZxGvRTW+XdL93g==}
peerDependencies:
'@mdx-js/loader': '>=0.15.0'
'@mdx-js/react': '>=0.15.0'
@ -688,50 +685,50 @@ packages:
'@mdx-js/react':
optional: true
'@next/swc-darwin-arm64@15.4.0-canary.4':
resolution: {integrity: sha512-98K8VoRkzmg+a68SA8bJlaEKUdv1T+k5+NxTOn0OHUvVFAK3IB44v7GGfsrHX/Sauh+C8AOa4eD3RM/BrqZvAA==}
'@next/swc-darwin-arm64@15.4.0-canary.5':
resolution: {integrity: sha512-JvgdecekiZP9u1CrLt/why7ZhEfq5Q81eXhqdt2GLgQ9orYhl1mPLQRyKgoK2scpYB22vkis32v9to3CEQ2tpA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.4.0-canary.4':
resolution: {integrity: sha512-LdrgqdqIZR2PtQkxb4w+DW4DSgHsj5XYQt/MmepYu5IJ313a2FC7RLP21rIotLbloprnunrIW8t+S1EXM85Bgw==}
'@next/swc-darwin-x64@15.4.0-canary.5':
resolution: {integrity: sha512-0l81meyl5mmEu+seBJ+EO4tkHAarRZHo9XmMhBaSEyRmq2GVh1snGg+b5/05FDgsWcdPFcP5CrnN49Qmhwkm+Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.4.0-canary.4':
resolution: {integrity: sha512-ILQIt/nz8XeoVugHYuxDAiz+YqOkzUfYaqBG+kZGIuVtndQTCKAvUu+ZPIqav+4lsZNw9zLlw9w0UgryraL/oA==}
'@next/swc-linux-arm64-gnu@15.4.0-canary.5':
resolution: {integrity: sha512-YA0ENPjHv7SeiZ3thEKHxKY1tMP8u+tXgpignt8guAPKEXIgXWKe970EbByRwvu3DYRPVf4MZtS038m164UrNA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.4.0-canary.4':
resolution: {integrity: sha512-Obp+gG8ZnC4syrvn58ZdeT0ZXStTfgS/spLLQprhgCcblPLyxKDZm6i5T5eDhDHTFOdC+ThqVxVSaw7kU20uyA==}
'@next/swc-linux-arm64-musl@15.4.0-canary.5':
resolution: {integrity: sha512-ZbdHlsKy2FlCt2Kwovfy3ziJO+hTtNnjW4Kh3TiUe0PrHrUThk6OJ2kRSwOIbyJY1Q93vKc4KVkmwhjjzym+VQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.4.0-canary.4':
resolution: {integrity: sha512-AsTTKgxXEJ3EwS3SSZhqUmJf4Hb5trazeJ+Yy9VO+nPsZWU6mdnRbIloOUV+7Gc/Hn3FOJQuZvAYCCApzVyeRA==}
'@next/swc-linux-x64-gnu@15.4.0-canary.5':
resolution: {integrity: sha512-uU2iEFTqvtuVaYX0D5URsEOMpvXR0AakqPfhvIVBjgSB9WSow8NVcUxhFzQzPwJEzOoWekoQaHuMNiMk8bwQSA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.4.0-canary.4':
resolution: {integrity: sha512-75KSJDWe3irbsja2GZjx9euV+SWfbXL7G/o8vdE3mU3iG1bWNIPeJDJlnDSYeqMb0NrqYT1VfqOy0TCQOrWa5w==}
'@next/swc-linux-x64-musl@15.4.0-canary.5':
resolution: {integrity: sha512-FAh6fHKSxzHh+LuLWzEeA3oMWFeNkgGYH6eJGoiKjlfavbr3nUr581Gafja2sjD2QyP7fQ2BX3f1RNuH4Vl/Sw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.4.0-canary.4':
resolution: {integrity: sha512-k1HDU4ipztIiHM0iHqxHz4sVtiHumAVCYfMhOOYdB3WcuSLZfNUnSRYzjZ0oT7+O/7a/sXsM9PIRV47OaRMuCA==}
'@next/swc-win32-arm64-msvc@15.4.0-canary.5':
resolution: {integrity: sha512-Bk9IFKOKjN6nj3wx05sLfMfMQqk7+xT5FaFtOFD+nN0iqwTiW1nGXBvyHJwCDty6dSIGq8RDqVz/dEFZbw0Egg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.4.0-canary.4':
resolution: {integrity: sha512-1C+r+7C+hrLNuN6KCx9lALkq4d9w6iw75V4bJGyIMj/ZhNoRJzAxALHP99peG5Qe6othTPC/ev3oCcnq2FZwsw==}
'@next/swc-win32-x64-msvc@15.4.0-canary.5':
resolution: {integrity: sha512-I8e5tOYWh0YTNCarU24Qfy9PBp1pv4iAp5KMcrsGv2BWXmqgN7PacBKmM/B0BUqDRFr5H5JJHOZgV08SnixnQg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -1629,8 +1626,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-next@15.4.0-canary.4:
resolution: {integrity: sha512-H0+7XKwmESYF+SCBEINypc7/j+F0lOxkj6ZridB7MRFk90P98KRmz3dmTn/TLbHm3P9rat83ay95W76MXMB24g==}
eslint-config-next@15.4.0-canary.5:
resolution: {integrity: sha512-QWVwd5KfoEVNMXoOjt4LkCMcoBou4qAEz11Zt5/eNWzi9++Nu3yMNaZvOxdFZ35N600KLY8QCbA4YcHLobz5AQ==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@ -2411,6 +2408,9 @@ packages:
known-css-properties@0.35.0:
resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==}
known-css-properties@0.36.0:
resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==}
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@ -2727,8 +2727,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.4.0-canary.4:
resolution: {integrity: sha512-ZdzHJLXVyzbWHHsQuCk9vdTcxcpgVVZWSZ7pjW7h6sBUXUyQnKAjZkdxixcD0Ge0OzWb1b6sfINqTsDQqE+Qzw==}
next@15.4.0-canary.5:
resolution: {integrity: sha512-Qpv129bewAf84lzjvGNbvjGIgkWVe/wPi4B615ltNSEsvb4K+sroa24WVggdDot1rnmfs+uSirvUhETPv1h0gA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -3041,8 +3041,8 @@ packages:
react-is@19.1.0:
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
react-lite-youtube-embed@2.5.0:
resolution: {integrity: sha512-EWmLRrZCl4NW7rLsPRqSkQhI/ME/GP+XT7vd6ZyiMPK4RcnkH7BHla3QajgreU+k1jlmuERcnw+VK38Wnk2B9g==}
react-lite-youtube-embed@2.5.1:
resolution: {integrity: sha512-qH/0RumywPtzSx5SmWX/cUGvB3mSB7zMx3VrDe1UwyCEQ0SX785xnjRAodEel1pu3A3EhZyzSjwmLpfcaUN6KQ==}
peerDependencies:
react: '>=18.2.0'
react-dom: '>=18.2.0'
@ -3502,8 +3502,8 @@ packages:
peerDependencies:
stylelint: ^16.0.2
stylelint@16.18.0:
resolution: {integrity: sha512-OXb68qzesv7J70BSbFwfK3yTVLEVXiQ/ro6wUE4UrSbKCMjLLA02S8Qq3LC01DxKyVjk7z8xh35aB4JzO3/sNA==}
stylelint@16.19.0:
resolution: {integrity: sha512-BJzc5mo/ez0H/ZSl3UbxGdkK/s0kFGsF5/k6IGu4z8wJ1qp49WrOS9RxswvcN6HMirt0g/iiJqOwLHTbWv49IQ==}
engines: {node: '>=18.12.0'}
hasBin: true
@ -4298,48 +4298,48 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next/bundle-analyzer@15.4.0-canary.4':
'@next/bundle-analyzer@15.4.0-canary.5':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.4.0-canary.4': {}
'@next/env@15.4.0-canary.5': {}
'@next/eslint-plugin-next@15.4.0-canary.4':
'@next/eslint-plugin-next@15.4.0-canary.5':
dependencies:
fast-glob: 3.3.1
'@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))':
'@next/mdx@15.4.0-canary.5(@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.4':
'@next/swc-darwin-arm64@15.4.0-canary.5':
optional: true
'@next/swc-darwin-x64@15.4.0-canary.4':
'@next/swc-darwin-x64@15.4.0-canary.5':
optional: true
'@next/swc-linux-arm64-gnu@15.4.0-canary.4':
'@next/swc-linux-arm64-gnu@15.4.0-canary.5':
optional: true
'@next/swc-linux-arm64-musl@15.4.0-canary.4':
'@next/swc-linux-arm64-musl@15.4.0-canary.5':
optional: true
'@next/swc-linux-x64-gnu@15.4.0-canary.4':
'@next/swc-linux-x64-gnu@15.4.0-canary.5':
optional: true
'@next/swc-linux-x64-musl@15.4.0-canary.4':
'@next/swc-linux-x64-musl@15.4.0-canary.5':
optional: true
'@next/swc-win32-arm64-msvc@15.4.0-canary.4':
'@next/swc-win32-arm64-msvc@15.4.0-canary.5':
optional: true
'@next/swc-win32-x64-msvc@15.4.0-canary.4':
'@next/swc-win32-x64-msvc@15.4.0-canary.5':
optional: true
'@nodelib/fs.scandir@2.1.5':
@ -4723,9 +4723,9 @@ snapshots:
dependencies:
crypto-js: 4.2.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)':
'@vercel/analytics@1.5.0(next@15.4.0-canary.5(@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.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)
next: 15.4.0-canary.5(@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: {}
@ -5318,9 +5318,9 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@15.4.0-canary.4(eslint@9.25.1)(typescript@5.8.3):
eslint-config-next@15.4.0-canary.5(eslint@9.25.1)(typescript@5.8.3):
dependencies:
'@next/eslint-plugin-next': 15.4.0-canary.4
'@next/eslint-plugin-next': 15.4.0-canary.5
'@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)
@ -5739,9 +5739,9 @@ snapshots:
functions-have-names@1.2.3: {}
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)):
geist@1.3.1(next@15.4.0-canary.5(@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.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)
next: 15.4.0-canary.5(@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: {}
@ -6286,7 +6286,10 @@ snapshots:
kleur@4.1.5: {}
known-css-properties@0.35.0: {}
known-css-properties@0.35.0:
optional: true
known-css-properties@0.36.0: {}
language-subtag-registry@0.3.23: {}
@ -6883,9 +6886,9 @@ snapshots:
natural-compare@1.4.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):
next@15.4.0-canary.5(@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.4
'@next/env': 15.4.0-canary.5
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@ -6895,14 +6898,14 @@ 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.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
'@next/swc-darwin-arm64': 15.4.0-canary.5
'@next/swc-darwin-x64': 15.4.0-canary.5
'@next/swc-linux-arm64-gnu': 15.4.0-canary.5
'@next/swc-linux-arm64-musl': 15.4.0-canary.5
'@next/swc-linux-x64-gnu': 15.4.0-canary.5
'@next/swc-linux-x64-musl': 15.4.0-canary.5
'@next/swc-win32-arm64-msvc': 15.4.0-canary.5
'@next/swc-win32-x64-msvc': 15.4.0-canary.5
babel-plugin-react-compiler: 19.0.0-beta-af1b7da-20250417
sharp: 0.34.1
transitivePeerDependencies:
@ -7213,7 +7216,7 @@ snapshots:
react-is@19.1.0: {}
react-lite-youtube-embed@2.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
react-lite-youtube-embed@2.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
@ -7833,28 +7836,28 @@ snapshots:
optionalDependencies:
'@babel/core': 7.26.10
stylelint-config-css-modules@4.4.0(stylelint@16.18.0(typescript@5.8.3)):
stylelint-config-css-modules@4.4.0(stylelint@16.19.0(typescript@5.8.3)):
dependencies:
stylelint: 16.18.0(typescript@5.8.3)
stylelint: 16.19.0(typescript@5.8.3)
optionalDependencies:
stylelint-scss: 6.11.1(stylelint@16.18.0(typescript@5.8.3))
stylelint-scss: 6.11.1(stylelint@16.19.0(typescript@5.8.3))
stylelint-config-recommended@16.0.0(stylelint@16.18.0(typescript@5.8.3)):
stylelint-config-recommended@16.0.0(stylelint@16.19.0(typescript@5.8.3)):
dependencies:
stylelint: 16.18.0(typescript@5.8.3)
stylelint: 16.19.0(typescript@5.8.3)
stylelint-config-standard@38.0.0(stylelint@16.18.0(typescript@5.8.3)):
stylelint-config-standard@38.0.0(stylelint@16.19.0(typescript@5.8.3)):
dependencies:
stylelint: 16.18.0(typescript@5.8.3)
stylelint-config-recommended: 16.0.0(stylelint@16.18.0(typescript@5.8.3))
stylelint: 16.19.0(typescript@5.8.3)
stylelint-config-recommended: 16.0.0(stylelint@16.19.0(typescript@5.8.3))
stylelint-prettier@5.0.3(prettier@3.5.3)(stylelint@16.18.0(typescript@5.8.3)):
stylelint-prettier@5.0.3(prettier@3.5.3)(stylelint@16.19.0(typescript@5.8.3)):
dependencies:
prettier: 3.5.3
prettier-linter-helpers: 1.0.0
stylelint: 16.18.0(typescript@5.8.3)
stylelint: 16.19.0(typescript@5.8.3)
stylelint-scss@6.11.1(stylelint@16.18.0(typescript@5.8.3)):
stylelint-scss@6.11.1(stylelint@16.19.0(typescript@5.8.3)):
dependencies:
css-tree: 3.1.0
is-plain-object: 5.0.0
@ -7864,10 +7867,10 @@ snapshots:
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.0
postcss-value-parser: 4.2.0
stylelint: 16.18.0(typescript@5.8.3)
stylelint: 16.19.0(typescript@5.8.3)
optional: true
stylelint@16.18.0(typescript@5.8.3):
stylelint@16.19.0(typescript@5.8.3):
dependencies:
'@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
'@csstools/css-tokenizer': 3.0.3
@ -7890,7 +7893,7 @@ snapshots:
ignore: 7.0.3
imurmurhash: 0.1.4
is-plain-object: 5.0.0
known-css-properties: 0.35.0
known-css-properties: 0.36.0
mathml-tag-names: 2.1.3
meow: 13.2.0
micromatch: 4.0.8

Binary file not shown.

View File

@ -1,131 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Welcome to the dark side 🌓</title>
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
/* non-color rules that apply globally */
body {
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
}
a {
text-decoration: none;
}
h1 {
font-size: 1.8em;
}
.dark-mode-toggle {
cursor: pointer;
padding: 1em;
/* hide toggle until we're sure user has JS enabled */
visibility: hidden;
}
/* theme-specific rules -- you probably only want color-related stuff here. */
/* SCSS makes this a whole lot easier by allowing nesting, but is not required. */
body.light {
background-color: #fff;
color: #222;
}
body.light a {
color: #06f;
}
body.dark {
background-color: #222;
color: #fff;
}
body.dark a {
color: #fe0;
}
</style>
</head>
<body class="light">
<h1>Welcome to the dark side 🌓</h1>
<button class="dark-mode-toggle">💡 Click to see the light... or not.</button>
<p><a href="https://github.com/jakejarvis/dark-mode-example" target="_blank" rel="noopener">View the source code</a> or <a href="https://jarv.is/notes/dark-mode/" target="_blank" rel="noopener">read the post</a>.</p>
<script>
/*! Dark mode switcheroo | MIT License | jrvs.io/darkmode */
(function () {
var classes = window.document.body.classList;
var storage = sessionStorage || localStorage; // modified for example
// check for preset `dark_mode_pref` preference in local storage
var pref_key = 'theme_example-for-dark-mode-post'; // modified for example
var pref = storage.getItem(pref_key);
// change CSS via these <body> classes:
var dark = 'dark';
var light = 'light';
// which class is <body> set to initially?
var default_theme = light;
// use an element with class `dark-mode-toggle` to trigger swap when clicked
var toggle = window.document.querySelector('.dark-mode-toggle');
// keep track of current state no matter how we got there
var active = (default_theme === dark);
// receives a class name and switches <body> to it
var activateTheme = function (theme) {
classes.remove(dark, light);
classes.add(theme);
active = (theme === dark);
};
// if user already explicitly toggled in the past, restore their preference
if (pref === dark) activateTheme(dark);
if (pref === light) activateTheme(light);
// user has never clicked the button, so go by their OS preference until/if they do so
if (!pref) {
// returns media query selector syntax
var prefers = function (theme) {
return '(prefers-color-scheme: ' + theme + ')';
};
// check for OS dark/light mode preference and switch accordingly
// default to `default_theme` set above if unsupported
if (window.matchMedia(prefers(dark)).matches)
activateTheme(dark);
else if (window.matchMedia(prefers(light)).matches)
activateTheme(light);
else
activateTheme(default_theme);
// real-time switching if supported by OS/browser
window.matchMedia(prefers(dark)).addListener(function (e) { if (e.matches) activateTheme(dark); });
window.matchMedia(prefers(light)).addListener(function (e) { if (e.matches) activateTheme(light); });
}
// don't freak out if page happens not to have a toggle
if (toggle) {
// toggle re-appears now that we know user has JS enabled
toggle.style.visibility = 'visible';
// handle toggle click
toggle.addEventListener('click', function () {
// switch to the opposite theme & save preference in local storage
if (active) {
activateTheme(light);
storage.setItem(pref_key, light);
} else {
activateTheme(dark);
storage.setItem(pref_key, dark);
}
}, true);
}
})();
</script>
</body>
</html>

Binary file not shown.

View File

@ -19,6 +19,6 @@
}
]
},
"include": ["next-env.d.ts", "webpack.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

19
webpack.d.ts vendored
View File

@ -1,19 +0,0 @@
/// <reference types="webpack/module" />
// see nextConfig.webpack in next.config.ts for non-standard "asset/resource" module rules.
// https://webpack.js.org/guides/typescript/#importing-other-assets
declare module "*.mp4" {
const src: string;
export default src;
}
declare module "*.webm" {
const src: string;
export default src;
}
declare module "*.vtt" {
const src: string;
export default src;
}