pre-render optimizations
@ -7,7 +7,8 @@ const Analytics = () => {
|
||||
|
||||
return (
|
||||
<Script
|
||||
src="/_stream/u/script.js" // see next.config.ts rewrite
|
||||
src="/_stream/u/script.js" // see middleware rewrite
|
||||
id="umami-js"
|
||||
strategy="afterInteractive"
|
||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
data-domains={process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}
|
||||
|
BIN
app/birthday/opengraph-image.png
Normal file
After Width: | Height: | Size: 128 KiB |
@ -11,7 +11,6 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
|
||||
images: [thumbnail.src],
|
||||
url: "/birthday",
|
||||
},
|
||||
alternates: {
|
||||
|
BIN
app/cli/opengraph-image.png
Normal file
After Width: | Height: | Size: 110 KiB |
@ -1,15 +1,12 @@
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
|
||||
import featuredImage from "./screenshot.png";
|
||||
|
||||
export const metadata = {
|
||||
title: "CLI",
|
||||
description: "AKA, the most useless Node module ever published, in history, by anyone, ever.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "CLI",
|
||||
images: [featuredImage.src],
|
||||
url: "/cli",
|
||||
},
|
||||
alternates: {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
import { Resend } from "resend";
|
||||
import config from "../../lib/config";
|
||||
import * as config from "../../lib/config";
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
|
@ -3,7 +3,7 @@
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid var(--colors-light);
|
||||
border-radius: var(--radii-corner);
|
||||
border-radius: 0.6em;
|
||||
color: var(--colors-text);
|
||||
background-color: var(--colors-superDuperLight);
|
||||
}
|
||||
@ -36,7 +36,7 @@
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: var(--radii-corner);
|
||||
border-radius: 0.6em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
|
BIN
app/hillary/opengraph-image.png
Normal file
After Width: | Height: | Size: 283 KiB |
@ -12,7 +12,6 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "My Brief Apperance in Hillary Clinton's DNC Video",
|
||||
images: [thumbnail.src],
|
||||
url: "/hillary",
|
||||
},
|
||||
alternates: {
|
||||
|
@ -13,7 +13,6 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: var(--sizes-maxLayoutWidth);
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import { ThemeProvider } from "../contexts/ThemeContext";
|
||||
import Header from "../components/Header";
|
||||
import Footer from "../components/Footer";
|
||||
import { SkipToContentLink, SkipToContentTarget } from "../components/SkipToContent";
|
||||
import config from "../lib/config";
|
||||
import * as config from "../lib/config";
|
||||
import { BASE_URL, MAX_WIDTH } from "../lib/config/constants";
|
||||
import type { Metadata } from "next";
|
||||
import type { Person, WithContext } from "schema-dts";
|
||||
|
||||
@ -15,10 +16,10 @@ import "./global.css";
|
||||
|
||||
import styles from "./layout.module.css";
|
||||
|
||||
import meJpg from "../public/static/me.jpg";
|
||||
import ogImage from "./opengraph-image.jpg";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(config.baseUrl),
|
||||
metadataBase: new URL(BASE_URL),
|
||||
title: {
|
||||
template: `%s – ${config.siteName}`,
|
||||
default: `${config.siteName} – ${config.shortDescription}`,
|
||||
@ -64,10 +65,10 @@ const jsonLd: WithContext<Person> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: config.authorName,
|
||||
url: config.baseUrl,
|
||||
image: `${config.baseUrl}${meJpg.src}`,
|
||||
url: BASE_URL,
|
||||
image: `${BASE_URL}${ogImage.src}`,
|
||||
sameAs: [
|
||||
config.baseUrl,
|
||||
BASE_URL,
|
||||
`https://github.com/${config.authorSocial?.github}`,
|
||||
`https://keybase.io/${config.authorSocial?.keybase}`,
|
||||
`https://twitter.com/${config.authorSocial?.twitter}`,
|
||||
@ -96,7 +97,9 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
<main className={styles.default}>
|
||||
<SkipToContentTarget />
|
||||
<div className={styles.container}>{children}</div>
|
||||
<div className={styles.container} style={{ maxWidth: MAX_WIDTH }}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
BIN
app/leo/opengraph-image.png
Normal file
After Width: | Height: | Size: 74 KiB |
@ -12,7 +12,6 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: 'Facebook App on "The Lab with Leo Laporte"',
|
||||
images: [thumbnail.src],
|
||||
url: "/leo",
|
||||
},
|
||||
alternates: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import config from "../lib/config";
|
||||
import * as config from "../lib/config";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const manifest = (): MetadataRoute.Manifest => {
|
||||
|
@ -3,7 +3,7 @@ import Video from "../components/Video";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "404 Not Found",
|
||||
title: "Page Not Found",
|
||||
description: null,
|
||||
openGraph: {},
|
||||
alternates: {
|
||||
@ -23,7 +23,7 @@ const Page = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<h1>404: Page Not Found 😢</h1>
|
||||
<h1 style={{ margin: "0.2em auto" }}>Page Not Found 😢</h1>
|
||||
|
||||
<Link href="/">Go home?</Link>
|
||||
</div>
|
||||
|
@ -1,13 +1,11 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
|
||||
import { ImageResponse } from "next/og";
|
||||
import { notFound } from "next/navigation";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import glob from "fast-glob";
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { readFile } from "fs/promises";
|
||||
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
|
||||
import { POSTS_DIR, AVATAR_PATH } from "../../../lib/config/constants";
|
||||
|
||||
export const dynamicParams = false;
|
||||
export const contentType = "image/png";
|
||||
export const size = {
|
||||
// https://developers.facebook.com/docs/sharing/webmasters/images/
|
||||
@ -15,6 +13,10 @@ export const size = {
|
||||
height: 630,
|
||||
};
|
||||
|
||||
// generate and cache these images at build-time for each slug, since doing this on-demand is mega slow...
|
||||
export const dynamic = "force-static";
|
||||
export const dynamicParams = false;
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const slugs = await getPostSlugs();
|
||||
|
||||
@ -24,29 +26,35 @@ export const generateStaticParams = async () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const getLocalImage = async (src: string) => {
|
||||
const imagePath = await glob(src);
|
||||
if (imagePath.length > 0) {
|
||||
const imageData = await fs.readFile(path.join(process.cwd(), imagePath[0]));
|
||||
return Uint8Array.from(imageData).buffer;
|
||||
}
|
||||
const getLocalImage = async (src: string): Promise<ArrayBuffer | string> => {
|
||||
// https://stackoverflow.com/questions/5775469/whats-the-valid-way-to-include-an-image-with-no-src/14115340#14115340
|
||||
const NO_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||||
|
||||
// image doesn't exist
|
||||
return null;
|
||||
const imagePath = join(process.cwd(), src);
|
||||
|
||||
try {
|
||||
if (!existsSync(imagePath)) {
|
||||
console.error(`[og-image] couldn't find an image file located at "${imagePath}"`);
|
||||
|
||||
// return a 1x1 transparent gif if the image doesn't exist instead of crashing
|
||||
return NO_IMAGE;
|
||||
}
|
||||
|
||||
// return the raw image data as a buffer
|
||||
return Uint8Array.from(await readFile(imagePath)).buffer;
|
||||
} catch (error) {
|
||||
// fail silently and return a 1x1 transparent gif instead of crashing
|
||||
console.error(`[og-image] found "${imagePath}" but couldn't read it:`, error);
|
||||
return NO_IMAGE;
|
||||
}
|
||||
};
|
||||
|
||||
const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
// get the note's title and image filename from its frontmatter
|
||||
const { title, image } = await getFrontMatter(slug);
|
||||
|
||||
// load the image specified in the note's frontmatter from its directory
|
||||
const imageSrc = await getLocalImage(`notes/${slug}/${image}`);
|
||||
|
||||
// load the author avatar
|
||||
const avatarSrc = await getLocalImage("public/static/me.jpg");
|
||||
// get the post's title and image filename from its frontmatter
|
||||
const { title, image: imagePath } = await getFrontMatter(slug);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
@ -59,7 +67,7 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
background: "linear-gradient(0deg, hsla(197, 14%, 57%, 1) 0%, hsla(192, 17%, 94%, 1) 100%)",
|
||||
}}
|
||||
>
|
||||
{imageSrc && (
|
||||
{imagePath && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@ -67,14 +75,15 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
<img
|
||||
// @ts-expect-error
|
||||
src={imageSrc}
|
||||
src={await getLocalImage(`${POSTS_DIR}/${slug}/${imagePath}`)}
|
||||
style={{ objectFit: "cover", height: "100%", width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{avatarSrc && (
|
||||
{AVATAR_PATH && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@ -83,9 +92,10 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
top: 42,
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
<img
|
||||
// @ts-expect-error
|
||||
src={avatarSrc}
|
||||
src={await getLocalImage(AVATAR_PATH)}
|
||||
style={{ height: 96, width: 96, borderRadius: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
@ -117,9 +127,8 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
{
|
||||
name: "Geist",
|
||||
// load the Geist font directly from its npm package
|
||||
data: await fs.readFile(
|
||||
path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")
|
||||
),
|
||||
// IMPORTANT: include this exact path in next.config.ts under "outputFileTracingIncludes"
|
||||
data: await readFile(join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")),
|
||||
style: "normal",
|
||||
weight: 600,
|
||||
},
|
||||
@ -127,7 +136,7 @@ const Image = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[og-image] Error generating image:", error);
|
||||
console.error("[og-image] error generating image:", error);
|
||||
notFound();
|
||||
}
|
||||
};
|
||||
|
@ -8,7 +8,8 @@ import Loading from "../../../components/Loading";
|
||||
import HitCounter from "./counter";
|
||||
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
|
||||
import { metadata as defaultMetadata } from "../../layout";
|
||||
import config from "../../../lib/config";
|
||||
import * as config from "../../../lib/config";
|
||||
import { BASE_URL } from "../../../lib/config/constants";
|
||||
import type { Metadata, Route } from "next";
|
||||
import type { Article, WithContext } from "schema-dts";
|
||||
|
||||
@ -72,7 +73,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: config.authorName,
|
||||
url: config.baseUrl,
|
||||
url: BASE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Link from "../../components/Link";
|
||||
import Time from "../../components/Time";
|
||||
import { getAllPosts } from "../../lib/helpers/posts";
|
||||
import config from "../../lib/config";
|
||||
import * as config from "../../lib/config";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { ReactElement } from "react";
|
||||
import type { Metadata, Route } from "next";
|
||||
|
BIN
app/opengraph-image.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
18
app/page.tsx
@ -2,27 +2,11 @@ import hash from "@emotion/hash";
|
||||
import { rgba } from "polished";
|
||||
import { LockIcon } from "lucide-react";
|
||||
import UnstyledLink from "../components/Link";
|
||||
import { metadata as defaultMetadata } from "./layout";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { Metadata, Route } from "next";
|
||||
import type { Route } from "next";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
import meJpg from "../public/static/me.jpg";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
images: [
|
||||
{
|
||||
url: meJpg.src,
|
||||
width: meJpg.width,
|
||||
height: meJpg.height,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const Link = ({
|
||||
lightColor,
|
||||
darkColor,
|
||||
|
BIN
app/previously/opengraph-image.png
Normal file
After Width: | Height: | Size: 86 KiB |
@ -24,7 +24,6 @@ body:has(.wackyWrapper) a {
|
||||
display: block;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid var(--colors-kindaLight);
|
||||
border-radius: var(--radii-corner);
|
||||
}
|
||||
|
||||
.screenshot,
|
||||
|
@ -13,7 +13,7 @@
|
||||
width: 370px;
|
||||
padding: 1.2em 1.2em 0.8em;
|
||||
border: 1px solid var(--colors-kindaLight);
|
||||
border-radius: var(--radii-corner);
|
||||
border-radius: 1em;
|
||||
font-size: 0.9em;
|
||||
color: var(--colors-mediumDark);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import RelativeTime from "../../components/RelativeTime";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import config from "../../lib/config";
|
||||
import * as config from "../../lib/config";
|
||||
import type { Metadata } from "next";
|
||||
import type { User, Repository } from "@octokit/graphql-schema";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import config from "../lib/config";
|
||||
import { BASE_URL } from "../lib/config/constants";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@ -74,7 +74,7 @@ const robots = (): MetadataRoute.Robots => {
|
||||
disallow: "/",
|
||||
},
|
||||
],
|
||||
sitemap: `${config.baseUrl}/sitemap.xml`,
|
||||
sitemap: `${BASE_URL}/sitemap.xml`,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import path from "path";
|
||||
import glob from "fast-glob";
|
||||
import { getAllPosts } from "../lib/helpers/posts";
|
||||
import config from "../lib/config";
|
||||
import { BASE_URL } from "../lib/config/constants";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@ -11,13 +11,13 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
|
||||
const routes: MetadataRoute.Sitemap = [
|
||||
{
|
||||
// homepage
|
||||
url: config.baseUrl,
|
||||
url: BASE_URL,
|
||||
priority: 1.0,
|
||||
lastModified: new Date(process.env.RELEASE_DATE || Date.now()), // timestamp frozen when a new build is deployed
|
||||
},
|
||||
{ url: `${config.baseUrl}/stats` },
|
||||
{ url: `${config.baseUrl}/tweets` },
|
||||
{ url: `${config.baseUrl}/y2k` },
|
||||
{ url: `${BASE_URL}/stats` },
|
||||
{ url: `${BASE_URL}/tweets` },
|
||||
{ url: `${BASE_URL}/y2k` },
|
||||
];
|
||||
|
||||
// add each directory in the app folder as a route (excluding special routes)
|
||||
@ -35,7 +35,7 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
|
||||
).forEach((route) => {
|
||||
routes.push({
|
||||
// remove matching page.(tsx|mdx) file and make all URLs absolute
|
||||
url: `${config.baseUrl}/${route.replace(/\/page\.(tsx|mdx)$/, "")}`,
|
||||
url: `${BASE_URL}/${route.replace(/\/page\.(tsx|mdx)$/, "")}`,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -26,8 +26,6 @@
|
||||
--colors-codeVariable: #d88200;
|
||||
--colors-codeAddition: #44a248;
|
||||
--colors-codeDeletion: #ff1b1b;
|
||||
--sizes-maxLayoutWidth: 865px;
|
||||
--radii-corner: 0.6rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
BIN
app/uses/opengraph-image.png
Normal file
After Width: | Height: | Size: 810 KiB |
@ -1,15 +1,12 @@
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
|
||||
import featuredImage from "./desktop.png";
|
||||
|
||||
export const metadata = {
|
||||
title: "/uses",
|
||||
description: "Things I use daily.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "/uses",
|
||||
images: [featuredImage.src],
|
||||
url: "/uses",
|
||||
},
|
||||
alternates: {
|
||||
|
BIN
app/zip/opengraph-image.jpg
Normal file
After Width: | Height: | Size: 477 KiB |
@ -26,7 +26,7 @@ const Page = () => {
|
||||
backgroundImage: `url(${backgroundImg.src})`,
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundPosition: "center",
|
||||
borderRadius: "var(--radii-corner)",
|
||||
borderRadius: "0.6em",
|
||||
}}
|
||||
>
|
||||
<CodeBlock
|
||||
|