1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 08:05:23 -04:00

experimental prefetching on hover for common links

This commit is contained in:
Jake Jarvis 2025-04-15 09:30:20 -04:00
parent fe7076f495
commit cfe77f98d6
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
10 changed files with 50 additions and 37 deletions

View File

@ -4,6 +4,7 @@ import CountUp from "../../../components/CountUp";
import redis from "../../../lib/redis"; import redis from "../../../lib/redis";
const HitCounter = async ({ slug }: { slug: string }) => { const HitCounter = async ({ slug }: { slug: string }) => {
// ensure this component isn't triggered by prerenders and/or preloads
await connection(); await connection();
try { try {

View File

@ -125,7 +125,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
className={styles.metaItem} className={styles.metaItem}
style={{ style={{
// fix potential layout shift when number of hits loads // fix potential layout shift when number of hits loads
minWidth: "7em", minWidth: "6em",
marginRight: 0, marginRight: 0,
}} }}
> >

View File

@ -40,7 +40,11 @@ const Page = async () => {
<li className={styles.post} key={slug}> <li className={styles.post} key={slug}>
<Time date={date} format="MMM d" className={styles.date} /> <Time date={date} format="MMM d" className={styles.date} />
<span> <span>
<Link href={`/${POSTS_DIR}/${slug}`} dangerouslySetInnerHTML={{ __html: htmlTitle || title }} /> <Link
dynamicOnHover
href={`/${POSTS_DIR}/${slug}`}
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
/>
</span> </span>
</li> </li>
))} ))}

View File

@ -15,7 +15,7 @@ const Header = ({ className, ...rest }: HeaderProps) => {
return ( return (
<header className={clsx(styles.header, className)} {...rest}> <header className={clsx(styles.header, className)} {...rest}>
<nav className={styles.nav}> <nav className={styles.nav}>
<Link href="/" rel="author" aria-label={config.authorName} plain className={styles.home}> <Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}>
<Image <Image
src={avatarImg} src={avatarImg}
alt={`Photo of ${config.authorName}`} alt={`Photo of ${config.authorName}`}

View File

@ -5,18 +5,24 @@ import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css"; import styles from "./Link.module.css";
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & { export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
plain?: boolean; // disable fancy text-decoration effect /** Disables fancy text-decoration effect when true. */
plain?: boolean;
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
dynamicOnHover?: boolean;
}; };
const Link = ({ href, rel, target, prefetch = false, plain, className, ...rest }: LinkProps) => { const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, className, ...rest }: LinkProps) => {
// This component auto-detects whether or not this link should open in the same window (the default for internal // 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 `target="_blank"`. // links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]); const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
return ( return (
<NextLink <NextLink
prefetch={dynamicOnHover ? null : prefetch}
// @ts-expect-error
unstable_dynamicOnHover={dynamicOnHover}
href={href} href={href}
prefetch={prefetch}
target={target || (isExternal ? "_blank" : undefined)} target={target || (isExternal ? "_blank" : undefined)}
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined} rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
className={clsx( className={clsx(

View File

@ -26,6 +26,7 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
if (href) { if (href) {
return ( return (
<Link <Link
dynamicOnHover
href={href} href={href}
aria-label={text} aria-label={text}
plain plain

View File

@ -94,7 +94,7 @@ export const env = createEnv({
/** /**
* Optional. Locale code to define the site's language in ISO-639 format. Defaults to `en-US`. * Optional. Locale code to define the site's language in ISO-639 format. Defaults to `en-US`.
* *
* @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes#Table * @see https://www.loc.gov/standards/iso639-2/php/code_list.php
*/ */
NEXT_PUBLIC_SITE_LOCALE: v.optional(v.string(), "en-US"), NEXT_PUBLIC_SITE_LOCALE: v.optional(v.string(), "en-US"),

View File

@ -45,6 +45,7 @@ const nextConfig: NextConfig = {
experimental: { experimental: {
reactCompiler: true, // https://react.dev/learn/react-compiler reactCompiler: true, // https://react.dev/learn/react-compiler
ppr: "incremental", // https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering#using-partial-prerendering ppr: "incremental", // https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering#using-partial-prerendering
dynamicOnHover: true,
scrollRestoration: true, scrollRestoration: true,
serverActions: { serverActions: {
allowedOrigins: ["jarv.is", "jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion"], allowedOrigins: ["jarv.is", "jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion"],

View File

@ -75,7 +75,7 @@
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"@types/prop-types": "^15.7.14", "@types/prop-types": "^15.7.14",
"@types/react": "^19.1.1", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.2",
"@types/react-is": "^19.0.0", "@types/react-is": "^19.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", "babel-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411",

58
pnpm-lock.yaml generated
View File

@ -25,13 +25,13 @@ importers:
version: 3.1.0(acorn@8.14.1) version: 3.1.0(acorn@8.14.1)
'@mdx-js/react': '@mdx-js/react':
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(@types/react@19.1.1)(react@19.1.0) version: 3.1.0(@types/react@19.1.2)(react@19.1.0)
'@next/bundle-analyzer': '@next/bundle-analyzer':
specifier: 15.3.1-canary.8 specifier: 15.3.1-canary.8
version: 15.3.1-canary.8 version: 15.3.1-canary.8
'@next/mdx': '@next/mdx':
specifier: 15.3.1-canary.8 specifier: 15.3.1-canary.8
version: 15.3.1-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.1)(react@19.1.0)) version: 15.3.1-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))
'@octokit/graphql': '@octokit/graphql':
specifier: ^8.2.2 specifier: ^8.2.2
version: 8.2.2 version: 8.2.2
@ -91,7 +91,7 @@ importers:
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
react-innertext: react-innertext:
specifier: ^1.1.5 specifier: ^1.1.5
version: 1.1.5(@types/react@19.1.1)(react@19.1.0) version: 1.1.5(@types/react@19.1.2)(react@19.1.0)
react-is: react-is:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0 version: 19.1.0
@ -103,7 +103,7 @@ importers:
version: 2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.3) version: 2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.3)
react-textarea-autosize: react-textarea-autosize:
specifier: ^8.5.9 specifier: ^8.5.9
version: 8.5.9(@types/react@19.1.1)(react@19.1.0) version: 8.5.9(@types/react@19.1.2)(react@19.1.0)
react-timeago: react-timeago:
specifier: ^8.2.0 specifier: ^8.2.0
version: 8.2.0(react@19.1.0) version: 8.2.0(react@19.1.0)
@ -178,11 +178,11 @@ importers:
specifier: ^15.7.14 specifier: ^15.7.14
version: 15.7.14 version: 15.7.14
'@types/react': '@types/react':
specifier: ^19.1.1 specifier: ^19.1.2
version: 19.1.1 version: 19.1.2
'@types/react-dom': '@types/react-dom':
specifier: ^19.1.2 specifier: ^19.1.2
version: 19.1.2(@types/react@19.1.1) version: 19.1.2(@types/react@19.1.2)
'@types/react-is': '@types/react-is':
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.0.0 version: 19.0.0
@ -922,8 +922,8 @@ packages:
'@types/react-is@19.0.0': '@types/react-is@19.0.0':
resolution: {integrity: sha512-71dSZeeJ0t3aoPyY9x6i+JNSvg5m9EF2i2OlSZI5QoJuI8Ocgor610i+4A10TQmURR+0vLwcVCEYFpXdzM1Biw==} resolution: {integrity: sha512-71dSZeeJ0t3aoPyY9x6i+JNSvg5m9EF2i2OlSZI5QoJuI8Ocgor610i+4A10TQmURR+0vLwcVCEYFpXdzM1Biw==}
'@types/react@19.1.1': '@types/react@19.1.2':
resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==} resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==}
'@types/supports-color@8.1.3': '@types/supports-color@8.1.3':
resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==}
@ -4198,10 +4198,10 @@ snapshots:
- acorn - acorn
- supports-color - supports-color
'@mdx-js/react@3.1.0(@types/react@19.1.1)(react@19.1.0)': '@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0)':
dependencies: dependencies:
'@types/mdx': 2.0.13 '@types/mdx': 2.0.13
'@types/react': 19.1.1 '@types/react': 19.1.2
react: 19.1.0 react: 19.1.0
'@napi-rs/wasm-runtime@0.2.8': '@napi-rs/wasm-runtime@0.2.8':
@ -4224,12 +4224,12 @@ snapshots:
dependencies: dependencies:
fast-glob: 3.3.1 fast-glob: 3.3.1
'@next/mdx@15.3.1-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.1)(react@19.1.0))': '@next/mdx@15.3.1-canary.8(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@19.1.0))':
dependencies: dependencies:
source-map: 0.7.4 source-map: 0.7.4
optionalDependencies: optionalDependencies:
'@mdx-js/loader': 3.1.0(acorn@8.14.1) '@mdx-js/loader': 3.1.0(acorn@8.14.1)
'@mdx-js/react': 3.1.0(@types/react@19.1.1)(react@19.1.0) '@mdx-js/react': 3.1.0(@types/react@19.1.2)(react@19.1.0)
'@next/swc-darwin-arm64@15.3.1-canary.8': '@next/swc-darwin-arm64@15.3.1-canary.8':
optional: true optional: true
@ -4483,15 +4483,15 @@ snapshots:
'@types/prop-types@15.7.14': {} '@types/prop-types@15.7.14': {}
'@types/react-dom@19.1.2(@types/react@19.1.1)': '@types/react-dom@19.1.2(@types/react@19.1.2)':
dependencies: dependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
'@types/react-is@19.0.0': '@types/react-is@19.0.0':
dependencies: dependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
'@types/react@19.1.1': '@types/react@19.1.2':
dependencies: dependencies:
csstype: 3.1.3 csstype: 3.1.3
@ -7048,9 +7048,9 @@ snapshots:
react: 19.1.0 react: 19.1.0
scheduler: 0.26.0 scheduler: 0.26.0
react-innertext@1.1.5(@types/react@19.1.1)(react@19.1.0): react-innertext@1.1.5(@types/react@19.1.2)(react@19.1.0):
dependencies: dependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
react: 19.1.0 react: 19.1.0
react-is@16.13.1: {} react-is@16.13.1: {}
@ -7072,12 +7072,12 @@ snapshots:
schema-dts: 1.1.5 schema-dts: 1.1.5
typescript: 5.8.3 typescript: 5.8.3
react-textarea-autosize@8.5.9(@types/react@19.1.1)(react@19.1.0): react-textarea-autosize@8.5.9(@types/react@19.1.2)(react@19.1.0):
dependencies: dependencies:
'@babel/runtime': 7.27.0 '@babel/runtime': 7.27.0
react: 19.1.0 react: 19.1.0
use-composed-ref: 1.4.0(@types/react@19.1.1)(react@19.1.0) use-composed-ref: 1.4.0(@types/react@19.1.2)(react@19.1.0)
use-latest: 1.3.0(@types/react@19.1.1)(react@19.1.0) use-latest: 1.3.0(@types/react@19.1.2)(react@19.1.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
@ -7980,24 +7980,24 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
use-composed-ref@1.4.0(@types/react@19.1.1)(react@19.1.0): use-composed-ref@1.4.0(@types/react@19.1.2)(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
optionalDependencies: optionalDependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
use-isomorphic-layout-effect@1.2.0(@types/react@19.1.1)(react@19.1.0): use-isomorphic-layout-effect@1.2.0(@types/react@19.1.2)(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
optionalDependencies: optionalDependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
use-latest@1.3.0(@types/react@19.1.1)(react@19.1.0): use-latest@1.3.0(@types/react@19.1.2)(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
use-isomorphic-layout-effect: 1.2.0(@types/react@19.1.1)(react@19.1.0) use-isomorphic-layout-effect: 1.2.0(@types/react@19.1.2)(react@19.1.0)
optionalDependencies: optionalDependencies:
'@types/react': 19.1.1 '@types/react': 19.1.2
use-sync-external-store@1.5.0(react@19.1.0): use-sync-external-store@1.5.0(react@19.1.0):
dependencies: dependencies: