1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-04-22 04:15:31 -04:00

properly import and optimize/cache images in markdown files

This commit is contained in:
2025-03-03 15:56:57 -05:00
parent 36faa6c234
commit ba10742c9b
71 changed files with 685 additions and 1100 deletions
+1
View File
@@ -1,4 +1,5 @@
.footer {
flex: 1;
width: 100%;
padding: 1.25em 1.5em;
border-top: 1px solid var(--colors-kindaLight);
+4 -4
View File
@@ -15,11 +15,11 @@ const Footer = ({ className, ...rest }: FooterProps) => {
<div className={styles.row}>
<div>
Content{" "}
<Link href="/license" title={config.license} underline={false} className={styles.link}>
<Link href="/license" title={config.license} plain className={styles.link}>
licensed under {config.licenseAbbr}
</Link>
,{" "}
<Link href="/previously" title="Previously on..." underline={false} className={styles.link}>
<Link href="/previously" title="Previously on..." plain className={styles.link}>
{config.copyrightYearStart}
</Link>{" "}
{new Date(process.env.RELEASE_DATE || Date.now()).getUTCFullYear()}.
@@ -35,7 +35,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
href="https://nextjs.org/"
title="Powered by Next.js"
aria-label="Next.js"
underline={false}
plain
className={clsx(styles.link, styles.hover)}
>
<SiNextdotjs className={styles.icon} />
@@ -44,7 +44,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
<Link
href={`https://github.com/${config.githubRepo}`}
title="View Source on GitHub"
underline={false}
plain
className={clsx(styles.link, styles.underline)}
>
View source.
+5 -1
View File
@@ -7,7 +7,11 @@
transition:
background var(--transitions-fade),
border var(--transitions-fade);
z-index: 9999px;
/* make sticky */
position: sticky;
top: 0;
z-index: 1000;
/* blurry glass-like background effect (except on firefox...?) */
backdrop-filter: saturate(180%) blur(5px);
+1 -1
View File
@@ -15,7 +15,7 @@ const Header = ({ className, ...rest }: HeaderProps) => {
return (
<header className={clsx(styles.header, className)} {...rest}>
<nav className={styles.nav}>
<Link href="/" rel="author" title={config.authorName} underline={false} className={styles.selfieLink}>
<Link href="/" rel="author" title={config.authorName} plain className={styles.selfieLink}>
<Image
src={selfieJpg}
alt={`Photo of ${config.authorName}`}
+1 -8
View File
@@ -9,14 +9,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
const HeadingAnchor = ({ id, title, ...rest }: HeadingAnchorProps) => {
return (
<Link
href={`#${id}`}
title={`Jump to "${title}"`}
aria-hidden
underline={false}
style={{ lineHeight: 1 }}
{...rest}
>
<Link href={`#${id}`} title={`Jump to "${title}"`} aria-hidden plain style={{ lineHeight: 1 }} {...rest}>
<FiLink size="0.8em" />
</Link>
);
+16 -54
View File
@@ -1,70 +1,32 @@
import NextImage from "next/image";
import clsx from "clsx";
import Link, { LinkProps } from "../Link";
import type { ComponentPropsWithoutRef } from "react";
import type { ImageProps as NextImageProps, StaticImageData } from "next/image";
import type { StaticImageData } from "next/image";
import styles from "./Image.module.css";
const DEFAULT_QUALITY = 60;
const DEFAULT_WIDTH = 865;
const MAX_WIDTH = 865;
export type ImageProps = ComponentPropsWithoutRef<typeof NextImage> &
Partial<Pick<LinkProps, "href">> & {
inline?: boolean; // don't wrap everything in a `<div>` block
export type ImageProps = ComponentPropsWithoutRef<typeof NextImage> & {
inline?: boolean; // don't wrap everything in a `<div>` block
};
const Image = ({ src, height, width, quality, inline, className, ...rest }: ImageProps) => {
const constrainWidth = (width?: number | `${number}`) => {
if (!width) return MAX_WIDTH;
return Math.min(typeof width === "string" ? parseInt(width, 10) : width, MAX_WIDTH);
};
const Image = ({
src,
width,
height,
quality = DEFAULT_QUALITY,
placeholder,
href,
inline,
className,
...rest
}: ImageProps) => {
const imageProps: NextImageProps = {
// strip "px" from dimensions: https://stackoverflow.com/a/4860249/1438024
width: typeof width === "string" ? Number.parseInt(width, 10) : width,
height: typeof height === "string" ? Number.parseInt(height, 10) : height,
quality,
const imageProps = {
src,
placeholder,
height,
width: constrainWidth(width || (src as StaticImageData).width),
quality: quality || 75,
...rest,
};
if (typeof src === "object" && (src as StaticImageData).src !== undefined) {
const staticImg = src as StaticImageData;
// all data for statically imported images is extracted from the object itself.
imageProps.src = staticImg;
// set image width to max layout width; height is calculated automatically via aspect ratio:
// https://github.com/vercel/next.js/pull/40278
imageProps.width = staticImg.width > DEFAULT_WIDTH ? DEFAULT_WIDTH : imageProps.width;
// default to blur placeholder while loading if it's been generated for us.
imageProps.placeholder = placeholder || (staticImg.blurDataURL !== undefined ? "blur" : "empty");
} else if (typeof src === "string") {
// regular path to a file was passed in, which makes explicit width and height required.
// https://nextjs.org/docs/api-reference/next/future/image#width
if (!(width && height)) {
throw new Error("'width' and 'height' are required for non-statically imported images.");
}
// optionally prepending src with "/public" makes images resolve properly in GitHub markdown previews, etc.
imageProps.src = src.replace(/^\/public/g, "");
} else {
throw new TypeError("'src' should be a string or a valid StaticImageData object.");
}
const StyledImageWithProps = href ? (
<Link href={href} underline={false}>
<NextImage className={clsx(styles.image, className)} {...imageProps} />
</Link>
) : (
<NextImage className={clsx(styles.image, className)} {...imageProps} />
);
const StyledImageWithProps = <NextImage className={clsx(styles.image, className)} {...imageProps} />;
return inline ? StyledImageWithProps : <div className={styles.block}>{StyledImageWithProps}</div>;
};
-10
View File
@@ -14,13 +14,3 @@
margin: 0 auto;
display: block;
}
.stickyHeader {
position: sticky;
top: 0;
z-index: 1000;
}
.flexedFooter {
flex: 1;
}
+2 -2
View File
@@ -14,14 +14,14 @@ const Layout = ({ className, children, ...rest }: LayoutProps) => {
<SkipToContentLink />
<div className={clsx(styles.flex, className)} {...rest}>
<Header className={styles.stickyHeader} />
<Header />
<main className={styles.default}>
<SkipToContentTarget />
<div className={styles.container}>{children}</div>
</main>
<Footer className={styles.flexedFooter} />
<Footer />
</div>
</>
);
+12 -7
View File
@@ -1,23 +1,28 @@
.link {
color: var(--colors-link);
text-decoration: none;
}
.link.underline {
/* fancy underline */
background-image: linear-gradient(var(--colors-linkUnderline), var(--colors-linkUnderline));
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% 2px;
transition: background-size 0.2s ease-in-out;
padding-bottom: 3px;
}
.link.underline:hover,
.link.underline:focus-visible {
.link:hover,
.link:focus-visible {
background-size: 100% 2px;
}
@media (prefers-reduced-motion: no-preference) {
.link.underline {
transition: background-size var(--transitions-linkHover);
.link.plain {
background: none !important;
transition: none !important;
}
@media (prefers-reduced-motion: reduce) {
.link {
transition: none !important;
}
}
+6 -19
View File
@@ -1,33 +1,20 @@
import NextLink from "next/link";
import clsx from "clsx";
import objStr from "obj-str";
import config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css";
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
underline?: boolean;
plain?: boolean; // disable fancy text-decoration effect
openInNewTab?: boolean;
};
const Link = ({
href,
rel,
target,
prefetch = false,
underline = true,
openInNewTab,
className,
...rest
}: LinkProps) => {
const Link = ({ href, rel, target, prefetch = false, plain, openInNewTab, className, ...rest }: LinkProps) => {
// This component auto-detects whether or not this link should open in the same window (the default for internal
// links) or a new tab (the default for external links). Defaults can be overridden with `openInNewTab={true}`.
const isExternal =
typeof href === "string" &&
!(
["/", "#"].includes(href[0]) ||
(process.env.NEXT_PUBLIC_BASE_URL && href.startsWith(process.env.NEXT_PUBLIC_BASE_URL))
);
const isExternal = typeof href === "string" && !(["/", "#"].includes(href[0]) || href.startsWith(config.baseUrl));
if (openInNewTab || isExternal) {
return (
@@ -40,7 +27,7 @@ const Link = ({
noreferrer: isExternal, // don't add "noreferrer" if link isn't external, and only opening in a new tab
})}
prefetch={false}
className={clsx(styles.link, underline && styles.underline, className)}
className={clsx(styles.link, plain && styles.plain, className)}
{...rest}
/>
);
@@ -49,7 +36,7 @@ const Link = ({
// If link is to an internal page, simply pass *everything* along as-is to next/link.
return (
<NextLink
className={clsx(styles.link, underline && styles.underline, className)}
className={clsx(styles.link, plain && styles.plain, className)}
{...{ href, rel, target, prefetch, ...rest }}
/>
);
+1 -1
View File
@@ -28,7 +28,7 @@ const Menu = ({ className, ...rest }: MenuProps) => {
})}
<li className={styles.menuItem}>
<MenuItem Icon={ThemeToggle} />
<MenuItem icon={ThemeToggle} />
</li>
</ul>
);
+5 -3
View File
@@ -6,14 +6,16 @@ import type { IconType } from "react-icons";
import styles from "./MenuItem.module.css";
export type MenuItemProps = {
Icon?: IconType;
text?: string;
href?: Route;
icon?: IconType;
current?: boolean;
className?: string;
};
const MenuItem = ({ Icon, text, href, current, className }: MenuItemProps) => {
const MenuItem = ({ text, href, icon, current, className }: MenuItemProps) => {
const Icon = icon;
const item = (
<>
{Icon && <Icon className={styles.icon} />}
@@ -28,7 +30,7 @@ const MenuItem = ({ Icon, text, href, current, className }: MenuItemProps) => {
href={href}
className={clsx(styles.link, current && styles.current, className)}
title={text}
underline={false}
plain
aria-label={text}
>
{item}
+1 -1
View File
@@ -11,7 +11,7 @@ export type OctocatLinkProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href
const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => {
return (
<Link href={`https://github.com/${repo}`} underline={false} className={styles.octocatLink} {...rest}>
<Link href={`https://github.com/${repo}`} plain className={styles.octocatLink} {...rest}>
<SiGithub className={clsx(styles.octocat, className)} />
</Link>
);
+1 -1
View File
@@ -15,7 +15,7 @@ const PageTitle = ({ className, children, ...rest }: PageTitleProps) => {
return (
<h1 className={clsx(styles.title, className)} {...rest}>
<Link href={pathname as Route} underline={false} className={styles.link}>
<Link href={pathname as Route} plain className={styles.link}>
{children}
</Link>
</h1>
+1 -1
View File
@@ -1,6 +1,6 @@
.tweet {
/* help with layout shift */
min-height: 300px;
min-height: 120px;
}
.tweet :global(.react-tweet-theme) {
+34 -19
View File
@@ -1,32 +1,47 @@
import { Suspense } from "react";
import Image from "next/image";
import { Tweet } from "react-tweet";
import { EmbeddedTweet, TweetSkeleton, TweetNotFound } from "react-tweet";
import { getTweet } from "react-tweet/api";
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./TweetEmbed.module.css";
export type TweetEmbedProps = ComponentPropsWithoutRef<typeof Tweet> & {
export type TweetEmbedProps = Omit<ComponentPropsWithoutRef<typeof EmbeddedTweet>, "tweet"> & {
id: string;
className?: string;
};
const TweetEmbed = ({ id, className, ...rest }: TweetEmbedProps) => {
return (
<div className={clsx(styles.tweet, className)}>
<Tweet
key={`tweet-${id}`}
id={id}
components={{
// https://react-tweet.vercel.app/twitter-theme/api-reference#custom-tweet-components
// eslint-disable-next-line jsx-a11y/alt-text
AvatarImg: (props) => <Image {...props} />,
// eslint-disable-next-line jsx-a11y/alt-text
MediaImg: (props) => <Image {...props} fill />,
}}
{...rest}
/>
</div>
);
const TweetEmbed = async ({ id, className, ...rest }: TweetEmbedProps) => {
try {
const tweet = await getTweet(id);
return (
<div className={clsx(styles.tweet, className)}>
<Suspense fallback={<TweetSkeleton />}>
{tweet ? (
<EmbeddedTweet
tweet={tweet}
components={{
// https://react-tweet.vercel.app/twitter-theme/api-reference#custom-tweet-components
// eslint-disable-next-line jsx-a11y/alt-text
AvatarImg: (props) => <Image {...props} unoptimized />,
// eslint-disable-next-line jsx-a11y/alt-text
MediaImg: (props) => <Image {...props} fill unoptimized />,
}}
{...rest}
/>
) : (
<TweetNotFound />
)}
</Suspense>
</div>
);
} catch (
error // eslint-disable-line @typescript-eslint/no-unused-vars
) {
return <TweetNotFound />;
}
};
export default TweetEmbed;
+3 -3
View File
@@ -10,14 +10,14 @@ export type VideoProps = Omit<Partial<ComponentPropsWithoutRef<"video">>, "src">
mp4?: string;
// optional:
vtt?: string;
image?: string;
};
poster?: string;
autoplay?: boolean;
responsive?: boolean;
className?: string;
};
const Video = ({ src, autoplay = false, responsive = true, className, ...rest }: VideoProps) => {
const Video = ({ src, poster, autoplay = false, responsive = true, className, ...rest }: VideoProps) => {
if (!src || (!src.mp4 && !src.webm)) {
throw new Error("'src' prop must include either 'mp4' or 'webm' URL.");
}
@@ -34,7 +34,7 @@ const Video = ({ src, autoplay = false, responsive = true, className, ...rest }:
playsInline={autoplay} // safari autoplay workaround
loop={autoplay || undefined}
muted={autoplay || undefined}
poster={src.image}
poster={poster}
{...rest}
>
{src.webm && <source key={src.webm} src={src.webm} type="video/webm" />}