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:
@@ -1,4 +1,5 @@
|
||||
.footer {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1.25em 1.5em;
|
||||
border-top: 1px solid var(--colors-kindaLight);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -14,13 +14,3 @@
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stickyHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.flexedFooter {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ const Menu = ({ className, ...rest }: MenuProps) => {
|
||||
})}
|
||||
|
||||
<li className={styles.menuItem}>
|
||||
<MenuItem Icon={ThemeToggle} />
|
||||
<MenuItem icon={ThemeToggle} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
.tweet {
|
||||
/* help with layout shift */
|
||||
min-height: 300px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.tweet :global(.react-tweet-theme) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
Reference in New Issue
Block a user