1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-03 13:06:37 -04:00

Migrate to app router (#2254)

This commit is contained in:
2025-02-07 11:33:38 -05:00
committed by GitHub
parent e97613dda5
commit 8aabb4a66f
179 changed files with 4095 additions and 4951 deletions

96
app/api/contact/route.ts Normal file
View File

@ -0,0 +1,96 @@
import nodemailer from "nodemailer";
import fetcher from "../../../lib/helpers/fetcher";
import config from "../../../lib/config";
import { headers } from "next/headers";
import { NextResponse, NextRequest } from "next/server";
export async function POST(req: NextRequest): Promise<
NextResponse<{
success?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error?: any;
} | null>
> {
try {
// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
const data = await req.formData();
const headersList = await headers();
// these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion.
if (!data.get("name") || !data.get("email") || !data.get("message")) {
// all fields are required
throw new Error("missing_data");
}
if (
!data.get("cf-turnstile-response") ||
!(await validateCaptcha(
data.get("cf-turnstile-response"),
headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || ""
))
) {
// either the captcha is wrong or completely missing
throw new Error("invalid_captcha");
}
// throw an internal error, not user's fault
if (!(await sendMessage(data))) {
throw new Error("nodemailer_error");
}
// success! let the client know
return NextResponse.json({ success: true }, { status: 201 });
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
) {
return NextResponse.json({ error: error.message ?? "Bad request." }, { status: 400 });
}
}
const validateCaptcha = async (formResponse: unknown, ip: string): Promise<unknown> => {
const response = await fetcher("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
// fallback to dummy secret for testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
response: formResponse,
remoteip: ip,
}),
});
return response?.success;
};
const sendMessage = async (data: FormData): Promise<boolean> => {
try {
const transporter = nodemailer.createTransport({
// https://resend.com/docs/send-with-nodemailer-smtp
host: "smtp.resend.com",
secure: true,
port: 465,
auth: {
user: "resend",
pass: process.env.RESEND_API_KEY,
},
});
await transporter.sendMail({
from: `${data.get("name")} <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
sender: `nodemailer <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
replyTo: `${data.get("name")} <${data.get("email")}>`,
to: `<${config.authorEmail}>`,
subject: `[${config.siteDomain}] Contact Form Submission`,
// TODO: add markdown parsing as promised on the form.
text: `${data.get("message")}`,
});
} catch (error) {
console.error(error);
return false;
}
return true;
};

31
app/api/count/route.ts Normal file
View File

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma";
import type { PageStats } from "../../../types";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function GET(req: NextRequest): Promise<NextResponse<PageStats>> {
const slug = req.nextUrl.searchParams.get("slug");
// extremely basic input validation.
// TODO: actually check if the note exists before continuing (and allow pages other than notes).
if (typeof slug !== "string" || !new RegExp(/^notes\/([A-Za-z0-9-]+)$/i).test(slug)) {
// @ts-expect-error
return NextResponse.json({ error: "Missing or invalid 'slug' parameter." }, { status: 400 });
}
// +1 hit!
const { hits } = await prisma.hits.upsert({
where: { slug },
create: { slug },
update: {
hits: {
increment: 1,
},
},
});
// add one to this page's count and return the new number
return NextResponse.json({ hits });
}

26
app/api/hits/route.ts Normal file
View File

@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma";
import type { SiteStats } from "../../../types";
export const revalidate = 900; // 15 mins
export async function GET(): Promise<NextResponse<SiteStats>> {
// fetch all rows from db sorted by most hits
const pages = await prisma.hits.findMany({
orderBy: [
{
hits: "desc",
},
],
});
const total = { hits: 0 };
// calculate total hits
pages.forEach((page) => {
// add these hits to running tally
total.hits += page.hits;
});
return NextResponse.json({ total, pages });
}

BIN
app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

40
app/birthday/page.tsx Normal file
View File

@ -0,0 +1,40 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Video from "../../components/Video";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import thumbnail from "../../public/static/images/birthday/thumb.png";
export const metadata: Metadata = {
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
description: "The origin of my hatred for the Happy Birthday song.",
openGraph: {
...defaultMetadata.openGraph,
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
images: [thumbnail.src],
url: "/birthday",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/birthday",
},
};
export default function Page() {
return (
<>
<PageTitle>📼 1996.MOV</PageTitle>
<Content>
<Video
src={{
webm: "/static/images/birthday/birthday.webm",
mp4: "/static/images/birthday/birthday.mp4",
image: thumbnail.src,
}}
/>
</Content>
</>
);
}

75
app/cli/page.tsx Normal file
View File

@ -0,0 +1,75 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Image from "../../components/Image";
import Blockquote from "../../components/Blockquote";
import CodeBlock from "../../components/CodeBlock";
import { H2 } from "../../components/Heading";
import { UnorderedList, ListItem } from "../../components/List";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import cliImg from "../../public/static/images/cli/screenshot.png";
export const metadata: Metadata = {
title: "CLI",
description: "AKA, the most useless Node module ever published, in history, by anyone, ever.",
openGraph: {
...defaultMetadata.openGraph,
title: "CLI",
images: [cliImg.src],
url: "/cli",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/cli",
},
};
export default function Page() {
return (
<>
<PageTitle>🤖 CLI</PageTitle>
<Content>
<Blockquote>
The <Link href="/">Jake Jarvis</Link> CLI (aka the most useless Node module ever published, in history, by
anyone, ever).
</Blockquote>
<Image src={cliImg} href="https://www.npmjs.com/package/@jakejarvis/cli" alt="Terminal Screenshot" priority />
<H2 id="usage">Usage</H2>
<CodeBlock withCopyButton>npx @jakejarvis/cli</CodeBlock>
<H2 id="inspired-by">Inspired by</H2>
<UnorderedList>
<ListItem>
<Link href="https://github.com/sindresorhus/sindresorhus-cli">@sindresorhus/sindresorhus-cli</Link>
</ListItem>
<ListItem>
<Link href="https://github.com/yg/ygcodes">@yg/ygcodes</Link>
</ListItem>
</UnorderedList>
<H2 id="built-with">Built with</H2>
<UnorderedList>
<ListItem>
<Link href="https://github.com/vadimdemedes/ink">ink</Link> - React for interactive command-line apps
</ListItem>
<ListItem>
<Link href="https://github.com/sindresorhus/meow">meow</Link> - CLI helper
</ListItem>
</UnorderedList>
<p>
<Link href="https://github.com/jakejarvis/jakejarvis/tree/main/cli">View source on GitHub.</Link>
</p>
<H2 id="license">License</H2>
<p>
MIT &copy; <Link href="/">Jake Jarvis</Link>, <Link href="https://sindresorhus.com">Sindre Sorhus</Link>
</p>
</Content>
</>
);
}

View File

@ -0,0 +1,95 @@
.input {
width: 100%;
padding: 0.8em;
margin: 0.6em 0;
border: 2px solid var(--colors-light);
border-radius: var(--radii-corner);
color: var(--colors-text);
background-color: var(--colors-superDuperLight);
transition: background var(--transitions-fade);
}
.input:focus {
outline: none;
border-color: var(--colors-link);
}
.input.missing {
border-color: var(--colors-error);
}
.input.textarea {
margin-bottom: 0;
line-height: 1.5;
min-height: 10em;
resize: vertical;
}
.markdownTip {
font-size: 0.825em;
line-height: 1.75;
}
.markdownIcon {
display: inline;
width: 1.25em;
height: 1.25em;
vertical-align: -0.25em;
margin-right: 0.25em;
}
.captcha {
margin: 1em 0;
}
.actionRow {
display: flex;
align-items: center;
min-height: 3.75em;
}
.submitButton {
flex-shrink: 0;
height: 3.25em;
padding: 1em 1.25em;
margin-right: 1.5em;
border: 0;
border-radius: var(--radii-corner);
cursor: pointer;
user-select: none;
font-weight: 500;
color: var(--colors-text);
background-color: var(--colors-kindaLight);
}
.submitButton:hover,
.submitButton:focus-visible {
color: var(--colors-superDuperLight);
background-color: var(--colors-link);
}
.submitIcon {
font-size: 1.3em;
margin-right: 0.3em;
line-height: 1;
}
.result {
font-weight: 600;
line-height: 1.5;
}
.result.success {
color: var(--colors-success);
}
.result.error {
color: var(--colors-error);
}
.resultIcon {
display: inline;
width: 1.3em;
height: 1.3em;
vertical-align: -0.3em;
fill: currentColor;
}

200
app/contact/form.tsx Normal file
View File

@ -0,0 +1,200 @@
"use client";
import { useState } from "react";
import { Formik, Form, Field } from "formik";
import TextareaAutosize from "react-textarea-autosize";
import Turnstile from "react-turnstile";
import clsx from "clsx";
import Link from "../../components/Link";
import useTheme from "../../hooks/useTheme";
import { GoCheck, GoX } from "react-icons/go";
import { SiMarkdown } from "react-icons/si";
import type { FormikHelpers, FormikProps, FieldInputProps, FieldMetaProps } from "formik";
import styles from "./form.module.css";
type FormValues = {
name: string;
email: string;
message: string;
"cf-turnstile-response": string;
};
export type ContactFormProps = {
className?: string;
};
const ContactForm = ({ className }: ContactFormProps) => {
const { activeTheme } = useTheme();
// status/feedback:
const [submitted, setSubmitted] = useState(false);
const [success, setSuccess] = useState(false);
const [feedback, setFeedback] = useState("");
const handleSubmit = (values: FormValues, { setSubmitting }: FormikHelpers<FormValues>) => {
// once a user attempts a submission, this is true and stays true whether or not the next attempt(s) are successful
setSubmitted(true);
// https://stackoverflow.com/a/68372184
const formData = new FormData();
for (const key in values) {
formData.append(key, values[key as keyof FormValues]);
}
// if we've gotten here then all data is (or should be) valid and ready to post to API
fetch("/api/contact/", {
method: "POST",
headers: {
Accept: "application/json",
},
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (data.success === true) {
// handle successful submission
// disable submissions, hide the send button, and let user know we were successful
setSuccess(true);
setFeedback("Thanks! You should hear from me soon.");
} else {
// pass on any error sent by the server to the catch block below
throw new Error(data.message);
}
})
.catch((error) => {
setSuccess(false);
if (error.message === "missing_data") {
// this should be validated client-side but it's also checked server-side just in case someone slipped past
setFeedback("Please make sure that all fields are properly filled in.");
} else if (error.message === "invalid_captcha") {
// missing/invalid captcha
setFeedback("Did you complete the CAPTCHA? (If you're human, that is...)");
} else {
// something else went wrong, and it's probably my fault...
setFeedback("Internal server error... Try again later or shoot me an old-fashioned email?");
}
})
.finally(() => setSubmitting(false));
};
return (
<Formik
onSubmit={handleSubmit}
initialValues={{
name: "",
email: "",
message: "",
"cf-turnstile-response": "",
}}
validate={(values: FormValues) => {
const errors: Partial<Record<keyof FormValues, boolean>> = {};
errors.name = !values.name;
errors.email = !values.email; // also loosely validated that it's email-like via browser (not foolproof)
errors.message = !values.message;
errors["cf-turnstile-response"] = !values["cf-turnstile-response"];
if (!errors.name && !errors.email && !errors.message && !errors["cf-turnstile-response"]) {
setFeedback("");
return {};
} else {
setSuccess(false);
setFeedback("Please make sure that all fields are properly filled in.");
}
return errors;
}}
>
{({ setFieldValue, isSubmitting }: FormikProps<FormValues>) => (
<Form className={className} name="contact">
<Field name="name">
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
<input
type="text"
placeholder="Name"
disabled={success}
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
{...field}
/>
)}
</Field>
<Field name="email">
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
<input
type="email"
inputMode="email"
placeholder="Email"
disabled={success}
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
{...field}
/>
)}
</Field>
<Field name="message">
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
<TextareaAutosize
placeholder="Write something..."
minRows={5}
disabled={success}
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
{...field}
/>
)}
</Field>
<div className={styles.markdownTip}>
<SiMarkdown className={styles.markdownIcon} /> Basic{" "}
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
Markdown syntax
</Link>{" "}
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
<Link href="https://jarv.is" underline={false} openInNewTab>
links
</Link>
](https://jarv.is), and <code>`code`</code>.
</div>
<Turnstile
sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"}
onVerify={(token) => setFieldValue("cf-turnstile-response", token)}
className={styles.captcha}
theme={activeTheme === "dark" ? activeTheme : "light"}
/>
<div className={styles.actionRow}>
<button
type="submit"
title="Send Message"
aria-label="Send Message"
onClick={() => setSubmitted(true)}
disabled={isSubmitting}
className={styles.submitButton}
style={{ display: success ? "none" : "inline-flex" }}
>
{isSubmitting ? (
<span>Sending...</span>
) : (
<>
<span className={styles.submitIcon}>📤</span> <span>Send</span>
</>
)}
</button>
<div
className={clsx(styles.result, success ? styles.success : styles.error)}
style={{ display: submitted && feedback && !isSubmitting ? "block" : "none" }}
>
{success ? <GoCheck className={styles.resultIcon} /> : <GoX className={styles.resultIcon} />} {feedback}
</div>
</div>
</Form>
)}
</Formik>
);
};
export default ContactForm;

52
app/contact/page.tsx Normal file
View File

@ -0,0 +1,52 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import ContactForm from "./form";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata, Route } from "next";
export const metadata: Metadata = {
title: "Contact Me",
openGraph: {
...defaultMetadata.openGraph,
title: "Contact Me",
url: "/contact",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/contact",
},
};
export default function Page() {
return (
<>
<PageTitle>📬 Contact Me</PageTitle>
<Content
style={{
maxWidth: "600px",
margin: "0 auto",
}}
>
<p>
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
<Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "}
<Link href="https://fediverse.jarv.is/@jake">direct message on Mastodon</Link>, or{" "}
<Link href="sms:+1-617-917-3737">text me</Link>.
</p>
<p>
🔐 You can grab my public key here:{" "}
<Link href={"/pubkey.asc" as Route} title="My Public PGP Key" rel="pgpkey authn" openInNewTab>
<code style={{ fontSize: "0.925em", letterSpacing: "0.075em", wordSpacing: "-0.3em" }}>
6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39
</code>
</Link>
.
</p>
<ContactForm />
</Content>
</>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

11
app/feed.atom/route.ts Normal file
View File

@ -0,0 +1,11 @@
import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static";
export const GET = async () => {
return new Response(await buildFeed({ type: "atom" }), {
headers: {
"content-type": "application/atom+xml; charset=utf-8",
},
});
};

11
app/feed.xml/route.ts Normal file
View File

@ -0,0 +1,11 @@
import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static";
export const GET = async () => {
return new Response(await buildFeed({ type: "rss" }), {
headers: {
"content-type": "application/rss+xml; charset=utf-8",
},
});
};

21
app/global.css Normal file
View File

@ -0,0 +1,21 @@
body {
font-family: var(--fonts-sans);
background-color: var(--colors-backgroundInner);
transition: background var(--transitions-fade);
}
code,
kbd,
samp,
pre {
font-family: var(--fonts-mono);
}
/* https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/ */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}

66
app/hillary/page.tsx Normal file
View File

@ -0,0 +1,66 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import thumbnail from "../../public/static/images/hillary/thumb.png";
export const metadata: Metadata = {
title: "My Brief Apperance in Hillary Clinton's DNC Video",
description: "My brief apperance in one of Hillary Clinton's 2016 DNC convention videos on substance abuse.",
openGraph: {
...defaultMetadata.openGraph,
title: "My Brief Apperance in Hillary Clinton's DNC Video",
images: [thumbnail.src],
url: "/hillary",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/hillary",
},
};
export default function Page() {
return (
<>
<PageTitle>My Brief Apperance in Hillary Clinton's DNC Video</PageTitle>
<Content>
<Video
src={{
webm: "/static/images/hillary/convention-720p.webm",
mp4: "/static/images/hillary/convention-720p.mp4",
vtt: "/static/images/hillary/subs.en.vtt",
image: thumbnail.src,
}}
/>
<p
style={{
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-mediumLight)",
}}
>
Video is property of{" "}
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
Hillary for America
</Link>
, the{" "}
<Link href="https://democrats.org/" style={{ fontWeight: 700 }}>
Democratic National Committee
</Link>
, and{" "}
<Link href="https://cnnpressroom.blogs.cnn.com/" style={{ fontWeight: 700 }}>
CNN / WarnerMedia
</Link>
. &copy; 2016.
</p>
</Content>
</>
);
}

BIN
app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

95
app/layout.tsx Normal file
View File

@ -0,0 +1,95 @@
import clsx from "clsx";
import { Analytics } from "@vercel/analytics/react";
import { ThemeProvider } from "../contexts/ThemeContext";
import Layout from "../components/Layout";
import config from "../lib/config";
import type { Metadata } from "next";
import type { Person, WithContext } from "schema-dts";
import { GeistMono, GeistSans } from "../lib/styles/fonts";
import "modern-normalize/modern-normalize.css"; // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css
import "./themes.css";
import "./global.css";
import { meJpg } from "../lib/config/favicons";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || `https://${config.siteDomain}`),
title: {
template: `%s ${config.siteName}`,
default: `${config.siteName} ${config.shortDescription}`,
},
description: config.longDescription,
openGraph: {
siteName: config.siteName,
title: {
template: `%s ${config.siteName}`,
default: `${config.siteName} ${config.shortDescription}`,
},
url: "/",
locale: config.siteLocale?.replace("-", "_"),
type: "website",
images: [
{
url: meJpg.src,
alt: `${config.siteName} ${config.shortDescription}`,
},
],
},
alternates: {
types: {
"application/rss+xml": "/feed.xml",
"application/atom+xml": "/feed.atom",
},
canonical: "/",
},
other: {
humans: "/humans.txt",
},
};
// https://nextjs.org/docs/app/building-your-application/optimizing/metadata#json-ld
const jsonLd: WithContext<Person> = {
"@context": "https://schema.org",
"@type": "Person",
name: config.authorName,
url: metadata.metadataBase?.href || `https://${config.siteDomain}/`,
image: new URL(meJpg.src, metadata.metadataBase || `https://${config.siteDomain}`).href,
sameAs: [
metadata.metadataBase?.href || `https://${config.siteDomain}/`,
`https://github.com/${config.authorSocial?.github}`,
`https://keybase.io/${config.authorSocial?.keybase}`,
`https://twitter.com/${config.authorSocial?.twitter}`,
`https://medium.com/@${config.authorSocial?.medium}`,
`https://www.linkedin.com/in/${config.authorSocial?.linkedin}/`,
`https://www.facebook.com/${config.authorSocial?.facebook}`,
`https://www.instagram.com/${config.authorSocial?.instagram}/`,
`https://${config.authorSocial?.mastodon}`,
`https://bsky.app/profile/${config.authorSocial?.bluesky}`,
],
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang={config.siteLocale} suppressHydrationWarning>
<head>
<script
// unminified: https://gist.github.com/jakejarvis/79b0ec8506bc843023546d0d29861bf0
dangerouslySetInnerHTML={{
__html: `(()=>{try{const e=document.documentElement,t="undefined"!=typeof Storage?window.localStorage.getItem("theme"):null,a=(t&&"dark"===t)??window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";e.dataset.theme=a,e.style.colorScheme=a}catch(e){}})()`,
}}
/>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
</head>
<body className={clsx(GeistMono.variable, GeistSans.variable)}>
<ThemeProvider>
<Layout>{children}</Layout>
</ThemeProvider>
<Analytics />
</body>
</html>
);
}

62
app/leo/page.tsx Normal file
View File

@ -0,0 +1,62 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import thumbnail from "../../public/static/images/leo/thumb.png";
export const metadata: Metadata = {
title: 'Facebook App on "The Lab with Leo Laporte"',
description: "Powncer app featured in Leo Laporte's TechTV show.",
openGraph: {
...defaultMetadata.openGraph,
title: 'Facebook App on "The Lab with Leo Laporte"',
images: [thumbnail.src],
url: "/leo",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/leo",
},
};
export default function Page() {
return (
<>
<PageTitle>Facebook App on "The Lab with Leo Laporte"</PageTitle>
<Content>
<Video
src={{
webm: "/static/images/leo/leo.webm",
mp4: "/static/images/leo/leo.mp4",
vtt: "/static/images/leo/subs.en.vtt",
image: thumbnail.src,
}}
/>
<p
style={{
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-mediumLight)",
}}
>
Video is property of{" "}
<Link href="https://web.archive.org/web/20070511004304/www.g4techtv.ca" style={{ fontWeight: 700 }}>
G4techTV Canada
</Link>{" "}
&amp;{" "}
<Link href="https://leolaporte.com/" style={{ fontWeight: 700 }}>
Leo Laporte
</Link>
. &copy; 2007 G4 Media, Inc.
</p>
</Content>
</>
);
}

481
app/license/page.tsx Normal file
View File

@ -0,0 +1,481 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import HorizontalRule from "../../components/HorizontalRule";
import Blockquote from "../../components/Blockquote";
import { H2, H3 } from "../../components/Heading";
import { UnorderedList, OrderedList, ListItem } from "../../components/List";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "License",
openGraph: {
...defaultMetadata.openGraph,
title: "License",
url: "/license",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/license",
},
};
export default function Page() {
return (
<>
<PageTitle>📜 License</PageTitle>
<Content>
<p>
Unless otherwise noted, content on this website is published under the{" "}
<Link href="https://creativecommons.org/licenses/by/4.0/">
<strong>Creative Commons Attribution 4.0 International Public License</strong>
</Link>{" "}
(CC-BY-4.0), which means that you can copy, redistribute, remix, transform, and build upon the content for any
purpose as long as you give appropriate credit (such as a hyperlink to the original URL).
</p>
<p>
The <Link href="https://creativecommons.org/licenses/by/4.0/legalcode">full license</Link> is re-printed
below.
</p>
<HorizontalRule />
<H2 id="full-text">Creative Commons Attribution 4.0 International Public License</H2>
<p style={{ textAlign: "center", lineHeight: 0 }}>
<Link
href="https://creativecommons.org/licenses/by/4.0/"
title="Creative Commons Attribution 4.0"
underline={false}
>
<svg width="120" height="42">
<path d="M3.1.5l113.4.2c1.6 0 3-.2 3 3.2l-.1 37.3H.3V3.7C.3 2.1.4.5 3 .5z" fill="#aab2ab"></path>
<path d="M117.8 0H2.2C1 0 0 1 0 2.2v39.3c0 .3.2.5.5.5h119c.3 0 .5-.2.5-.5V2.2c0-1.2-1-2.2-2.2-2.2zM2.2 1h115.6c.6 0 1.2.6 1.2 1.2v27.3H36.2a17.8 17.8 0 01-31.1 0H1V2.2C1 1.6 1.5 1 2.1 1z"></path>
<path
d="M73.8 32.7l.9.1.6.3.5.5.1.8c0 .3 0 .6-.2.8l-.7.6c.4 0 .7.3 1 .6l.2 1-.1 1-.6.5-.7.4H70.7v-6.6h3.1zm-.2 2.7c.3 0 .5 0 .7-.2l.2-.6v-.3l-.3-.3H74l-.4-.1h-1.4v1.5h1.5zm.1 2.8h.4l.4-.1.2-.3v-.4c0-.4 0-.6-.2-.8l-.8-.2h-1.6v1.8h1.6zM76.5 32.7h1.6l1.6 2.7 1.5-2.7H83l-2.5 4.1v2.6h-1.5v-2.6l-2.4-4zM34.3 19.6a13.6 13.6 0 01-27.3 0 13.6 13.6 0 0127.3 0z"
fill="#fff"
></path>
<path d="M31.7 8.5c3 3 4.5 6.7 4.5 11.1a15.4 15.4 0 01-15.6 15.6 15 15 0 01-11-4.6 15 15 0 01-4.6-11c0-4.3 1.5-8 4.6-11.1 3-3 6.7-4.5 11-4.5 4.4 0 8 1.5 11.1 4.5zm-20 2a12.5 12.5 0 00-3.9 9.1c0 3.5 1.3 6.5 3.8 9s5.6 3.8 9 3.8c3.5 0 6.6-1.3 9.2-3.8a12 12 0 003.6-9c0-3.6-1.2-6.6-3.7-9a12.3 12.3 0 00-9-3.8c-3.6 0-6.6 1.2-9 3.7zm6.7 7.6c-.4-.9-1-1.3-1.8-1.3-1.4 0-2 1-2 2.8 0 1.8.6 2.8 2 2.8 1 0 1.6-.5 2-1.4l1.9 1a4.4 4.4 0 01-4.1 2.5c-1.4 0-2.5-.5-3.4-1.3-.8-.9-1.3-2-1.3-3.6 0-1.5.5-2.7 1.3-3.5 1-1 2-1.3 3.3-1.3 2 0 3.3.7 4.1 2.2l-2 1zm9 0c-.4-.9-1-1.3-1.8-1.3-1.4 0-2 1-2 2.8 0 1.8.6 2.8 2 2.8 1 0 1.6-.5 2-1.4l2 1a4.4 4.4 0 01-4.2 2.5c-1.4 0-2.5-.5-3.3-1.3-.9-.9-1.3-2-1.3-3.6 0-1.5.4-2.7 1.3-3.5.8-1 2-1.3 3.2-1.3 2 0 3.3.7 4.2 2.2l-2.1 1z"></path>
<g transform="matrix(.99377 0 0 .99367 -177.7 0)">
<circle cx="255.6" cy="15.3" r="10.8" fill="#fff"></circle>
<path d="M258.7 12.2c0-.4-.4-.8-.8-.8h-4.7c-.5 0-.8.4-.8.8V17h1.3v5.6h3.6V17h1.4v-4.8z"></path>
<circle cx="255.5" cy="9.2" r="1.6"></circle>
<path
clipRule="evenodd"
d="M255.5 3.4c-3.2 0-6 1.1-8.2 3.4A11.4 11.4 0 00244 15c0 3.2 1.1 6 3.4 8.2 2.3 2.3 5 3.4 8.2 3.4 3.2 0 6-1.1 8.4-3.4a11 11 0 003.3-8.2c0-3.3-1.1-6-3.4-8.3-2.2-2.3-5-3.4-8.3-3.4zm0 2.1c2.7 0 5 1 6.8 2.8a9.2 9.2 0 012.8 6.8c0 2.7-1 4.9-2.7 6.7-2 1.9-4.2 2.8-6.8 2.8-2.7 0-5-1-6.8-2.8A9.2 9.2 0 01246 15c0-2.6 1-4.9 2.8-6.8a9 9 0 016.8-2.8z"
fillRule="evenodd"
></path>
</g>
</svg>
</Link>
</p>
<Blockquote>
<p>
<em>
Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or
legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other
relationship. Creative Commons makes its licenses and related information available on an "as-is" basis.
Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and
conditions, or any related information. Creative Commons disclaims all liability for damages resulting
from their use to the fullest extent possible.
</em>
</p>
</Blockquote>
<H3>Using Creative Commons Public Licenses</H3>
<p>
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights
holders may use to share original works of authorship and other material subject to copyright and certain
other rights specified in the public license below. The following considerations are for informational
purposes only, are not exhaustive, and do not form part of our licenses.
</p>
<UnorderedList>
<ListItem>
<p>
<strong>Considerations for licensors:</strong> Our public licenses are intended for use by those
authorized to give the public permission to use material in ways otherwise restricted by copyright and
certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and
conditions of the license they choose before applying it. Licensors should also secure all rights
necessary before applying our licenses so that the public can reuse the material as expected. Licensors
should clearly mark any material not subject to the license. This includes other CC-licensed material, or
material used under an exception or limitation to copyright.{" "}
<Link href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors">
More considerations for licensors
</Link>
.
</p>
</ListItem>
<ListItem>
<p>
<strong>Considerations for the public:</strong> By using one of our public licenses, a licensor grants the
public permission to use the licensed material under specified terms and conditions. If the licensor's
permission is not necessary for any reasonfor example, because of any applicable exception or limitation
to copyrightthen that use is not regulated by the license. Our licenses grant only permissions under
copyright and certain other rights that a licensor has authority to grant. Use of the licensed material
may still be restricted for other reasons, including because others have copyright or other rights in the
material. A licensor may make special requests, such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to respect those requests where reasonable.{" "}
<Link href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees">
More considerations for the public
</Link>
.
</p>
</ListItem>
</UnorderedList>
<H3>Licensed Rights</H3>
<p>
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and
conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the Licensed Material available under these terms
and conditions.
</p>
<H3>Section 1 Definitions.</H3>
<p>
a. <strong>Adapted Material</strong> means material subject to Copyright and Similar Rights that is derived
from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged,
transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights
held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work,
performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in
timed relation with a moving image.
</p>
<p>
b. <strong>Adapter's License</strong> means the license You apply to Your Copyright and Similar Rights in Your
contributions to Adapted Material in accordance with the terms and conditions of this Public License.
</p>
<p>
c. <strong>Copyright and Similar Rights</strong> means copyright and/or similar rights closely related to
copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the
rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
</p>
<p>
d. <strong>Effective Technological Measures</strong> means those measures that, in the absence of proper
authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international agreements.
</p>
<p>
e. <strong>Exceptions and Limitations</strong> means fair use, fair dealing, and/or any other exception or
limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
</p>
<p>
f. <strong>Licensed Material</strong> means the artistic or literary work, database, or other material to
which the Licensor applied this Public License.
</p>
<p>
g. <strong>Licensed Rights</strong> means the rights granted to You subject to the terms and conditions of
this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
</p>
<p>
h. <strong>Licensor</strong> means the individual(s) or entity(ies) granting rights under this Public License.
</p>
<p>
i. <strong>Share</strong> means to provide material to the public by any means or process that requires
permission under the Licensed Rights, such as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material available to the public including in ways
that members of the public may access the material from a place and at a time individually chosen by them.
</p>
<p>
j. <strong>Sui Generis Database Rights</strong> means rights other than copyright resulting from Directive
96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
</p>
<p>
k. <strong>You</strong> means the individual or entity exercising the Licensed Rights under this Public
License. <strong>Your</strong> has a corresponding meaning.
</p>
<H3>Section 2 Scope.</H3>
<p>
a.{" "}
<em>
<strong>License grant.</strong>
</em>
</p>
<OrderedList>
<ListItem>
<p>
Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide,
royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the
Licensed Material to:
</p>
<p>A. reproduce and Share the Licensed Material, in whole or in part; and</p>
<p>B. produce, reproduce, and Share Adapted Material.</p>
</ListItem>
<ListItem>
<p>
<strong>Exceptions and Limitations.</strong> For the avoidance of doubt, where Exceptions and Limitations
apply to Your use, this Public License does not apply, and You do not need to comply with its terms and
conditions.
</p>
</ListItem>
<ListItem>
<p>
<strong>Term.</strong> The term of this Public License is specified in Section 6(a).
</p>
</ListItem>
<ListItem>
<p>
<strong>Media and formats; technical modifications allowed.</strong> The Licensor authorizes You to
exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make
technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications necessary to exercise the Licensed Rights,
including technical modifications necessary to circumvent Effective Technological Measures. For purposes
of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces
Adapted Material.
</p>
</ListItem>
<ListItem>
<p>
<strong>Downstream recipients.</strong>
</p>
<p>
A. <strong>Offer from the Licensor Licensed Material.</strong> Every recipient of the Licensed Material
automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and
conditions of this Public License.
</p>
<p>
B. <strong>No downstream restrictions.</strong> You may not offer or impose any additional or different
terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing
so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
</p>
</ListItem>
<ListItem>
<p>
<strong>No endorsement.</strong> Nothing in this Public License constitutes or may be construed as
permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with,
or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive
attribution as provided in Section 3(a)(1)(A)(i).
</p>
</ListItem>
</OrderedList>
<p>
b.{" "}
<em>
<strong>Other rights.</strong>
</em>
</p>
<OrderedList>
<ListItem>
<p>
Moral rights, such as the right of integrity, are not licensed under this Public License, nor are
publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor
waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to
allow You to exercise the Licensed Rights, but not otherwise.
</p>
</ListItem>
<ListItem>
<p>Patent and trademark rights are not licensed under this Public License.</p>
</ListItem>
<ListItem>
<p>
To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of
the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable
statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to
collect such royalties.
</p>
</ListItem>
</OrderedList>
<H3>Section 3 License Conditions.</H3>
<p>Your exercise of the Licensed Rights is expressly made subject to the following conditions.</p>
<p>
a.{" "}
<em>
<strong>Attribution.</strong>
</em>
</p>
<OrderedList>
<ListItem>
<p>If You Share the Licensed Material (including in modified form), You must:</p>
<p>A. retain the following if it is supplied by the Licensor with the Licensed Material:</p>
<p>
i. identification of the creator(s) of the Licensed Material and any others designated to receive
attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
</p>
<p>ii. a copyright notice;</p>
<p>iii. a notice that refers to this Public License;</p>
<p>iv. a notice that refers to the disclaimer of warranties;</p>
<p>v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;</p>
<p>
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications;
and
</p>
<p>
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the
URI or hyperlink to, this Public License.
</p>
</ListItem>
<ListItem>
<p>
You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and
context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the
conditions by providing a URI or hyperlink to a resource that includes the required information.
</p>
</ListItem>
<ListItem>
<p>
If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the
extent reasonably practicable.
</p>
</ListItem>
<ListItem>
<p>
If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of
the Adapted Material from complying with this Public License.
</p>
</ListItem>
</OrderedList>
<H3>Section 4 Sui Generis Database Rights.</H3>
<p>
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
</p>
<p>
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share
all or a substantial portion of the contents of the database;
</p>
<p>
b. if You include all or a substantial portion of the database contents in a database in which You have Sui
Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its
individual contents) is Adapted Material; and
</p>
<p>
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the
contents of the database.
</p>
<p>
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public
License where the Licensed Rights include other Copyright and Similar Rights.
</p>
<H3>Section 5 Disclaimer of Warranties and Limitation of Liability.</H3>
<p>
a.{" "}
<strong>
Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the
Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning
the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation,
warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent
or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable.
Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
</strong>
</p>
<p>
b.{" "}
<strong>
To the extent possible, in no event will the Licensor be liable to You on any legal theory (including,
without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential,
punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use
of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs,
expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may
not apply to You.
</strong>
</p>
<p>
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner
that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
</p>
<H3>Section 6 Term and Termination.</H3>
<p>
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You
fail to comply with this Public License, then Your rights under this Public License terminate automatically.
</p>
<p>b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:</p>
<OrderedList>
<ListItem>
<p>
automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery
of the violation; or
</p>
</ListItem>
<ListItem>
<p>upon express reinstatement by the Licensor.</p>
</ListItem>
</OrderedList>
<p>
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies
for Your violations of this Public License.
</p>
<p>
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or
conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this
Public License.
</p>
<p>d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.</p>
<H3>Section 7 Other Terms and Conditions.</H3>
<p>
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You
unless expressly agreed.
</p>
<p>
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are
separate from and independent of the terms and conditions of this Public License.
</p>
<H3>Section 8 Interpretation.</H3>
<p>
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit,
restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without
permission under this Public License.
</p>
<p>
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be
automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be
reformed, it shall be severed from this Public License without affecting the enforceability of the remaining
terms and conditions.
</p>
<p>
c. No term or condition of this Public License will be waived and no failure to comply consented to unless
expressly agreed to by the Licensor.
</p>
<p>
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any
privileges and immunities that apply to the Licensor or You, including from the legal processes of any
jurisdiction or authority.
</p>
<Blockquote>
<p>
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply
one of its public licenses to material it publishes and in those instances will be considered the
"Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the{" "}
<Link href="https://creativecommons.org/publicdomain/zero/1.0/legalcode">
<em>CC0 Public Domain Dedication</em>
</Link>
. Except for the limited purpose of indicating that material is shared under a Creative Commons public
license or as otherwise permitted by the Creative Commons policies published at{" "}
<Link href="https://creativecommons.org/policies">creativecommons.org/policies</Link>, Creative Commons does
not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons
without its prior written consent including, without limitation, in connection with any unauthorized
modifications to any of its public licenses or any other arrangements, understandings, or agreements
concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the
public licenses.
</p>
<p>
Creative Commons may be contacted at <Link href="https://creativecommons.org/">creativecommons.org</Link>.
</p>
</Blockquote>
</Content>
</>
);
}

42
app/manifest.ts Normal file
View File

@ -0,0 +1,42 @@
import config from "../lib/config";
import { chrome512Png, chrome192Png, maskable512Png, maskable192Png } from "../lib/config/favicons";
import type { MetadataRoute } from "next";
const manifest = (): MetadataRoute.Manifest => {
return {
name: config.siteName,
short_name: config.siteDomain,
description: config.longDescription,
lang: config.siteLocale,
icons: [
{
src: chrome512Png.src,
sizes: `${chrome512Png.width}x${chrome512Png.height}`,
type: "image/png",
purpose: "any",
},
{
src: chrome192Png.src,
sizes: `${chrome192Png.width}x${chrome192Png.height}`,
type: "image/png",
purpose: "any",
},
{
src: maskable512Png.src,
sizes: `${maskable512Png.width}x${maskable512Png.height}`,
type: "image/png",
purpose: "maskable",
},
{
src: maskable192Png.src,
sizes: `${maskable192Png.width}x${maskable192Png.height}`,
type: "image/png",
purpose: "maskable",
},
],
display: "browser",
start_url: "/",
};
};
export default manifest;

29
app/not-found.tsx Normal file
View File

@ -0,0 +1,29 @@
import Link from "../components/Link";
import Video from "../components/Video";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "404 Not Found",
};
export default async function Page() {
return (
<div style={{ textAlign: "center" }}>
<Video
src={{
webm: "/static/images/angry-panda.webm",
mp4: "/static/images/angry-panda.mp4",
}}
autoplay
responsive={false}
style={{
maxWidth: "400px",
}}
/>
<h1>404: Page Not Found 😢</h1>
<Link href="/">Go home?</Link>
</div>
);
}

View File

@ -0,0 +1,67 @@
.meta {
display: inline-flex;
flex-wrap: wrap;
font-size: 0.825em;
line-height: 2.3;
letter-spacing: 0.04em;
color: var(--colors-medium);
}
.meta .item {
margin-right: 1.6em;
white-space: nowrap;
}
.meta .link {
color: inherit !important;
}
.meta .icon {
display: inline;
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
margin-right: 0.6em;
}
.meta .tags {
white-space: normal;
display: inline-flex;
flex-wrap: wrap;
}
.meta .tag {
text-transform: lowercase;
white-space: nowrap;
margin-right: 0.75em;
}
.meta .tag:before {
content: "\0023"; /* cosmetically hashtagify tags */
padding-right: 0.125em;
color: var(--colors-light);
}
.meta .tag:last-of-type {
margin-right: 0;
}
.title {
margin: 0.3em 0 0.5em -1px; /* misaligned left margin, super nitpicky */
font-size: 2.1em;
line-height: 1.3;
font-weight: 700;
}
.title code {
margin: 0 0.075em;
}
.title .link {
color: var(--colors-text) !important;
}
@media (max-width: 768px) {
.title {
font-size: 1.8em;
}
}

178
app/notes/[slug]/page.tsx Normal file
View File

@ -0,0 +1,178 @@
import * as runtime from "react/jsx-runtime";
import { ErrorBoundary } from "react-error-boundary";
import { evaluate } from "@mdx-js/mdx";
import Content from "../../../components/Content";
import Link from "../../../components/Link";
import Time from "../../../components/Time";
import HitCounter from "../../../components/HitCounter";
import Comments from "../../../components/Comments";
import { getPostSlugs, getPostData } from "../../../lib/helpers/posts";
import * as mdxComponents from "../../../lib/helpers/mdx-components";
import { metadata as defaultMetadata } from "../../layout";
import config from "../../../lib/config";
import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi";
import type { Metadata, Route } from "next";
import type { Article, WithContext } from "schema-dts";
import styles from "./page.module.css";
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params#disable-rendering-for-unspecified-paths
export const dynamicParams = false;
export async function generateStaticParams() {
const slugs = await getPostSlugs();
// map slugs into a static paths object required by next.js
return slugs.map((slug: string) => ({
slug,
}));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const { frontMatter } = await getPostData(slug);
return {
title: frontMatter.title,
description: frontMatter.description,
openGraph: {
...defaultMetadata.openGraph,
title: frontMatter.title,
url: `/notes/${slug}`,
type: "article",
authors: [config.authorName],
tags: frontMatter.tags,
publishedTime: frontMatter.date,
modifiedTime: frontMatter.date,
images: frontMatter.image
? [{ url: frontMatter.image, alt: frontMatter.title }]
: defaultMetadata.openGraph?.images,
},
alternates: {
...defaultMetadata.alternates,
canonical: `/notes/${slug}`,
},
};
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const { frontMatter, markdown } = await getPostData(slug);
const jsonLd: WithContext<Article> = {
"@context": "https://schema.org",
"@type": "Article",
name: frontMatter.title,
description: frontMatter.description || config.longDescription,
url: frontMatter.permalink,
image: frontMatter.image,
datePublished: frontMatter.date,
dateModified: frontMatter.date,
author: {
"@type": "Person",
name: config.authorName,
url: defaultMetadata.metadataBase?.href || `https://${config.siteDomain}`,
},
};
const { remarkGfm, remarkSmartypants, rehypeSlug, rehypeUnwrapImages, rehypePrism } = await import(
"../../../lib/helpers/remark-rehype-plugins"
);
const { default: MDXContent } = await evaluate(markdown, {
...runtime,
remarkPlugins: [
[remarkGfm, { singleTilde: false }],
[
remarkSmartypants,
{
quotes: true,
dashes: "oldschool",
backticks: false,
ellipses: false,
},
],
],
rehypePlugins: [rehypeSlug, rehypeUnwrapImages, [rehypePrism, { ignoreMissing: true }]],
});
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<div className={styles.meta}>
<div className={styles.item}>
<Link href={`/notes/${frontMatter.slug}` as Route} underline={false} className={styles.link}>
<FiCalendar className={styles.icon} />
<Time date={frontMatter.date} format="MMMM D, YYYY" />
</Link>
</div>
{frontMatter.tags && (
<div className={styles.item}>
<FiTag className={styles.icon} />
<span className={styles.tags}>
{frontMatter.tags.map((tag) => (
<span key={tag} title={tag} className={styles.tag} aria-label={`Tagged with ${tag}`}>
{tag}
</span>
))}
</span>
</div>
)}
<div className={styles.item}>
<Link
href={`https://github.com/${config.githubRepo}/blob/main/notes/${frontMatter.slug}.mdx`}
title={`Edit "${frontMatter.title}" on GitHub`}
underline={false}
className={styles.link}
>
<FiEdit className={styles.icon} />
<span>Improve This Post</span>
</Link>
</div>
{/* only count hits on production site */}
{process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && (
<div
className={styles.item}
style={{
// fix potential layout shift when number of hits loads
minWidth: "7em",
marginRight: 0,
}}
>
{/* completely hide this block if anything goes wrong on the backend */}
<ErrorBoundary fallback={null}>
<FiEye className={styles.icon} />
<HitCounter slug={`notes/${frontMatter.slug}`} />
</ErrorBoundary>
</div>
)}
</div>
<h1 className={styles.title}>
<Link
href={`/notes/${frontMatter.slug}` as Route}
dangerouslySetInnerHTML={{ __html: frontMatter.htmlTitle || frontMatter.title }}
underline={false}
className={styles.link}
/>
</h1>
<Content>
<MDXContent
// @ts-ignore
components={{ ...mdxComponents }}
/>
</Content>
{!frontMatter.noComments && (
<div id="comments">
<Comments title={frontMatter.title} />
</div>
)}
</>
);
}

51
app/notes/page.module.css Normal file
View File

@ -0,0 +1,51 @@
.section {
font-size: 1.1em;
line-height: 1.1;
margin: 2.4em 0;
}
.section:first-of-type {
margin-top: 0;
}
.section:last-of-type {
margin-bottom: 0;
}
.year {
font-size: 2.2em;
font-weight: 700;
margin-top: 0;
margin-bottom: 0.5em;
}
.list {
list-style-type: none;
margin: 0;
padding: 0;
}
.post {
display: flex;
line-height: 1.75;
margin-bottom: 1em;
}
.post:last-of-type {
margin-bottom: 0;
}
.postDate {
width: 5.25em;
flex-shrink: 0;
color: var(--colors-medium);
}
@media (max-width: 768px) {
.section {
margin: 1.8em 0;
}
.year {
font-size: 2em;
}
}

65
app/notes/page.tsx Normal file
View File

@ -0,0 +1,65 @@
import Content from "../../components/Content";
import Link from "../../components/Link";
import Time from "../../components/Time";
import { getAllPosts } from "../../lib/helpers/posts";
import config from "../../lib/config";
import { metadata as defaultMetadata } from "../layout";
import type { ReactElement } from "react";
import type { Metadata, Route } from "next";
import type { PostsByYear } from "../../types";
import styles from "./page.module.css";
export const metadata: Metadata = {
title: "Notes",
description: `Recent posts by ${config.authorName}.`,
openGraph: {
...defaultMetadata.openGraph,
title: "Notes",
url: "/notes",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/notes",
},
};
export default async function Page() {
// parse the year of each note and group them together
const notes = await getAllPosts();
const notesByYear: PostsByYear = {};
notes.forEach((note) => {
const year = new Date(note.date).getUTCFullYear();
(notesByYear[year] || (notesByYear[year] = [])).push(note);
});
const sections: ReactElement[] = [];
Object.entries(notesByYear).forEach(([year, posts]) => {
sections.push(
<section className={styles.section} key={year}>
<h2 className={styles.year}>{year}</h2>
<ul className={styles.list}>
{posts.map(({ slug, date, title, htmlTitle }) => (
<li className={styles.post} key={slug}>
<Time date={date} format="MMM D" className={styles.postDate} />
<span>
<Link href={`/notes/${slug}` as Route} dangerouslySetInnerHTML={{ __html: htmlTitle || title }} />
</span>
</li>
))}
</ul>
</section>
);
});
// grouped posts enter this component ordered chronologically -- we want reverse chronological
const reversed = sections.reverse();
return (
<>
<Content>{reversed}</Content>
</>
);
}

100
app/page.module.css Normal file
View File

@ -0,0 +1,100 @@
.page h1 {
margin: 0 0 0.5em -1px; /* misaligned left margin, super nitpicky */
font-size: 1.75em;
font-weight: 500;
line-height: 1.1;
color: var(--colors-text);
}
.page h2 {
margin: 0.5em 0 0.5em -1px;
font-size: 1.2em;
font-weight: 400;
line-height: 1.4;
color: var(--colors-text);
}
.page p {
margin: 0.85em 0;
font-size: 0.95em;
line-height: 1.7;
color: var(--colors-text);
}
.page p:last-of-type {
margin-bottom: 0;
}
.page sup {
margin: 0 0.1em;
font-size: 0.6em;
}
.pgpIcon {
vertical-align: -0.25em;
stroke-width: 0.5;
}
.pgpKey {
margin: 0 0.15em;
font-family: var(--fonts-mono);
letter-spacing: 0.075em;
word-spacing: -0.4em;
}
.wave {
display: inline-block;
margin-left: 0.1em;
font-size: 1.2em;
}
@media (prefers-reduced-motion: no-preference) {
.wave {
animation: wave 5s ease 1s infinite;
transform-origin: 65% 80%;
will-change: transform;
}
@keyframes wave {
0% {
transform: rotate(0deg);
}
5% {
transform: rotate(14deg);
}
10% {
transform: rotate(-8deg);
}
15% {
transform: rotate(14deg);
}
20% {
transform: rotate(-4deg);
}
25% {
transform: rotate(10deg);
}
30% {
transform: rotate(0deg);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: rotate(0deg);
}
}
}
@media (max-width: 768px) {
.h1 {
font-size: 1.6em;
}
.h2 {
font-size: 1.25em;
}
.paragraph {
font-size: 0.925em;
line-height: 1.825;
}
}

294
app/page.tsx Normal file
View File

@ -0,0 +1,294 @@
import { useId } from "react";
import { GoLock } from "react-icons/go";
import { rgba } from "polished";
import Link from "../components/Link";
import type { ComponentPropsWithoutRef } from "react";
import type { Route } from "next";
import styles from "./page.module.css";
const ColorfulLink = ({
lightColor,
darkColor,
children,
...rest
}: ComponentPropsWithoutRef<typeof Link> & {
lightColor: string;
darkColor: string;
}) => {
const uniqueId = `Link_themed__${useId().replace(/\W/g, "")}`;
return (
<>
<Link id={uniqueId} {...rest}>
{children}
</Link>
<style>{`.${styles.page} #${uniqueId}{color:${lightColor};--colors-linkUnderline:${rgba(lightColor, 0.4)}}[data-theme="dark"] .${styles.page} #${uniqueId}{color:${darkColor};--colors-linkUnderline:${rgba(darkColor, 0.4)}}`}</style>
</>
);
};
export default function Page() {
return (
<div className={styles.page}>
<h1>
Hi there! I'm Jake. <span className={styles.wave}>👋</span>
</h1>
<h2>
I'm a frontend web developer based in the{" "}
<ColorfulLink
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3"
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
lightColor="#fb4d42"
darkColor="#ff5146"
>
Boston
</ColorfulLink>{" "}
area.
</h2>
<p>
I specialize in{" "}
<ColorfulLink
href="https://reactjs.org/"
title="React Official Website"
lightColor="#1091b3"
darkColor="#6fcbe3"
>
React
</ColorfulLink>{" "}
and{" "}
<ColorfulLink
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
lightColor="#f48024"
darkColor="#e18431"
>
vanilla JavaScript
</ColorfulLink>{" "}
to make nifty{" "}
<ColorfulLink href="https://jamstack.wtf/" title="WTF is Jamstack?" lightColor="#04a699" darkColor="#08bbac">
Jamstack sites
</ColorfulLink>{" "}
with dynamic{" "}
<ColorfulLink
href="https://nodejs.org/en/"
title="Node.js Official Website"
lightColor="#6fbc4e"
darkColor="#84d95f"
>
Node.js
</ColorfulLink>{" "}
services. But I still know my way around less buzzwordy stacks like{" "}
<ColorfulLink
href="https://www.jetbrains.com/lp/php-25/"
title="25 Years of PHP History"
lightColor="#8892bf"
darkColor="#a4afe3"
>
LAMP
</ColorfulLink>
, too.
</p>
<p>
Whenever possible, I also apply my experience in{" "}
<ColorfulLink
href="https://bugcrowd.com/jakejarvis"
title="Jake Jarvis on Bugcrowd"
lightColor="#00b81a"
darkColor="#57f06d"
>
application security
</ColorfulLink>
,{" "}
<ColorfulLink
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare'
lightColor="#0098ec"
darkColor="#43b9fb"
>
serverless stacks
</ColorfulLink>
, and{" "}
<ColorfulLink
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
title='My repositories tagged with "github-actions" on GitHub'
lightColor="#ff6200"
darkColor="#f46c16"
>
DevOps automation
</ColorfulLink>
.
</p>
<p>
I fell in love with{" "}
<ColorfulLink
href="/previously"
title="My Terrible, Horrible, No Good, Very Bad First Websites"
lightColor="#4169e1"
darkColor="#8ca9ff"
>
frontend web design
</ColorfulLink>{" "}
and{" "}
<ColorfulLink
href={"/notes/my-first-code" as Route}
title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc"
darkColor="#d588fb"
>
backend programming
</ColorfulLink>{" "}
when my only source of income was{" "}
<ColorfulLink
href="/birthday"
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088"
darkColor="#fd40b1"
style={{
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
}}
>
the Tooth Fairy
</ColorfulLink>
. <span style={{ color: "var(--colors-mediumLight)" }}>I've improved a bit since then, I think? 🤷</span>
</p>
<p>
Over the years, some of my side projects{" "}
<ColorfulLink
href="/leo"
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
lightColor="#ff1b1b"
darkColor="#f06060"
>
have
</ColorfulLink>{" "}
<ColorfulLink
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
lightColor="#f78200"
darkColor="#fd992a"
>
been
</ColorfulLink>{" "}
<ColorfulLink
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
lightColor="#f2b702"
darkColor="#ffcc2e"
>
featured
</ColorfulLink>{" "}
<ColorfulLink
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money'
lightColor="#5ebd3e"
darkColor="#78df55"
>
by
</ColorfulLink>{" "}
<ColorfulLink
href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
lightColor="#009cdf"
darkColor="#29bfff"
>
various
</ColorfulLink>{" "}
<ColorfulLink
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
lightColor="#3e49bb"
darkColor="#7b87ff"
>
media
</ColorfulLink>{" "}
<ColorfulLink
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
lightColor="#973999"
darkColor="#db60dd"
>
outlets
</ColorfulLink>
.
</p>
<p>
You can find my work on{" "}
<ColorfulLink
href="https://github.com/jakejarvis"
rel="me"
title="Jake Jarvis on GitHub"
lightColor="#8d4eff"
darkColor="#a379f0"
>
GitHub
</ColorfulLink>{" "}
and{" "}
<ColorfulLink
href="https://www.linkedin.com/in/jakejarvis/"
rel="me"
title="Jake Jarvis on LinkedIn"
lightColor="#0073b1"
darkColor="#3b9dd2"
>
LinkedIn
</ColorfulLink>
. I'm always available to connect over{" "}
<ColorfulLink href="/contact" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
email
</ColorfulLink>{" "}
<sup>
<ColorfulLink
href={"/pubkey.asc" as Route}
rel="pgpkey authn"
title="My Public Key"
lightColor="#757575"
darkColor="#959595"
underline={false}
openInNewTab
>
<GoLock size="1.25em" className={styles.pgpIcon} />{" "}
<span className={styles.pgpKey}>2B0C 9CF2 51E6 9A39</span>
</ColorfulLink>
</sup>
,{" "}
<ColorfulLink
href="https://bsky.app/profile/jarv.is"
rel="me"
title="Jake Jarvis on Bluesky"
lightColor="#0085FF"
darkColor="#208BFE"
>
Bluesky
</ColorfulLink>
,{" "}
<ColorfulLink
href="https://fediverse.jarv.is/@jake"
rel="me"
title="Jake Jarvis on Mastodon"
lightColor="#6d6eff"
darkColor="#7b87ff"
>
Mastodon
</ColorfulLink>
, or{" "}
<ColorfulLink
href="sms:+1-617-917-3737"
title="Send SMS to +1 (617) 917-3737"
lightColor="#6fcc01"
darkColor="#8edb34"
>
SMS
</ColorfulLink>{" "}
as well!
</p>
</div>
);
}

View File

@ -0,0 +1,39 @@
.wackyWrapper {
font-weight: 700;
font-size: 1em;
text-align: center;
/* classic windows 9x cursor easter egg */
cursor:
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAZklEQVR4AWIAgn/uBT6A9uoAAwAQiIJo97/0Rgy0ANoJH8MPeEgtqwPQEACqCoQHAKECQKgAECoAhAoAoQJAqAAQxh1oPQfcW3kJpxHtL1AAHAwEwwdYiH8BIEgBTBRAAAEEEEAAG7mRt30hEhoLAAAAAElFTkSuQmCC")
2 1,
auto;
}
.wackyWrapper a {
/* windows 9x hand cursor */
cursor:
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAACVBMVEVHcEwAAAD///8W1S+BAAAAAXRSTlMAQObYZgAAAEdJREFUeAFjoAVghTGkHIhghMAYmQEwxlIYYxlYlSiQMQEsELUKyli1ahWYwQZjMGIwGLKQGA4QA1EYEP0rGVAZrKGhSF4BAHw/HsVwshytAAAAAElFTkSuQmCC")
16 12,
auto;
}
.screenshot,
.divider {
margin: 1em auto;
}
.screenshot figcaption {
font-size: 0.9em;
line-height: 1.5;
text-align: center;
color: var(--colors-medium);
}
.screenshot:last-of-type {
margin-bottom: 0;
}
.divider {
margin: 1em auto;
}

220
app/previously/page.tsx Normal file
View File

@ -0,0 +1,220 @@
// import Layout from "../../components/Layout";
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Figure from "../../components/Figure";
import IFrame from "../../components/IFrame";
import CodeInline from "../../components/CodeInline";
import HorizontalRule from "../../components/HorizontalRule";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import { ComicNeue } from "../../lib/styles/fonts";
import styles from "./page.module.css";
import img_wayback from "../../public/static/images/previously/wayback.png";
import img_2002_02 from "../../public/static/images/previously/2002_02.png";
import img_2002_10 from "../../public/static/images/previously/2002_10.png";
import img_2003_08 from "../../public/static/images/previously/2003_08.png";
import img_2004_11 from "../../public/static/images/previously/2004_11.png";
import img_2006_04 from "../../public/static/images/previously/2006_04.png";
import img_2006_05 from "../../public/static/images/previously/2006_05.png";
import img_2007_01 from "../../public/static/images/previously/2007_01.png";
import img_2007_04 from "../../public/static/images/previously/2007_04.png";
import img_2007_05 from "../../public/static/images/previously/2007_05.png";
import img_2009_07 from "../../public/static/images/previously/2009_07.png";
import img_2012_09 from "../../public/static/images/previously/2012_09.png";
import img_2018_04 from "../../public/static/images/previously/2018_04.png";
import img_2020_03 from "../../public/static/images/previously/2020_03.png";
export const metadata: Metadata = {
title: "Previously on...",
description: "An incredibly embarrassing and somewhat painful trip down this site's memory lane...",
openGraph: {
...defaultMetadata.openGraph,
title: "Previously on...",
url: "/previously",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/previously",
},
};
export default async function Page() {
return (
<>
<PageTitle>🕰 Previously on...</PageTitle>
<Content
className={styles.wackyWrapper}
style={{
fontFamily: `${ComicNeue.style.fontFamily}, var(--fonts-sans)`,
}}
>
<Figure
src={img_wayback}
href="https://web.archive.org/web/20010501000000*/jakejarvis.com"
alt="Timeline of this website's past."
priority
className={styles.screenshot}
>
...the{" "}
<Link href="https://web.archive.org/web/20010501000000*/jakejarvis.com">Cringey Chronicles&trade;</Link> of
this website's past.
</Figure>
<HorizontalRule className={styles.divider} />
<p style={{ marginBottom: "0.5em" }}>
🚨 Trigger warning: excessive marquees, animated GIFs, Comic Sans, popups,{" "}
<CodeInline
style={{
fontSize: "0.8em",
fontWeight: 400,
}}
>
color: <span style={{ color: "#32cd32" }}>limegreen</span>
</CodeInline>{" "}
ahead...
</p>
<p style={{ fontSize: "0.95em", marginBottom: "0.5em" }}>
<Link href="/y2k">
<svg
fill="currentColor"
stroke="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
role="img"
style={{
display: "inline",
width: "1.2em",
height: "1.2em",
verticalAlign: "-0.15em",
marginRight: "0.1em",
fill: "currentColor",
stroke: "currentcolor",
strokeWidth: 0,
}}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.712 1.596l-.756.068-.238.55.734-.017zm1.39.927l-.978.137-.326.807.96-.12.345-.824zM4.89 3.535l-.72.05-.24.567.721-.017zm3.724.309l-1.287.068-.394.96 1.27-.052zm1.87.566l-1.579.069-.566 1.357 1.596-.088.548-1.338zm-4.188.037l-.977.153-.343.806.976-.12zm6.144.668l-1.87.135-.637 1.527 1.87-.154zm2.925.219c-.11 0-.222 0-.334.002l-.767 1.85c1.394-.03 2.52.089 3.373.38l-1.748 4.201c-.955-.304-2.082-.444-3.36-.394l-.54 1.305a8.762 8.762 0 0 1 3.365.396l-1.663 4.014c-1.257-.27-2.382-.395-3.387-.344l-.782 1.887c3.363-.446 6.348.822 9.009 3.773L24 9.23c-2.325-2.575-5.2-3.88-8.637-3.896zm-.644.002l-2.024.12-.687 1.68 2.025-.19zm-10.603.05l-.719.036-.224.566h.703l.24-.601zm3.69.397l-1.287.069-.395.959 1.27-.05zM5.54 6.3l-.994.154-.344.807.98-.121zm4.137.066l-1.58.069L7.53 7.77l1.596-.085.55-1.32zm1.955.688l-1.87.135-.636 1.527 1.887-.154zm2.282.19l-2.01.136-.7 1.682 2.04-.19.67-1.63zm-10.57.066l-.739.035-.238.564h.72l.257-.6zm3.705.293l-1.303.085-.394.96 1.287-.034zm11.839.255a6.718 6.718 0 0 1 2.777 1.717l-1.75 4.237c-.617-.584-1.15-.961-1.611-1.149l-1.201-.498zM4.733 8.22l-.976.154-.344.807.961-.12.36-.841zm4.186 0l-1.594.052-.549 1.354L8.37 9.54zm1.957.668L8.99 9.04l-.619 1.508 1.87-.135.636-1.527zm2.247.275l-2.007.12-.703 1.665 2.042-.156zM2.52 9.267l-.718.033-.24.549.718-.016zm3.725.273l-1.289.07-.41.96 1.287-.03.412-1zm1.87.6l-1.596.05-.55 1.356 1.598-.084.547-1.322zm-4.186.037l-.979.136-.324.805.96-.119zm6.14.633l-1.87.154-.653 1.527 1.906-.154zm2.267.275l-2.026.12-.686 1.663 2.025-.172zm-10.569.031l-.739.037-.238.565.72-.016zm3.673.362l-1.289.068-.41.978 1.305-.05zm-2.285.533l-.976.154-.326.805.96-.12.342-.84zm4.153.07l-1.596.066-.565 1.356 1.612-.084zm1.957.666l-1.889.154-.617 1.526 1.886-.15zm2.28.223l-2.025.12-.685 1.665 2.041-.172.67-1.613zm-10.584.05l-.738.053L0 13.64l.72-.02.24-.6zm3.705.31l-1.285.07-.395.976 1.287-.05.393-.997zm11.923.07c1.08.29 2.024.821 2.814 1.613l-1.715 4.183c-.892-.754-1.82-1.32-2.814-1.664l1.715-4.133zm-10.036.515L4.956 14l-.549 1.32 1.578-.066.567-1.338zm-4.184.014l-.996.156-.309.79.961-.106zm6.14.67l-1.904.154-.617 1.527 1.89-.154.632-1.527zm2.231.324l-2.025.123-.686 1.682 2.026-.174zm-6.863.328l-1.3.068-.397.98 1.285-.054zm1.871.584l-1.578.068-.566 1.334 1.595-.064zm1.953.701l-1.867.137-.635 1.51 1.87-.137zm2.23.31l-2.005.122-.703 1.68 2.04-.19.67-1.61z"></path>
</svg>{" "}
Click here for the <em>full</em> experience anyway.
</Link>
</p>
<figure className={styles.screenshot}>
<IFrame
src="https://jakejarvis.github.io/my-first-website/"
title="My Terrible, Horrible, No Good, Very Bad First Website"
height={500}
allowScripts
style={{ margin: "0.6em 0" }}
/>
<figcaption>
<Link href="https://jakejarvis.github.io/my-first-website/">November 2001</Link> (
<Link href="https://github.com/jakejarvis/my-first-website">view source</Link>)
</figcaption>
</figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2002_02} className={styles.screenshot}>
February 2002
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2002_10} className={styles.screenshot}>
October 2002
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2003_08} className={styles.screenshot}>
August 2003
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2004_11} className={styles.screenshot}>
November 2004
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2006_04} className={styles.screenshot}>
April 2006
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2006_05} className={styles.screenshot}>
May 2006
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2007_01} className={styles.screenshot}>
January 2007
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2007_04} className={styles.screenshot}>
April 2007
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2007_05} className={styles.screenshot}>
May 2007
</Figure>
<HorizontalRule className={styles.divider} />
<Figure src={img_2009_07} className={styles.screenshot}>
July 2009
</Figure>
<HorizontalRule className={styles.divider} />
<Figure
src={img_2012_09}
href="https://focused-knuth-7bc10d.netlify.app/"
alt="September 2012"
className={styles.screenshot}
>
<Link href="https://focused-knuth-7bc10d.netlify.app/">September 2012</Link> (
<Link href="https://github.com/jakejarvis/jarv.is/tree/v1">view source</Link>)
</Figure>
<HorizontalRule className={styles.divider} />
<Figure
src={img_2018_04}
href="https://hungry-mayer-40e790.netlify.app/"
alt="April 2018"
className={styles.screenshot}
>
<Link href="https://hungry-mayer-40e790.netlify.app/">April 2018</Link> (
<Link href="https://github.com/jakejarvis/jarv.is/tree/v2">view source</Link>)
</Figure>
<HorizontalRule className={styles.divider} />
<Figure
src={img_2020_03}
href="https://quiet-truffle-92842d.netlify.app/"
alt="March 2020"
className={styles.screenshot}
>
<Link href="https://quiet-truffle-92842d.netlify.app/">March 2020</Link> (
<Link href="https://github.com/jakejarvis/jarv.is-hugo">view source</Link>)
</Figure>
</Content>
</>
);
}

138
app/privacy/page.tsx Normal file
View File

@ -0,0 +1,138 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Blockquote from "../../components/Blockquote";
import CodeInline from "../../components/CodeInline";
import { H2 } from "../../components/Heading";
import { UnorderedList, ListItem } from "../../components/List";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy",
openGraph: {
...defaultMetadata.openGraph,
title: "Privacy",
url: "/privacy",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/privacy",
},
};
export default function Page() {
return (
<>
<PageTitle>🕵 Privacy</PageTitle>
<Content>
<p>Okay, this is an easy one. 😉</p>
<H2 id="hosting">Hosting</H2>
<p>
Pages and first-party assets on this website are served by{" "}
<Link href="https://vercel.com/">
<strong> Vercel</strong>
</Link>
. Refer to their <Link href="https://vercel.com/legal/privacy-policy">privacy policy</Link> for more
information.
</p>
<p>
For a likely excessive level of privacy and security, this website is also mirrored on the{" "}
<Link href="https://www.torproject.org/">🧅 Tor network</Link> at:
</p>
<Blockquote style={{ overflowWrap: "break-word" }}>
<Link href="http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion">
<strong>jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion</strong>
</Link>
</Blockquote>
<H2 id="analytics">Analytics</H2>
<p>
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e.{" "}
<CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://turso.tech/">Turso</Link> SQLite database.
Individual views and identifying (or non-identifying) details are <strong>never stored or logged</strong>.
</p>
<p>
The <Link href="https://github.com/jakejarvis/jarv.is/blob/main/prisma/schema.prisma">database schema</Link>,{" "}
<Link href="https://github.com/jakejarvis/jarv.is/blob/main/pages/api/count.ts">serverless function</Link> and{" "}
<Link href="https://github.com/jakejarvis/jarv.is/blob/main/components/HitCounter/HitCounter.tsx">
client script
</Link>{" "}
are open source, and <Link href="https://github.com/jakejarvis/website-stats">snapshots of the database</Link>{" "}
are public.
</p>
<p>
<Link href="https://vercel.com/products/observability">
<strong>Vercel Analytics</strong>
</Link>{" "}
is also used to gain insights into referrers, search terms, etc.{" "}
<Link href="https://vercel.com/docs/analytics/privacy-policy#data-point-information">
without collecting anything identifiable
</Link>{" "}
about you.
</p>
<H2 id="third-party">Third-Party Content</H2>
<p>
Occasionally, embedded content from third-party services is included in posts, and some may contain tracking
code that is outside of my control. Please refer to their privacy policies for more information:
</p>
<UnorderedList>
<ListItem>
<Link href="https://blog.codepen.io/documentation/privacy/">CodePen</Link>
</ListItem>
<ListItem>
<Link href="https://www.facebook.com/policy.php">Facebook</Link>
</ListItem>
<ListItem>
<Link href="https://docs.github.com/en/github/site-policy/github-privacy-statement">GitHub</Link>
</ListItem>
<ListItem>
<Link href="https://soundcloud.com/pages/privacy">SoundCloud</Link>
</ListItem>
<ListItem>
<Link href="https://twitter.com/en/privacy">Twitter</Link>
</ListItem>
<ListItem>
<Link href="https://vimeo.com/privacy">Vimeo</Link>
</ListItem>
<ListItem>
<Link href="https://policies.google.com/privacy">YouTube</Link>
</ListItem>
</UnorderedList>
<H2 id="spam">Fighting Spam</H2>
<p>
Using{" "}
<Link href="https://www.cloudflare.com/products/turnstile/">
<strong>Cloudflare Turnstile</strong>
</Link>{" "}
to fight bot spam on the <Link href="/contact">contact form</Link> was an easy choice over seemingly
unavoidable alternatives like <Link href="https://developers.google.com/recaptcha/">reCAPTCHA</Link>.
</p>
<p>
You can refer to Cloudflare's <Link href="https://www.cloudflare.com/privacypolicy/">privacy policy</Link> and{" "}
<Link href="https://www.cloudflare.com/website-terms/">terms of service</Link> for more details. While some
information is sent to the Turnstile API about your behavior <strong>(on the contact page only)</strong>, at
least you won't be helping a certain internet conglomerate{" "}
<Link href="https://blog.cloudflare.com/moving-from-recaptcha-to-hcaptcha/">
train their self-driving cars
</Link>
. 🚗
</p>
</Content>
</>
);
}

View File

@ -0,0 +1,86 @@
.grid {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
width: 100%;
line-height: 1.1;
}
.card {
flex-grow: 1;
margin: 0.6em;
width: 370px;
padding: 1.2em 1.2em 0.8em 1.2em;
border: 1px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
font-size: 0.85em;
color: var(--colors-mediumDark);
transition: border var(--transitions-fade);
}
.card .name {
font-size: 1.2em;
font-weight: 600;
}
.card .description {
margin-top: 0.7em;
margin-bottom: 0.5em;
line-height: 1.7;
}
.card .meta {
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
.card .metaItem {
margin-right: 1.5em;
line-height: 2;
color: var(--colors-medium);
}
.card .metaLink {
color: inherit !important;
}
.card .metaLink:hover,
.card .metaLink:focus-visible {
color: var(--colors-link) !important;
}
.card .metaIcon {
display: inline;
width: 16px;
height: 16px;
vertical-align: -0.3em;
margin-right: 0.5em;
stroke-width: 0.75;
}
.card .metaLanguage {
display: inline-block;
position: relative;
width: 1.15em;
height: 1.15em;
margin-right: 0.5em;
border-radius: 50%;
vertical-align: text-top;
}
.viewMore {
text-align: center;
margin-bottom: 0;
font-weight: 500px;
}
.githubIcon {
display: inline;
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
margin: 0 0.15em;
fill: var(--colors-text);
}

175
app/projects/page.tsx Normal file
View File

@ -0,0 +1,175 @@
import { graphql } from "@octokit/graphql";
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import commaNumber from "comma-number";
import config from "../../lib/config";
import { metadata as defaultMetadata } from "../layout";
import { GoStar, GoRepoForked } from "react-icons/go";
import { SiGithub } from "react-icons/si";
import type { Metadata } from "next";
import type { User, Repository } from "@octokit/graphql-schema";
import type { Project } from "../../types";
import styles from "./page.module.css";
export const revalidate = 600; // 10 minutes
export const metadata: Metadata = {
title: "Projects",
openGraph: {
...defaultMetadata.openGraph,
title: "Projects",
url: "/projects",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/projects",
},
};
async function getRepos(): Promise<Project[] | null> {
// don't fail the entire site build if the required API key for this page is missing
if (!process.env.GH_PUBLIC_TOKEN || process.env.GH_PUBLIC_TOKEN === "") {
console.warn(`ERROR: I can't fetch any GitHub projects without "GH_PUBLIC_TOKEN" set! Skipping for now...`);
return null;
}
// https://docs.github.com/en/graphql/reference/objects#repository
const { user } = await graphql<{ user: User }>(
`
query ($username: String!, $sort: RepositoryOrderField!, $limit: Int) {
user(login: $username) {
repositories(
first: $limit
isLocked: false
isFork: false
ownerAffiliations: OWNER
privacy: PUBLIC
orderBy: { field: $sort, direction: DESC }
) {
edges {
node {
name
url
description
pushedAt
stargazerCount
forkCount
primaryLanguage {
name
color
}
}
}
}
}
}
`,
{
username: config.authorSocial.github,
sort: "STARGAZERS",
limit: 12,
headers: {
accept: "application/vnd.github.v3+json",
authorization: `token ${process.env.GH_PUBLIC_TOKEN}`,
},
}
);
const results = user.repositories.edges as Array<{ node: Repository }>;
const repos = results.map<Project>(({ node: repo }) => ({
name: repo.name,
url: repo.url,
description: repo.description as string,
updatedAt: repo.pushedAt,
stars: repo.stargazerCount,
forks: repo.forkCount,
language: repo.primaryLanguage as Project["language"],
}));
return repos;
}
export default async function Page() {
const repos = await getRepos();
return (
<>
<PageTitle>💾 Projects</PageTitle>
<Content>
<div className={styles.grid}>
{repos?.map((repo) => (
<div key={repo.name} className={styles.card}>
<Link
// @ts-ignore
href={repo.url}
className={styles.name}
>
{repo.name}
</Link>
{repo.description && <p className={styles.description}>{repo.description}</p>}
<div className={styles.meta}>
{repo.language && (
<div className={styles.metaItem}>
{repo.language.color && (
<span className={styles.metaLanguage} style={{ backgroundColor: repo.language.color }} />
)}
{repo.language.name}
</div>
)}
{repo.stars && repo.stars > 0 && (
<div className={styles.metaItem}>
<Link
// @ts-ignore
href={`${repo.url}/stargazers`}
title={`${commaNumber(repo.stars)} ${repo.stars === 1 ? "star" : "stars"}`}
underline={false}
className={styles.metaLink}
>
<GoStar className={styles.metaIcon} />
{commaNumber(repo.stars)}
</Link>
</div>
)}
{repo.forks && repo.forks > 0 && (
<div className={styles.metaItem}>
<Link
// @ts-ignore
href={`${repo.url}/network/members`}
title={`${commaNumber(repo.forks)} ${repo.forks === 1 ? "fork" : "forks"}`}
underline={false}
className={styles.metaLink}
>
<GoRepoForked className={styles.metaIcon} />
{commaNumber(repo.forks)}
</Link>
</div>
)}
{/* only use relative "time ago" on client side, since it'll be outdated via SSG and cause hydration errors */}
<div className={styles.metaItem}>
<RelativeTime date={repo.updatedAt} verb="Updated" staticFormat="MMM D, YYYY" />
</div>
</div>
</div>
))}
</div>
<p className={styles.viewMore}>
<Link href={`https://github.com/${config.authorSocial.github}`}>
View more on <SiGithub className={styles.githubIcon} /> GitHub...
</Link>
</p>
</Content>
</>
);
}

80
app/robots.ts Normal file
View File

@ -0,0 +1,80 @@
import config from "../lib/config";
import { metadata } from "./layout";
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
const robots = (): MetadataRoute.Robots => {
// I'm already _so_ over this shit...
// https://github.com/ai-robots-txt/ai.robots.txt/blob/main/robots.txt
const naughtySpiders = [
"AI2Bot",
"Ai2Bot-Dolma",
"Amazonbot",
"anthropic-ai",
"Applebot",
"Applebot-Extended",
"Bytespider",
"CCBot",
"ChatGPT-User",
"Claude-Web",
"ClaudeBot",
"cohere-ai",
"cohere-training-data-crawler",
"Crawlspace",
"Diffbot",
"DuckAssistBot",
"FacebookBot",
"FriendlyCrawler",
"Google-Extended",
"GoogleOther",
"GoogleOther-Image",
"GoogleOther-Video",
"GPTBot",
"iaskspider/2.0",
"ICC-Crawler",
"ImagesiftBot",
"img2dataset",
"ISSCyberRiskCrawler",
"Kangaroo Bot",
"Meta-ExternalAgent",
"Meta-ExternalFetcher",
"OAI-SearchBot",
"omgili",
"omgilibot",
"PanguBot",
"PerplexityBot",
"PetalBot",
"Scrapy",
"SemrushBot-OCOB",
"SemrushBot-SWA",
"Sidetrade indexer bot",
"Timpibot",
"VelenPublicWebCrawler",
"Webzio-Extended",
"YouBot",
"AhrefsBot",
"BLEXBot",
"DataForSeoBot",
"magpie-crawler",
"MJ12bot",
"TurnitinBot",
];
return {
rules: [
{
userAgent: "*",
// block access to staging sites
[process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ? "allow" : "disallow"]: "/",
},
{
userAgent: naughtySpiders,
disallow: "/",
},
],
sitemap: new URL("sitemap.xml", metadata.metadataBase?.href || `https://${config.siteDomain}`).href,
};
};
export default robots;

60
app/sitemap.ts Normal file
View File

@ -0,0 +1,60 @@
import path from "path";
import glob from "fast-glob";
import { getAllPosts } from "../lib/helpers/posts";
import { metadata } from "./layout";
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
// start with manual routes
const routes: MetadataRoute.Sitemap = [
{
// homepage
url: "/",
priority: 1.0,
changeFrequency: "weekly",
lastModified: new Date(process.env.RELEASE_DATE || Date.now()), // timestamp frozen when a new build is deployed
},
{
url: "/tweets/",
changeFrequency: "yearly",
},
];
// add each directory in the app folder as a route (excluding special routes)
(
await glob("*", {
cwd: path.join(process.cwd(), "app"),
deep: 0,
onlyDirectories: true,
markDirectories: true,
ignore: [
// don't include special routes, see: https://nextjs.org/docs/app/api-reference/file-conventions/metadata
"api",
"feed.atom",
"feed.xml",
],
})
).forEach((route) => {
routes.push({
// make all URLs absolute
url: route,
});
});
(await getAllPosts()).forEach((post) => {
routes.push({
url: post.permalink,
// pull lastModified from front matter date
lastModified: new Date(post.date),
});
});
// make all URLs absolute
routes.forEach((page) => (page.url = new URL(page.url, metadata.metadataBase || "").href));
return routes;
};
export default sitemap;

63
app/themes.css Normal file
View File

@ -0,0 +1,63 @@
:root {
--colors-backgroundInner: #ffffff;
--colors-backgroundOuter: #fcfcfc;
--colors-backgroundHeader: rgba(252, 252, 252, 0.7);
--colors-text: #202020;
--colors-mediumDark: #515151;
--colors-medium: #5e5e5e;
--colors-mediumLight: #757575;
--colors-light: #d2d2d2;
--colors-kindaLight: #e3e3e3;
--colors-superLight: #f4f4f4;
--colors-superDuperLight: #fbfbfb;
--colors-link: #0e6dc2;
--colors-linkUnderline: rgba(14, 109, 194, 0.4);
--colors-success: #44a248;
--colors-error: #ff1b1b;
--colors-warning: #f78200;
--colors-codeText: #313131;
--colors-codeBackground: #fdfdfd;
--colors-codeComment: #656e77;
--colors-codeKeyword: #029cb9;
--colors-codeAttribute: #70a800;
--colors-codeNamespace: #f92672;
--colors-codeLiteral: #ae81ff;
--colors-codePunctuation: #111111;
--colors-codeVariable: #d88200;
--colors-codeAddition: #44a248;
--colors-codeDeletion: #ff1b1b;
--sizes-maxLayoutWidth: 865px;
--radii-corner: 0.6rem;
--transitions-fade: 0.25s ease;
--transitions-linkHover: 0.2s ease-in-out;
}
[data-theme="dark"] {
--colors-backgroundInner: #1e1e1e;
--colors-backgroundOuter: #252525;
--colors-backgroundHeader: rgba(37, 37, 37, 0.85);
--colors-text: #f1f1f1;
--colors-mediumDark: #d7d7d7;
--colors-medium: #b1b1b1;
--colors-mediumLight: #959595;
--colors-light: #646464;
--colors-kindaLight: #535353;
--colors-superLight: #272727;
--colors-superDuperLight: #1f1f1f;
--colors-link: #88c7ff;
--colors-linkUnderline: rgba(136, 199, 255, 0.4);
--colors-success: #78df55;
--colors-error: #ff5151;
--colors-warning: #f2b702;
--colors-codeText: #e4e4e4;
--colors-codeBackground: #212121;
--colors-codeComment: #929292;
--colors-codeKeyword: #3b9dd2;
--colors-codeAttribute: #78df55;
--colors-codeNamespace: #f95757;
--colors-codeLiteral: #d588fb;
--colors-codePunctuation: #cccccc;
--colors-codeVariable: #fd992a;
--colors-codeAddition: #78df55;
--colors-codeDeletion: #ff5151;
}

812
app/uses/page.tsx Normal file
View File

@ -0,0 +1,812 @@
import Content from "../../components/Content";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Image from "../../components/Image";
import CodeInline from "../../components/CodeInline";
import { H2 } from "../../components/Heading";
import { UnorderedList, ListItem } from "../../components/List";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata, Route } from "next";
import desktopImg from "../../public/static/images/uses/desktop.png";
export const metadata: Metadata = {
title: "/uses",
description: "Things I use daily.",
openGraph: {
...defaultMetadata.openGraph,
title: "/uses",
images: [desktopImg.src],
url: "/uses",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/uses",
},
};
export default function Page() {
return (
<>
<PageTitle>/uses</PageTitle>
<Content>
<p>
<del>I regularly get messages asking about which tools I use to work.</del>
</p>
<p>
Nobody has asked me what I use. Ever. But here's a list of my daily drivers and necessities anyways, mostly
revolving around my Apple sheepy-ness. Inspired by <Link href="https://uses.tech/">many, many others</Link>.
❤️
</p>
<Image src={desktopImg} href={desktopImg.src as Route} alt="My mess of a desktop." priority />
<H2 id="hardware">
<span style={{ marginRight: "0.45em" }}>🚘</span>
Daily Drivers
</H2>
<UnorderedList>
<ListItem>
<Link href="https://browser.geekbench.com/v6/cpu/4493541">
<strong>MacBook Pro</strong> (14-inch)
</Link>
<UnorderedList>
<ListItem>Apple M3 Pro (12core CPU, 18core GPU) 🏎️💨</ListItem>
<ListItem>36 GB RAM</ListItem>
<ListItem>1 TB SSD</ListItem>
<ListItem>Space Black 🖤</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<strong>iPhone 15 Pro</strong>
<UnorderedList>
<ListItem>512 GB in Natural Titanium</ListItem>
<ListItem>
<Link href="https://a.co/d/5ixA4kx">TORRAS Magnetic Shockproof Case</Link> (surprisingly nice, also in
Natural Titanium)
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<strong>iPad Pro 10.5"</strong>
<UnorderedList>
<ListItem>256 GB in Space Gray</ListItem>
<ListItem>Smart Keyboard &amp; Apple Pencil</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<strong>Apple Watch Series 6</strong> (GPS)
<UnorderedList>
<ListItem>Aluminum Space Gray</ListItem>
<ListItem>40mm (I have incredibly small wrists.)</ListItem>
<ListItem>
Usually with the{" "}
<Link href="https://www.apple.com/shop/product/MLL02ZM/A/42mm-midnight-blue-sport-band-s-m-m-l">
Midnight Blue sport band
</Link>
, 🏳️‍🌈{" "}
<Link href="https://www.apple.com/shop/product/MQ4F2AM/A/38mm-pride-edition-woven-nylon">
Pride Edition woven nylon band
</Link>
, or employee 🏋️{" "}
<Link href="https://www.macrumors.com/2018/04/03/apple-employees-rewards-challenge/">
Close Your Rings Challenge woven nylon band
</Link>
.
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<strong>Google Pixel 4a</strong>
<UnorderedList>
<ListItem>For some recreational Android development and experimentation.</ListItem>
<ListItem>
Activated on <Link href="https://fi.google.com/">Google Fi</Link> (
<Link href="https://g.co/fi/r/4X38K6">referral link</Link>) during rare trips.
</ListItem>
<ListItem>
Really just putting this here in a futile effort to prove I'm not a <em>complete</em> Apple sheep. 🐑
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
<H2 id="homelab">
<span style={{ marginRight: "0.45em" }}>🧪</span>
Homelab
</H2>
<UnorderedList>
<ListItem>
<Link href="https://store.ui.com/us/en/collections/unifi-dream-router/products/udr">
<strong>UniFi Dream Router</strong>
</Link>
, plus:
<UnorderedList>
<ListItem>
2x{" "}
<Link href="https://store.ui.com/us/en/collections/unifi-switching-utility-mini/products/usw-flex-mini">
Switch Flex Mini
</Link>
</ListItem>
<ListItem>
<Link href="https://store.ui.com/us/en/products/unifi-smart-power">SmartPower Plug</Link>{" "}
<em>
(<Link href="https://www.youtube.com/watch?v=iW1tHr4Y_cI">It's Comcastic!™</Link>)
</em>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://www.synology.com/en-us/products/DS224+">
<strong>Synology DiskStation DS224+</strong>
</Link>
<UnorderedList>
<ListItem>
2x recertified{" "}
<Link href="https://serverpartdeals.com/products/western-digital-ultrastar-dc-hc550-wuh721816ale6l4-0f38462-16tb-7-2k-rpm-sata-6gb-s-512e-512mb-3-5-se-manufacturer-recertified-hdd">
16TB WD Ultrastar
</Link>{" "}
drives
</ListItem>
<ListItem>
<Link href="https://www.plex.tv/personal-media-server/">
<strong>Plex</strong>
</Link>{" "}
(installed as a package via{" "}
<Link href="https://github.com/michealespinola/syno.plexupdate">
<CodeInline>syno.plexupdate</CodeInline>
</Link>{" "}
for hardware encoding)
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
A <strong>🥧 Raspberry Pi</strong> <Link href="https://a.co/d/bmii52A">cluster</Link> with:
<UnorderedList>
<ListItem>
1x <Link href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4B</Link>, 8GB
RAM
</ListItem>
<UnorderedList>
<ListItem>
Running <Link href="https://www.home-assistant.io/">Home Assistant OS</Link> with a{" "}
<Link href="https://www.home-assistant.io/skyconnect/">SkyConnect USB Dongle</Link>
</ListItem>
</UnorderedList>
<ListItem>
2x <Link href="https://www.raspberrypi.com/products/raspberry-pi-5/">Raspberry Pi 5</Link>, 4GB RAM
</ListItem>
<UnorderedList>
<ListItem>
Running{" "}
<Link href="https://github.com/jakejarvis/dotfiles/tree/main/lab">a few Docker containers</Link>,
including:
<UnorderedList>
<ListItem>
<Link href="https://nextcloud.com/">Nextcloud</Link>
</ListItem>
<ListItem>
<Link href="https://sonarr.tv/">Sonarr</Link>, <Link href="https://radarr.video/">Radarr</Link>,{" "}
<Link href="https://www.bazarr.media/">Bazarr</Link>,{" "}
<Link href="https://github.com/Prowlarr/Prowlarr">Prowlarr</Link>
</ListItem>
<ListItem>
<Link href="https://www.qbittorrent.org/">qBittorrent</Link> (web client)
</ListItem>
<ListItem>
<Link href="https://tautulli.com/">Tautulli</Link>
</ListItem>
<ListItem>
<Link href="https://www.wireguard.com/">WireGuard</Link>
</ListItem>
<ListItem>
<Link href="https://traefik.io/traefik/">Traefik</Link>
</ListItem>
<ListItem>
<Link href="https://www.authelia.com/">Authelia</Link>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>Full post with more details coming soon!</ListItem>
</UnorderedList>
</UnorderedList>
</ListItem>
<ListItem>
<strong>An overpowered custom homelab server</strong>, powered by an{" "}
<Link href="https://www.asus.com/commercial-motherboard/q87me/">ASUS Q87M-E</Link> board,{" "}
<Link href="https://www.intel.com/content/www/us/en/products/sku/80808/intel-core-i74790s-processor-8m-cache-up-to-4-00-ghz/specifications.html">
i7-4790S
</Link>
, 32 GB of RAM, 2x recertified{" "}
<Link href="https://serverpartdeals.com/products/hgst-ultrastar-he10-0f27612-huh721008ale604-8tb-7-2k-rpm-sata-6gb-s-512e-256mb-cache-3-5-se-manufacturer-recertified-hdd">
16TB WD Ultrastar
</Link>{" "}
drives, and <Link href="https://www.proxmox.com/en/proxmox-virtual-environment/overview">Proxmox VE</Link>,
in a cheap <Link href="https://www.thermaltakeusa.com/versa-h22.html">Thermaltake Versa H22</Link> case with
expensive <Link href="https://noctua.at/en/nf-a12x25-pwm">Noctua 🤎</Link> fans.
</ListItem>
</UnorderedList>
<H2 id="development">
<span style={{ marginRight: "0.45em" }}>💾</span>
Development
</H2>
<UnorderedList>
<ListItem>
<Link href="https://iterm2.com/">
<strong>iTerm 2</strong>
</Link>
<UnorderedList>
<ListItem>
My various{" "}
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/zsh/aliases.zsh">ZSH aliases</Link> and{" "}
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/zsh/functions.zsh">functions</Link> are in{" "}
<Link href="https://github.com/jakejarvis/dotfiles">
my <CodeInline>.dotfiles</CodeInline> repository.
</Link>
</ListItem>
<ListItem>
<Link href="https://ohmyz.sh/">Oh My ZSH</Link>
<UnorderedList>
<ListItem>
<Link href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</Link>
</ListItem>
<ListItem>
<Link href="https://github.com/zsh-users/zsh-syntax-highlighting">zsh-syntax-highlighting</Link>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://code.visualstudio.com/">
<strong>Visual Studio Code</strong>
</Link>
<UnorderedList>
<ListItem>
<Link href="https://github.com/jakejarvis/dotfiles/tree/main/vscode">All of my settings.</Link>
</ListItem>
<ListItem>
Themes:
<UnorderedList>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.github-vscode-theme">
GitHub Dark theme
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme">
Material Icon theme
</Link>
</ListItem>
<ListItem>
<Link href="https://developer.apple.com/fonts/">SF Mono font</Link>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
Extensions:
<UnorderedList>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig">
EditorConfig
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">
ESLint
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">
GitHub Pull Requests
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.remotehub">
GitHub Repositories
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost">
Import Cost
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx">MDX</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">
Prettier
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">
Remote Development Pack
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint">
Stylelint
</Link>
</ListItem>
<ListItem>
<Link href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount">
Word Count
</Link>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://brew.sh/">
<strong>Homebrew</strong>
</Link>
<UnorderedList>
<ListItem>
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/Brewfile">
View my messy <CodeInline>Brewfile</CodeInline> dump
</Link>{" "}
with all of my installed packages.
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://developer.apple.com/xcode/">
<strong>Xcode</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.docker.com/products/docker-desktop">
<strong>Docker Desktop</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.parallels.com/products/desktop/">
<strong>Parallels Desktop Pro</strong>
</Link>
<UnorderedList>
<ListItem>
Switched from VMware Fusion when Parallels beat them to the punch with{" "}
<Link href="https://www.parallels.com/blogs/parallels-desktop-m1/">M1 support</Link>.
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://www.sketch.com/">
<strong>Sketch</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.adobe.com/creativecloud.html">
<strong>Adobe Creative Cloud</strong>
</Link>
<UnorderedList>
<ListItem>
Still on the $20/month{" "}
<Link href="https://www.adobe.com/creativecloud/buy/students.html">Student Plan</Link>, somehow. 🤫 Will
need to re-evaulate once I'm kicked off; it's hard to justify spending almost 3x that...
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://panic.com/transmit/">
<strong>Transmit</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.sequelpro.com/">
<del>Sequel Pro</del>
</Link>{" "}
→{" "}
<Link href="https://tableplus.com/">
<strong>TablePlus</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://robomongo.org/">
<del>Robo 3T</del>
</Link>{" "}
→{" "}
<Link href="https://tableplus.com/">
<strong>TablePlus</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://imageoptim.com/mac">
<strong>ImageOptim</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://sipapp.io/">
<strong>Sip</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://localwp.com/">
<strong>Local</strong>
</Link>{" "}
for WordPress development.
</ListItem>
</UnorderedList>
<H2 id="browsing">
<span style={{ marginRight: "0.45em" }}>🌎</span>
Browsing
</H2>
<UnorderedList>
<ListItem>
<Link href="https://www.mozilla.org/en-US/firefox/new/">
<strong>Firefox</strong>
</Link>{" "}
🦊
<UnorderedList>
<ListItem>
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/firefox/user.js">
My default <CodeInline>user.js</CodeInline> settings.
</Link>
</ListItem>
<ListItem>
Extensions:
<UnorderedList>
<ListItem>
<Link href="https://support.1password.com/cs/1password-classic-extension/">1Password Classic</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/betterttv/">BetterTTV</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/darkreader/">Dark Reader</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/decentraleyes/">Decentraleyes</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/mailvelope/">Mailvelope</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/react-devtools/">
React Developer Tools
</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/octolinker/">OctoLinker</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17/">Privacy Badger</Link>
</ListItem>
<ListItem>
<Link href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">uBlock Origin</Link>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Link href="https://www.google.com/chrome/browser/?extra=devchannel">
<strong>Google Chrome</strong>
</Link>{" "}
😈
<UnorderedList>
<ListItem>
For testing only! See more of <Link href="#cloud">my de-Googling efforts below</Link>.
</ListItem>
<ListItem>
Extensions:
<UnorderedList>
<ListItem>
<Link href="https://chrome.google.com/webstore/detail/amp-validator/nmoffdblmcmgeicmolmhobpoocbbmknc?hl=en">
AMP Validator
</Link>
</ListItem>
<ListItem>
<Link href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk?h1=en">
Lighthouse
</Link>
</ListItem>
<ListItem>
<Link href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">
React Developer Tools
</Link>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
</ListItem>
</UnorderedList>
<H2 id="macos">
<span style={{ marginRight: "0.45em" }}>💻</span>
macOS
</H2>
<UnorderedList>
<ListItem>
<Link href="https://1password.com/">
<strong>1Password</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.obdev.at/products/littlesnitch/index.html">
<strong>Little Snitch</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://bjango.com/mac/istatmenus/">
<strong>iStat Menus</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.backblaze.com/">
<strong>Backblaze</strong>
</Link>{" "}
(<Link href="https://secure.backblaze.com/r/00x84e">referral link</Link>)
</ListItem>
<ListItem>
<Link href="https://github.com/billycastelli/Silicon-Info">
<strong>Silicon Info</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.corecode.io/macupdater/">
<strong>MacUpdater</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://freemacsoft.net/appcleaner/">
<strong>AppCleaner</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://gpgtools.org/">
<strong>GPG Suite</strong>
</Link>
</ListItem>
<ListItem>
<del>VLC</del> →{" "}
<Link href="https://iina.io/">
<strong>IINA</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://transmissionbt.com/">
<strong>Transmission</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://roaringapps.com/app/amphetamine">
<strong>Amphetamine</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.tunabellysoftware.com/tgpro/">
<strong>TG Pro</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://coconut-flavour.com/coconutbattery/">
<strong>coconutBattery</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://www.keka.io/en/">
<strong>Keka</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://tapbots.com/ivory/mac/">
<strong>Ivory</strong>
</Link>
</ListItem>
</UnorderedList>
<H2 id="ios">
<span style={{ marginRight: "0.45em" }}>📱</span>
iOS
</H2>
<p>I have far too many apps to count, but here the essentials that have earned a spot on my home screen:</p>
<UnorderedList>
<ListItem>
<Link href="https://apps.apple.com/app/id568903335">
<strong>1Password</strong> (beta)
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id323229106">
<strong>Waze</strong> (beta)
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id414834813">
<strong>Pocket Casts</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id945077360">
<strong>Sling TV</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id383457673">
<strong>Plex</strong> (beta)
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id985746746">
<strong>Discord</strong>
</Link>
</ListItem>
<ListItem>
<Link href="https://apps.apple.com/app/id1057750338">
<strong>UniFi</strong>
</Link>{" "}
and{" "}
<Link href="https://apps.apple.com/app/id1385561119">
<strong>WiFiman</strong>
</Link>
</ListItem>
</UnorderedList>
<H2 id="cloud">
<span style={{ marginRight: "0.45em" }}>☁️</span>
Cloud
</H2>
<p>
I've been making recent efforts to <Link href="https://www.stallman.org/google.html">de-Google</Link> my life,
with mixed results...
</p>
<UnorderedList>
<ListItem>
<del>Gmail</del> {" "}
<Link href="https://www.fastmail.com/">
<strong>Fastmail</strong>
</Link>{" "}
(<Link href="https://ref.fm/u20274504">referral link</Link>) &amp;{" "}
<Link href="https://en.wikipedia.org/wiki/Apple_Mail">
<strong>Mail.app</strong>
</Link>
</ListItem>
<ListItem>
<del>Google Drive</del> <del>Dropbox</del> {" "}
<Link href="https://www.icloud.com/iclouddrive">
<strong>iCloud Drive</strong>
</Link>
<UnorderedList>
<ListItem>
<Link href={"/notes/dropping-dropbox/" as Route}>Read why.</Link>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<del>Google Docs</del> {" "}
<Link href="https://products.office.com/en-us/mac/microsoft-office-for-mac">
<strong>Microsoft Office</strong>
</Link>{" "}
(hey, it works 🤷)
</ListItem>
<ListItem>
<del>Google Photos</del> {" "}
<Link href="https://www.icloud.com/photos/">
<strong>iCloud Photos</strong>
</Link>
</ListItem>
<ListItem>
<del>Google Analytics</del> {" "}
<Link href="https://usefathom.com/">
<strong>Fathom Analytics</strong>
</Link>{" "}
(<Link href="https://usefathom.com/ref/ZEYG0O">referral link</Link>)
</ListItem>
<ListItem>
<del>Google Public DNS</del> {" "}
<Link href="https://1.1.1.1/dns/">
<strong>Cloudflare's 1.1.1.1</strong>
</Link>{" "}
on my home network.
</ListItem>
<ListItem>
<del>GoDaddy</del> <del>Google Domains</del> {" "}
<Link href="https://www.cloudflare.com/products/registrar/">
<strong>Cloudflare Registrar</strong>
</Link>{" "}
(and{" "}
<Link href="https://isnic.is/en/">
<strong>ISNIC</strong>
</Link>{" "}
for this domain, of course 🇮🇸)
</ListItem>
</UnorderedList>
<p>Other geeky stuff:</p>
<UnorderedList>
<ListItem>
<Link href="https://vercel.com/">
<strong>Vercel</strong>
</Link>{" "}
and{" "}
<Link href="https://www.netlify.com/">
<strong>Netlify</strong>
</Link>{" "}
for "serverless" sites.
</ListItem>
<ListItem>
<Link href="https://www.digitalocean.com/">
<strong>DigitalOcean</strong>
</Link>{" "}
(<Link href="https://m.do.co/c/afcf288a7dac">referral link</Link>) and{" "}
<Link href="https://www.linode.com/">
<strong>Linode</strong>
</Link>{" "}
(<Link href="https://www.linode.com/?r=0c5aeace9bd591be9fbf32f96f58470295f1ee05">referral link</Link>) for
virtual Linux servers.
</ListItem>
<ListItem>
<Link href="https://dnsimple.com/">
<strong>DNSimple</strong>
</Link>{" "}
(<Link href="https://dnsimple.com/r/eb6ced548f1e0a">referral link</Link>) and{" "}
<Link href="https://www.cloudflare.com/">
<strong>Cloudflare</strong>
</Link>{" "}
for domain DNS.
</ListItem>
<ListItem>
<Link href="https://www.backblaze.com/">
<strong>Backblaze</strong>
</Link>{" "}
(<Link href="https://secure.backblaze.com/r/00x84e">referral link</Link>) for off-site Mac backups.
</ListItem>
<ListItem>
<Link href="https://gitea.io/en-us/">
<strong>Gitea</strong>
</Link>{" "}
as a <Link href="https://git.jarv.is/">self-hosted</Link> Git backup/mirror.
</ListItem>
<ListItem>
<Link href="https://tailscale.com/">
<strong>Tailscale</strong>
</Link>{" "}
and{" "}
<Link href="https://developers.cloudflare.com/cloudflare-one/">
<strong>Cloudflare Zero Trust</strong>
</Link>{" "}
to access my home network and VPSes from anywhere.
</ListItem>
</UnorderedList>
<H2 id="iot">
<span style={{ marginRight: "0.45em" }}>🏠</span>
Internet of <del>Things</del> <Link href={"/notes/shodan-search-queries/" as Route}>Crap</Link>
</H2>
<UnorderedList>
<ListItem>
<Link href="https://www2.meethue.com/en-us">
<strong>Philips Hue</strong>
</Link>{" "}
color bulbs, dimmer switches, etc.
</ListItem>
<ListItem>
2x{" "}
<Link href="https://www.ecobee.com/en-us/smart-thermostats/smart-wifi-thermostat/">
<strong>ecobee3 lite</strong>
</Link>
</ListItem>
<ListItem>
2x{" "}
<Link href="https://www.sonos.com/en-us/shop/one.html">
<strong>Sonos One</strong>
</Link>{" "}
(with Alexa turned off...hopefully? 🤫)
</ListItem>
<ListItem>
2x{" "}
<Link href="https://www.apple.com/apple-tv-4k/">
<strong>Apple TV 4K</strong> (2021)
</Link>
</ListItem>
</UnorderedList>
</Content>
</>
);
}

77
app/zip/page.tsx Normal file
View File

@ -0,0 +1,77 @@
import Content from "../../components/Content";
import Link from "../../components/Link";
import CodeBlock from "../../components/CodeBlock/CodeBlock";
import { metadata as defaultMetadata } from "../layout";
import type { Metadata } from "next";
import backgroundImg from "../../public/static/images/zip/bg.jpg";
export const metadata: Metadata = {
title: "fuckyougoogle.zip",
description: "This is a horrible idea.",
openGraph: {
...defaultMetadata.openGraph,
title: "fuckyougoogle.zip",
url: "/zip",
},
alternates: {
...defaultMetadata.alternates,
canonical: "/zip",
},
};
export default async function Page() {
return (
<Content
style={{
backgroundImage: `url(${backgroundImg.src})`,
backgroundRepeat: "repeat",
backgroundPosition: "center",
borderRadius: "var(--radii-corner)",
}}
>
<CodeBlock
style={{
backgroundColor: "var(--colors-backgroundHeader)",
backdropFilter: "saturate(180%) blur(5px))",
}}
>
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
<span style={{ color: "var(--colors-codeLiteral)" }}>mv</span> /root
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} underline={false}>
/stable_products_that_people_rely_on/
</Link>
googledomains.zip /tmp/
<br />
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
<span style={{ color: "var(--colors-codeLiteral)" }}>crontab</span>{" "}
<span style={{ color: "var(--colors-codeVariable)" }}>-l</span>
<br />
<br />
<span style={{ color: "var(--colors-codeComment)" }}>
# TODO(someone else): make super duper sure this only deletes actual zip files and *NOT* the sketchy domains
ending with file extensions released by us & purchased on our registrar (which i just yeeted btw cuz i'm bored
& also my evil superpowers are fueled by my reckless disregard for the greater good of the internet). - xoxo
sundar <span style={{ color: "var(--colors-codeNamespace)" }}>&lt;3</span>
</span>
<br />
<span style={{ color: "var(--colors-codeAttribute)" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span style={{ color: "var(--colors-codeLiteral)" }}>rm</span>{" "}
<span style={{ color: "var(--colors-codeVariable )" }}>-f</span> /tmp/
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} underline={false}>
*.zip
</Link>
<br />
<br />
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
<span style={{ color: "var(--colors-codeLiteral)" }}>reboot</span> 0
</CodeBlock>
</Content>
);
}