1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 18:15:33 -04:00

fix: batch server requests from posts list

This commit is contained in:
2026-01-28 14:37:37 -05:00
parent 9d8e775fcd
commit 4dca81b58a
50 changed files with 160 additions and 143 deletions
+3
View File
@@ -35,3 +35,6 @@ yarn.lock
# vercel # vercel
.vercel .vercel
.env*.local .env*.local
# next-agents-md
.next-docs/
+2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -10,4 +10,4 @@ const Analytics = () => {
); );
}; };
export default Analytics; export { Analytics };
+2 -2
View File
@@ -1,7 +1,7 @@
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg"; import { JsonLd } from "react-schemaorg";
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import Video from "@/components/video"; import { Video } from "@/components/video";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import type { VideoObject } from "schema-dts"; import type { VideoObject } from "schema-dts";
+1 -1
View File
@@ -1,4 +1,4 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({ export const metadata = createMetadata({
+2 -2
View File
@@ -1,5 +1,5 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import ContactForm from "@/components/contact-form"; import { ContactForm } from "@/components/contact-form";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({ export const metadata = createMetadata({
+2 -1
View File
@@ -193,9 +193,10 @@
::selection { ::selection {
@apply bg-selection text-selection-foreground; @apply bg-selection text-selection-foreground;
} }
/* https://ui.shadcn.com/docs/components/button#cursor */
button:not(:disabled), button:not(:disabled),
[role="button"]:not(:disabled) { [role="button"]:not(:disabled) {
@apply cursor-pointer; cursor: pointer;
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg"; import { JsonLd } from "react-schemaorg";
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import Video from "@/components/video"; import { Video } from "@/components/video";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import type { VideoObject } from "schema-dts"; import type { VideoObject } from "schema-dts";
+4 -4
View File
@@ -1,11 +1,11 @@
import { ViewTransition } from "react"; import { ViewTransition } from "react";
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg"; import { JsonLd } from "react-schemaorg";
import Providers from "@/components/providers"; import { Providers } from "@/components/providers";
import Header from "@/components/layout/header"; import { Header } from "@/components/layout/header";
import Footer from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import Analytics from "@/app/analytics"; import { Analytics } from "@/app/analytics";
import { defaultMetadata } from "@/lib/metadata"; import { defaultMetadata } from "@/lib/metadata";
import { GeistSans, GeistMono } from "@/lib/fonts"; import { GeistSans, GeistMono } from "@/lib/fonts";
import siteConfig from "@/lib/config/site"; import siteConfig from "@/lib/config/site";
+2 -2
View File
@@ -1,7 +1,7 @@
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg"; import { JsonLd } from "react-schemaorg";
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import Video from "@/components/video"; import { Video } from "@/components/video";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import type { VideoObject } from "schema-dts"; import type { VideoObject } from "schema-dts";
+1 -1
View File
@@ -1,4 +1,4 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({ export const metadata = createMetadata({
+1 -1
View File
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Video from "@/components/video"; import { Video } from "@/components/video";
import Link from "next/link"; import Link from "next/link";
import type { Metadata } from "next"; import type { Metadata } from "next";
+4 -4
View File
@@ -3,10 +3,10 @@ import { Suspense } from "react";
import Link from "next/link"; import Link from "next/link";
import { JsonLd } from "react-schemaorg"; import { JsonLd } from "react-schemaorg";
import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon, MessagesSquareIcon } from "lucide-react"; import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon, MessagesSquareIcon } from "lucide-react";
import ViewCounter from "@/components/view-counter"; import { ViewCounter } from "@/components/view-counter";
import CommentCount from "@/components/comment-count"; import { CommentCount } from "@/components/comment-count";
import Comments from "@/components/comments/comments"; import { Comments } from "@/components/comments/comments";
import CommentsSkeleton from "@/components/comments/comments-skeleton"; import { CommentsSkeleton } from "@/components/comments/comments-skeleton";
import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts"; import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import siteConfig from "@/lib/config/site"; import siteConfig from "@/lib/config/site";
+7 -5
View File
@@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import PostStats from "@/components/post-stats"; import { PostStats, PostStatsProvider } from "@/components/post-stats";
import { getFrontMatter, POSTS_DIR, type FrontMatter } from "@/lib/posts"; import { getFrontMatter, POSTS_DIR, type FrontMatter } from "@/lib/posts";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import authorConfig from "@/lib/config/author"; import authorConfig from "@/lib/config/author";
@@ -61,12 +61,12 @@ const PostsList = async () => {
{dateDisplay} {dateDisplay}
</time> </time>
</span> </span>
<div className="space-x-2.5"> <div className="space-x-2">
{/* htmlTitle is sanitized by rehypeSanitize in lib/posts.ts with strict allowlist: only code, em, strong tags */} {/* htmlTitle is sanitized by rehypeSanitize in lib/posts.ts with strict allowlist: only code, em, strong tags */}
<Link <Link
href={`/${POSTS_DIR}/${slug}`} href={`/${POSTS_DIR}/${slug}`}
dangerouslySetInnerHTML={{ __html: htmlTitle || title }} dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
className="underline-offset-4 hover:underline" className="mr-2.5 underline-offset-4 hover:underline"
style={{ viewTransitionName: `note-title-${slug}` }} style={{ viewTransitionName: `note-title-${slug}` }}
/> />
@@ -87,7 +87,9 @@ const Page = async () => {
return ( return (
<> <>
<PageTitle canonical="/notes">Notes</PageTitle> <PageTitle canonical="/notes">Notes</PageTitle>
<PostsList /> <PostStatsProvider>
<PostsList />
</PostStatsProvider>
</> </>
); );
}; };
+2 -2
View File
@@ -1,5 +1,5 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import Marquee from "@/components/marquee"; import { Marquee } from "@/components/marquee";
import { Win95Icon } from "@/components/icons"; import { Win95Icon } from "@/components/icons";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import { PageStyles } from "./page-styles"; import { PageStyles } from "./page-styles";
+1 -1
View File
@@ -1,4 +1,4 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({ export const metadata = createMetadata({
+3 -3
View File
@@ -3,9 +3,9 @@ import { Suspense } from "react";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { GitForkIcon, StarIcon } from "lucide-react"; import { GitForkIcon, StarIcon } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import RelativeTime from "@/components/relative-time"; import { RelativeTime } from "@/components/relative-time";
import ActivityCalendar from "@/components/activity-calendar"; import { ActivityCalendar } from "@/components/activity-calendar";
import { GitHubIcon } from "@/components/icons"; import { GitHubIcon } from "@/components/icons";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
+1 -1
View File
@@ -1,4 +1,4 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({ export const metadata = createMetadata({
+1 -1
View File
@@ -1,4 +1,4 @@
import PageTitle from "@/components/layout/page-title"; import { PageTitle } from "@/components/layout/page-title";
import { createMetadata } from "@/lib/metadata"; import { createMetadata } from "@/lib/metadata";
import backgroundImg from "./sundar.jpg"; import backgroundImg from "./sundar.jpg";
+4 -4
View File
@@ -1,10 +1,10 @@
"use client"; "use client";
import { ActivityCalendar, type Activity } from "react-activity-calendar"; import { ActivityCalendar as ActivityCalendarPrimitive, type Activity } from "react-activity-calendar";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Calendar = ({ const ActivityCalendar = ({
data, data,
noun = "thing", noun = "thing",
className, className,
@@ -24,7 +24,7 @@ const Calendar = ({
)} )}
{...rest} {...rest}
> >
<ActivityCalendar <ActivityCalendarPrimitive
data={data} data={data}
colorScheme="dark" colorScheme="dark"
theme={{ theme={{
@@ -55,4 +55,4 @@ const Calendar = ({
); );
}; };
export default Calendar; export { ActivityCalendar };
+2 -2
View File
@@ -1,6 +1,6 @@
import { codeToHtml } from "shiki"; import { codeToHtml } from "shiki";
import { cacheLife } from "next/cache"; import { cacheLife } from "next/cache";
import CopyButton from "@/components/copy-button"; import { CopyButton } from "@/components/copy-button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
/** /**
@@ -72,4 +72,4 @@ const CodeBlock = async ({ children, className, showLineNumbers = true, ...props
); );
}; };
export default CodeBlock; export { CodeBlock };
+1 -1
View File
@@ -36,4 +36,4 @@ const CommentCount = ({ slug }: { slug: string }) => {
); );
}; };
export default CommentCount; export { CommentCount };
+4 -4
View File
@@ -10,7 +10,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import Form from "./comment-form"; import { CommentForm } from "./comment-form";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { deleteComment, type CommentWithUser } from "@/lib/server/comments"; import { deleteComment, type CommentWithUser } from "@/lib/server/comments";
@@ -42,7 +42,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
return ( return (
<div className="mt-4"> <div className="mt-4">
{isEditing ? ( {isEditing ? (
<Form <CommentForm
slug={comment.pageSlug} slug={comment.pageSlug}
initialContent={comment.content} initialContent={comment.content}
commentId={comment.id} commentId={comment.id}
@@ -81,7 +81,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
{isReplying && ( {isReplying && (
<div className="mt-4"> <div className="mt-4">
<Form <CommentForm
slug={comment.pageSlug} slug={comment.pageSlug}
parentId={comment.id} parentId={comment.id}
onCancel={() => setIsReplying(false)} onCancel={() => setIsReplying(false)}
@@ -93,4 +93,4 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
); );
}; };
export default CommentActions; export { CommentActions };
+1 -1
View File
@@ -193,4 +193,4 @@ const CommentForm = ({
); );
}; };
export default CommentForm; export { CommentForm };
+4 -4
View File
@@ -2,8 +2,8 @@ import { getImageProps } from "next/image";
import Link from "next/link"; import Link from "next/link";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import RelativeTime from "@/components/relative-time"; import { RelativeTime } from "@/components/relative-time";
import Actions from "./comment-actions"; import { CommentActions } from "./comment-actions";
import { remarkGfm, remarkSmartypants } from "@/lib/remark"; import { remarkGfm, remarkSmartypants } from "@/lib/remark";
import { rehypeExternalLinks } from "@/lib/rehype"; import { rehypeExternalLinks } from "@/lib/rehype";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -66,11 +66,11 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
</Markdown> </Markdown>
</div> </div>
<Actions comment={comment} /> <CommentActions comment={comment} />
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default CommentSingle; export { CommentSingle };
+3 -3
View File
@@ -1,4 +1,4 @@
import Single from "./comment-single"; import { CommentSingle } from "./comment-single";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { CommentWithUser } from "@/lib/server/comments"; import type { CommentWithUser } from "@/lib/server/comments";
@@ -18,7 +18,7 @@ const CommentThread = ({
return ( return (
<> <>
<Single comment={comment} /> <CommentSingle comment={comment} />
{replies.length > 0 && ( {replies.length > 0 && (
<div className={cn("mt-6 space-y-6", level < maxLevel && "ml-6 border-l-2 pl-6")}> <div className={cn("mt-6 space-y-6", level < maxLevel && "ml-6 border-l-2 pl-6")}>
@@ -37,4 +37,4 @@ const CommentThread = ({
); );
}; };
export default CommentThread; export { CommentThread };
+1 -1
View File
@@ -23,4 +23,4 @@ const CommentsSkeleton = () => {
); );
}; };
export default CommentsSkeleton; export { CommentsSkeleton };
+6 -6
View File
@@ -1,7 +1,7 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
import Form from "./comment-form"; import { CommentForm } from "./comment-form";
import Thread from "./comment-thread"; import { CommentThread } from "./comment-thread";
import SignIn from "./sign-in"; import { SignIn } from "./sign-in";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { getComments, type CommentWithUser } from "@/lib/server/comments"; import { getComments, type CommentWithUser } from "@/lib/server/comments";
@@ -29,7 +29,7 @@ const Comments = async ({ slug }: { slug: string }) => {
return ( return (
<> <>
{session ? ( {session ? (
<Form slug={slug} /> <CommentForm slug={slug} />
) : ( ) : (
<div className="bg-muted/40 flex flex-col items-center justify-center gap-y-4 rounded-lg p-6"> <div className="bg-muted/40 flex flex-col items-center justify-center gap-y-4 rounded-lg p-6">
<p className="text-center font-medium">Join the discussion by signing in:</p> <p className="text-center font-medium">Join the discussion by signing in:</p>
@@ -40,7 +40,7 @@ const Comments = async ({ slug }: { slug: string }) => {
{rootComments.length > 0 ? ( {rootComments.length > 0 ? (
<div className="space-y-6"> <div className="space-y-6">
{rootComments.map((comment: CommentWithUser) => ( {rootComments.map((comment: CommentWithUser) => (
<Thread <CommentThread
key={comment.id} key={comment.id}
comment={comment} comment={comment}
replies={commentsByParentId[comment.id] || []} replies={commentsByParentId[comment.id] || []}
@@ -57,4 +57,4 @@ const Comments = async ({ slug }: { slug: string }) => {
); );
}; };
export default Comments; export { Comments };
+1 -1
View File
@@ -34,4 +34,4 @@ const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
); );
}; };
export default SignIn; export { SignIn };
+1 -1
View File
@@ -163,4 +163,4 @@ const ContactForm = () => {
); );
}; };
export default ContactForm; export { ContactForm };
+1 -1
View File
@@ -67,4 +67,4 @@ function CopyButton({
); );
} }
export default CopyButton; export { CopyButton };
+1 -1
View File
@@ -3,4 +3,4 @@
// marking the library as a proper client component so that react doesn't complain about hydration whenever we use it in // marking the library as a proper client component so that react doesn't complain about hydration whenever we use it in
// a server component. // a server component.
// see: https://react.dev/reference/rsc/use-client#using-third-party-libraries // see: https://react.dev/reference/rsc/use-client#using-third-party-libraries
export { default } from "react-countup"; export { default as CountUp } from "react-countup";
+1 -1
View File
@@ -18,4 +18,4 @@ const HeadingAnchor = ({ id, title, className }: { id: string; title: string; cl
); );
}; };
export default HeadingAnchor; export { HeadingAnchor };
+1 -1
View File
@@ -127,4 +127,4 @@ const ImageDiff = ({ children, className }: { children: React.ReactElement[]; cl
); );
}; };
export default ImageDiff; export { ImageDiff };
+1 -1
View File
@@ -23,4 +23,4 @@ const Footer = () => {
); );
}; };
export default Footer; export { Footer };
+2 -2
View File
@@ -6,7 +6,7 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import Menu from "@/components/layout/menu"; import { Menu } from "@/components/layout/menu";
import { GitHubIcon } from "@/components/icons"; import { GitHubIcon } from "@/components/icons";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import authorConfig from "@/lib/config/author"; import authorConfig from "@/lib/config/author";
@@ -90,4 +90,4 @@ const Header = ({ className }: { className?: string }) => {
); );
}; };
export default Header; export { Header };
+1 -1
View File
@@ -87,4 +87,4 @@ const Menu = () => {
); );
}; };
export default Menu; export { Menu };
+1 -1
View File
@@ -21,4 +21,4 @@ const PageTitle = ({
); );
}; };
export default PageTitle; export { PageTitle };
+1 -1
View File
@@ -24,4 +24,4 @@ const Marquee = ({
); );
}; };
export default Marquee; export { Marquee };
+60 -29
View File
@@ -1,55 +1,86 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { EyeIcon, MessagesSquareIcon } from "lucide-react"; import { EyeIcon, MessagesSquareIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { getViewCount } from "@/lib/server/views"; import { getAllViewCounts } from "@/lib/server/views";
import { getCommentCount } from "@/lib/server/comments"; import { getAllCommentCounts } from "@/lib/server/comments";
const numberFormatter = new Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE); const numberFormatter = new Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE);
const PostStats = ({ slug }: { slug: string }) => { type Stats = {
const [stats, setStats] = useState<{ views: number; comments: number } | null>(null); views: Record<string, number>;
comments: Record<string, number>;
loaded: boolean;
};
const StatsContext = createContext<Stats>({ views: {}, comments: {}, loaded: false });
/**
* Provider that fetches ALL post stats in a single batch (2 requests total).
* Wrap this around any component tree that contains PostStats components.
*/
export const PostStatsProvider = ({ children }: { children: ReactNode }) => {
const [stats, setStats] = useState<Stats>({ views: {}, comments: {}, loaded: false });
useEffect(() => { useEffect(() => {
Promise.all([getViewCount(slug), getCommentCount(slug)]) Promise.all([getAllViewCounts(), getAllCommentCounts()])
.then(([views, comments]) => { .then(([views, comments]) => {
setStats({ views, comments }); setStats({ views, comments, loaded: true });
}) })
.catch((err) => { .catch((err) => {
console.error("[post-stats] error:", err); console.error("[post-stats] error fetching stats:", err);
// Silently fail - just don't show stats setStats({ views: {}, comments: {}, loaded: true });
}); });
}, [slug]); }, []);
if (!stats) { return <StatsContext.Provider value={stats}>{children}</StatsContext.Provider>;
return null; // No loading state - badges just appear when ready };
/**
* Displays view/comment badges for a single post.
* Must be used within a PostStatsProvider.
*/
const PostStats = ({ slug }: { slug: string }) => {
const { views, comments, loaded } = useContext(StatsContext);
if (!loaded) {
return (
<>
<Skeleton className="inline-block h-5 w-12 rounded-full align-text-top" />
<Skeleton className="inline-block h-5 w-8 rounded-full align-text-top" />
</>
);
} }
const viewCount = views[slug] ?? 0;
const commentCount = comments[slug] ?? 0;
return ( return (
<> <>
{stats.views > 0 && ( {viewCount > 0 && (
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none"> <Badge variant="secondary" className="tabular-nums">
<EyeIcon className="inline-block size-4 shrink-0" aria-hidden="true" /> <EyeIcon className="text-foreground/85" aria-hidden="true" />
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(stats.views)}</span> {numberFormatter.format(viewCount)}
</span> </Badge>
)} )}
{stats.comments > 0 && ( {commentCount > 0 && (
<Link <Badge variant="secondary" className="tabular-nums" asChild>
href={`/${slug}#comments`} <Link
title={`${numberFormatter.format(stats.comments)} ${stats.comments === 1 ? "comment" : "comments"}`} href={`/${slug}#comments`}
className="inline-flex hover:no-underline" title={`${numberFormatter.format(commentCount)} ${commentCount === 1 ? "comment" : "comments"}`}
> >
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none"> <MessagesSquareIcon className="text-foreground/85" aria-hidden="true" />
<MessagesSquareIcon className="inline-block size-3 shrink-0" aria-hidden="true" /> {numberFormatter.format(commentCount)}
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(stats.comments)}</span> </Link>
</span> </Badge>
</Link>
)} )}
</> </>
); );
}; };
export default PostStats; export { PostStats };
+1 -1
View File
@@ -10,4 +10,4 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
); );
}; };
export default Providers; export { Providers };
+1 -1
View File
@@ -17,4 +17,4 @@ const RelativeTime = ({ ...rest }: React.ComponentProps<typeof TimeAgo>) => {
); );
}; };
export default RelativeTime; export { RelativeTime };
+1 -1
View File
@@ -28,4 +28,4 @@ const CodePen = ({
); );
}; };
export default CodePen; export { CodePen };
+1 -1
View File
@@ -52,4 +52,4 @@ const Gist = async ({
); );
}; };
export default Gist; export { Gist };
+1 -1
View File
@@ -57,4 +57,4 @@ const Tweet = async ({ id, className }: { id: string; className?: string }) => {
return <TweetContent data={data} className={className} />; return <TweetContent data={data} className={className} />;
}; };
export default Tweet; export { Tweet };
+1 -1
View File
@@ -8,4 +8,4 @@ const YouTube = ({ ...rest }: Omit<React.ComponentProps<typeof YouTubeEmbed>, "t
return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />; return <YouTubeEmbed cookie={false} containerElement="div" title="" {...rest} />;
}; };
export default YouTube; export { YouTube };
+1 -1
View File
@@ -47,4 +47,4 @@ const Video = ({
); );
}; };
export default Video; export { Video };
+2 -2
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import CountUp from "@/components/count-up"; import { CountUp } from "@/components/count-up";
import { incrementViews } from "@/lib/server/views"; import { incrementViews } from "@/lib/server/views";
const ViewCounter = ({ slug }: { slug: string }) => { const ViewCounter = ({ slug }: { slug: string }) => {
@@ -36,4 +36,4 @@ const ViewCounter = ({ slug }: { slug: string }) => {
); );
}; };
export default ViewCounter; export { ViewCounter };
+7 -7
View File
@@ -1,12 +1,12 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import CodeBlock from "@/components/code-block"; import { CodeBlock } from "@/components/code-block";
import Video from "@/components/video"; import { Video } from "@/components/video";
import ImageDiff from "@/components/image-diff"; import { ImageDiff } from "@/components/image-diff";
import Tweet from "@/components/third-party/tweet"; import { Tweet } from "@/components/third-party/tweet";
import YouTube from "@/components/third-party/youtube"; import { YouTube } from "@/components/third-party/youtube";
import Gist from "@/components/third-party/gist"; import { Gist } from "@/components/third-party/gist";
import CodePen from "@/components/third-party/codepen"; import { CodePen } from "@/components/third-party/codepen";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { MDXComponents } from "mdx/types"; import type { MDXComponents } from "mdx/types";
+4 -26
View File
@@ -6,7 +6,6 @@ import "./lib/env";
const nextConfig = { const nextConfig = {
cacheComponents: true, cacheComponents: true,
reactStrictMode: true,
reactCompiler: true, reactCompiler: true,
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
images: { images: {
@@ -45,30 +44,6 @@ const nextConfig = {
}, },
}, },
headers: async () => [ headers: async () => [
{
// matches any path
source: "/(.*)",
headers: [
{
key: "strict-transport-security",
value: "max-age=63072000",
},
{
// 🥛 debugging
key: "x-got-milk",
value: "2%",
},
],
},
{
source: "/api/auth/(.*)",
headers: [
{
key: "cache-control",
value: "private, max-age=0",
},
],
},
// https://community.torproject.org/onion-services/advanced/onion-location/ // https://community.torproject.org/onion-services/advanced/onion-location/
...(process.env.NEXT_PUBLIC_ONION_DOMAIN ...(process.env.NEXT_PUBLIC_ONION_DOMAIN
? [ ? [
@@ -92,9 +67,12 @@ const nextConfig = {
source: "/tweets/:path*", source: "/tweets/:path*",
destination: "https://tweets-khaki.vercel.app/:path*", destination: "https://tweets-khaki.vercel.app/:path*",
}, },
{
source: "/y2k/:path*",
destination: "https://y2k.pages.dev/:path*",
},
], ],
redirects: async () => [ redirects: async () => [
{ source: "/y2k", destination: "https://y2k.pages.dev", permanent: false },
{ {
source: "/pubkey.asc", source: "/pubkey.asc",
destination: destination: