1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-27 17:45:41 -04:00

re-enable comments on non-post pages

This commit is contained in:
2025-05-18 14:38:10 -04:00
parent 3f56632313
commit a9d83768ca
19 changed files with 211 additions and 83 deletions

103
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,103 @@
# Copilot Instructions
This file provides guidance to GitHub Copilot when working with code in this repository.
## Project Overview
This is a personal website (jarv.is) built with Next.js, TypeScript, and various modern web technologies. The site uses the Next.js App Router and is designed to be deployed on Vercel. It includes features like blog posts (notes), projects showcase, contact form, and hit counter API.
## Development Commands
```bash
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Lint code
pnpm lint
# Type check
pnpm typecheck
# Database migrations (using Drizzle)
npx drizzle-kit generate
```
## Environment Setup
The project requires several environment variables to function properly. Copy `.env.example` to a new `.env` file and populate the required values. The environment variables are documented and type-checked in `lib/env.ts`.
Required server environment variables:
- `AUTH_SECRET`: Random value for authentication encryption
- `AUTH_GITHUB_CLIENT_ID`: Client ID from GitHub OAuth App
- `AUTH_GITHUB_CLIENT_SECRET`: Client secret from GitHub OAuth App
- `DATABASE_URL`: PostgreSQL connection string (Neon)
- `GITHUB_TOKEN`: GitHub API token for projects page
- `RESEND_API_KEY`: Resend API key for contact form
- `RESEND_TO_EMAIL`: Destination email for contact form
- `TURNSTILE_SECRET_KEY`: Cloudflare Turnstile secret key
Required client environment variables:
- `NEXT_PUBLIC_GITHUB_REPO`: Repository in format "username/repo"
- `NEXT_PUBLIC_GITHUB_USERNAME`: GitHub username
- `NEXT_PUBLIC_TURNSTILE_SITE_KEY`: Cloudflare Turnstile site key
## Architecture
### Core Technologies
- **Next.js App Router**: Modern React framework with server components
- **TypeScript**: Type safety throughout the project
- **Tailwind CSS**: Styling with utility classes
- **Drizzle ORM**: Database interactions with type safety
- **Better Auth**: Authentication system
- **MDX**: For notes/blog content with enhanced features
- **Neon PostgreSQL**: Serverless database
### Database Schema
The database schema is defined in `lib/db/schema.ts` and includes tables for:
- User accounts and sessions (auth system)
- Pages (for hit counter)
- Comments system
### Content Structure
- `/app`: Next.js App Router pages and layouts
- `/components`: React components organized by feature
- `/lib`: Utility functions and configurations
- `/notes`: MDX content for blog posts
- `/public`: Static assets
### Important Features
1. **Authentication**: GitHub OAuth integration via Better Auth
2. **MDX Processing**: Custom rehype/remark plugins for enhanced content
3. **Comments System**: GitHub-authenticated commenting system
4. **Hit Counter**: Simple analytics for page views
5. **Contact Form**: With Cloudflare Turnstile protection
## Development Considerations
1. The project assumes deployment to Vercel and makes use of Vercel-specific features
2. When working with MDX content, note the custom plugins and transformations in `next.config.ts`
3. Database operations use Drizzle ORM with Neon's serverless PostgreSQL client
4. The repository uses strict linting and type checking through ESLint and TypeScript
5. React components follow patterns from shadcn/ui style system
6. Always prefer React Server Components (RSC) over client components, and server actions ("use server") over API routes

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { unstable_cache as cache } from "next/cache";
import { getViews as _getViews } from "@/lib/server/views";
import { getViews as _getViews } from "@/lib/views";
const getViews = cache(_getViews, undefined, {
revalidate: 300, // 5 minutes

View File

@ -1,4 +1,7 @@
import { Suspense } from "react";
import PageTitle from "@/components/layout/page-title";
import Comments from "@/components/comments/comments";
import CommentsSkeleton from "@/components/comments/comments-skeleton";
import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({
@ -7,6 +10,8 @@ export const metadata = createMetadata({
canonical: "/cli",
});
export const experimental_ppr = true;
<PageTitle canonical="/cli">CLI</PageTitle>
> The [Jake Jarvis](https://jarv.is/) CLI (aka the most useless Node module ever published, in history, by anyone, ever).
@ -34,3 +39,7 @@ npx @jakejarvis/cli
## License
MIT &copy; [Jake Jarvis](https://jarv.is/), [Sindre Sorhus](https://sindresorhus.com/)
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug="uses" />
</Suspense>

View File

@ -3,9 +3,8 @@ import path from "path";
import fs from "fs";
import { ImageResponse } from "next/og";
import { notFound } from "next/navigation";
import { getSlugs, getFrontMatter } from "@/lib/posts";
import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts";
import siteConfig from "@/lib/config/site";
import { POSTS_DIR } from "@/lib/config/constants";
export const contentType = "image/png";
export const size = {

View File

@ -7,11 +7,10 @@ import Link from "@/components/link";
import ViewCounter from "@/components/view-counter";
import Comments from "@/components/comments/comments";
import CommentsSkeleton from "@/components/comments/comments-skeleton";
import { getSlugs, getFrontMatter } from "@/lib/posts";
import { getSlugs, getFrontMatter, POSTS_DIR } from "@/lib/posts";
import { createMetadata } from "@/lib/metadata";
import siteConfig from "@/lib/config/site";
import authorConfig from "@/lib/config/author";
import { POSTS_DIR } from "@/lib/config/constants";
import { size as ogImageSize } from "./opengraph-image";
import type { Metadata } from "next";
import type { BlogPosting } from "schema-dts";
@ -143,13 +142,9 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<MDXContent />
<section id="comments" className="isolate mt-8 mb-10 min-h-36 w-full border-t-2 pt-8">
<div className="mx-auto w-full max-w-3xl">
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug={`${POSTS_DIR}/${frontmatter!.slug}`} closed={frontmatter!.noComments} />
</Suspense>
</div>
</section>
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug={`${POSTS_DIR}/${frontmatter!.slug}`} closed={frontmatter!.noComments} />
</Suspense>
</>
);
};

View File

@ -1,16 +1,15 @@
import { env } from "@/lib/env";
import { EyeIcon } from "lucide-react";
import Link from "@/components/link";
import { getFrontMatter } from "@/lib/posts";
import { getFrontMatter, POSTS_DIR } from "@/lib/posts";
import { createMetadata } from "@/lib/metadata";
import { formatDate, formatDateISO } from "@/lib/date";
import authorConfig from "@/lib/config/author";
import { POSTS_DIR } from "@/lib/config/constants";
import { getViews } from "@/lib/server/views";
import { getViews } from "@/lib/views";
import type { ReactElement } from "react";
import type { FrontMatter } from "@/lib/posts";
export const revalidate = 600; // 10 minutes
export const revalidate = 300; // 5 minutes
export const metadata = createMetadata({
title: "Notes",

View File

@ -21,7 +21,7 @@ export const getContributions = async (): Promise<
},
cache: "force-cache",
next: {
revalidate: 3600, // 1 hour
revalidate: 900, // 15 minutes
tags: ["github-contributions"],
},
});
@ -126,7 +126,7 @@ export const getRepos = async (): Promise<Repository[] | undefined> => {
...options,
cache: "force-cache",
next: {
revalidate: 3600, // 1 hour
revalidate: 900, // 15 minutes
tags: ["github-repos"],
},
});

View File

@ -9,7 +9,7 @@ import ActivityCalendar from "@/components/activity-calendar";
import { GitHubIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import { createMetadata } from "@/lib/metadata";
import { getContributions, getRepos } from "@/lib/server/github";
import { getContributions, getRepos } from "./github";
export const metadata = createMetadata({
title: "Projects",

View File

@ -1,4 +1,7 @@
import { Suspense } from "react";
import PageTitle from "@/components/layout/page-title";
import Comments from "@/components/comments/comments";
import CommentsSkeleton from "@/components/comments/comments-skeleton";
import { createMetadata } from "@/lib/metadata";
export const metadata = createMetadata({
@ -7,6 +10,8 @@ export const metadata = createMetadata({
canonical: "/uses",
});
export const experimental_ppr = true;
<PageTitle canonical="/uses">Uses</PageTitle>
~~I regularly get messages asking about which tools I use to work.~~
@ -164,3 +169,7 @@ Other geeky stuff:
- 2x [**ecobee3 lite**](https://www.ecobee.com/en-us/smart-thermostats/smart-wifi-thermostat/)
- 2x [**Sonos One**](https://www.sonos.com/en-us/shop/one.html) (with Alexa turned off...hopefully? 🤫)
- 2x [**Apple TV 4K** (2021)](https://www.apple.com/apple-tv-4k/)
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug="uses" />
</Suspense>

View File

@ -1,4 +1,7 @@
import { Suspense } from "react";
import PageTitle from "@/components/layout/page-title";
import Comments from "@/components/comments/comments";
import CommentsSkeleton from "@/components/comments/comments-skeleton";
import { createMetadata } from "@/lib/metadata";
import backgroundImg from "./sundar.jpg";
@ -9,6 +12,8 @@ export const metadata = createMetadata({
canonical: "/zip",
});
export const experimental_ppr = true;
export const Terminal = () => (
<div
className="relative mx-auto my-6 w-full rounded-lg"
@ -71,3 +76,7 @@ A little-known monopolistic internet conglomorate simply unleashed [multiple](ht
- **Kaspersky:** [Beware the .zip and .mov domains!](https://usa.kaspersky.com/blog/zip-mov-domain-extension-confusion/28351/)
- **Palo Alto Networks:** [New Top Level Domains .zip and .mov open the door for new attacks](https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA14u000000g1wOCAQ)
- **Google, twenty years ago:** ["Don't be evil"](https://web.archive.org/web/20050204181615/http://investor.google.com/conduct.html)
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug="zip" />
</Suspense>

View File

@ -1,8 +1,9 @@
import Skeleton from "@/components/ui/skeleton";
import Wrapper from "./comments-wrapper";
const CommentsSkeleton = () => {
return (
<div className="mx-auto max-w-3xl space-y-6">
<Wrapper>
<Skeleton className="h-32 w-full" />
<div className="flex gap-4">
@ -19,7 +20,7 @@ const CommentsSkeleton = () => {
</div>
</div>
</div>
</div>
</Wrapper>
);
};

View File

@ -0,0 +1,11 @@
import { cn } from "@/lib/utils";
const CommentsWrapper = ({ className, children }: { className?: string; children: React.ReactNode }) => {
return (
<section id="comments" className={cn("isolate mt-8 mb-10 min-h-36 w-full border-t-2 pt-8", className)}>
<div className="mx-auto w-full max-w-3xl space-y-6 text-base leading-normal [&_p]:my-auto">{children}</div>
</section>
);
};
export default CommentsWrapper;

View File

@ -1,4 +1,5 @@
import { headers } from "next/headers";
import Wrapper from "./comments-wrapper";
import Form from "./comment-form";
import Thread from "./comment-thread";
import SignIn from "./sign-in";
@ -27,14 +28,14 @@ const Comments = async ({ slug, closed = false }: { slug: string; closed?: boole
const rootComments = commentsByParentId["root"] || [];
return (
<div className="space-y-6">
<Wrapper>
{closed ? (
<div className="bg-muted/40 flex min-h-32 items-center justify-center rounded-lg p-6">
<p className="text-center font-medium">Comments are closed for this post.</p>
</div>
) : !session ? (
<div className="bg-muted/40 flex flex-col items-center justify-center rounded-lg p-6">
<p className="mb-4 text-center font-medium">Join the discussion by signing in:</p>
<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>
<SignIn callbackPath={`/${slug}#comments`} />
</div>
) : (
@ -57,7 +58,7 @@ const Comments = async ({ slug, closed = false }: { slug: string; closed?: boole
))}
</div>
)}
</div>
</Wrapper>
);
};

View File

@ -1,7 +1,7 @@
import { env } from "@/lib/env";
import { connection } from "next/server";
import CountUp from "@/components/count-up";
import { incrementViews } from "@/lib/server/views";
import { incrementViews } from "@/lib/views";
const ViewCounter = async ({ slug }: { slug: string }) => {
// ensure this component isn't triggered by prerenders and/or preloads

View File

@ -1,2 +0,0 @@
/** Path to directory with .mdx files, relative to project root. */
export const POSTS_DIR = "notes" as const;

View File

@ -14,7 +14,6 @@ import {
remarkStripMdxImportsExports,
} from "@/lib/remark";
import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
import { POSTS_DIR } from "@/lib/config/constants";
export type FrontMatter = {
slug: string;
@ -28,6 +27,9 @@ export type FrontMatter = {
noComments?: boolean;
};
/** Path to directory with .mdx files, relative to project root. */
export const POSTS_DIR = "notes" as const;
/** Use filesystem to get a simple list of all post slugs */
export const getSlugs = cache(async (): Promise<string[]> => {
// list all .mdx files in POSTS_DIR

View File

@ -1,6 +1,5 @@
import "server-only";
import { cache } from "react";
import { eq, inArray } from "drizzle-orm";
import { db } from "@/lib/db";
import { page } from "@/lib/db/schema";
@ -43,43 +42,41 @@ export const getViews: {
* Retrieves the numbers of views for ALL slugs
*/
(): Promise<Record<string, number>>;
} = cache(
async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slug?: any
): // eslint-disable-next-line @typescript-eslint/no-explicit-any
Promise<any> => {
try {
// return one page
if (typeof slug === "string") {
const pages = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
return pages[0].views;
}
} = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slug?: any
): // eslint-disable-next-line @typescript-eslint/no-explicit-any
Promise<any> => {
try {
// return one page
if (typeof slug === "string") {
const pages = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
return pages[0].views;
}
// return multiple pages
if (Array.isArray(slug)) {
const pages = await db.select().from(page).where(inArray(page.slug, slug));
return pages.reduce(
(acc, page, index) => {
acc[slug[index]] = page.views;
return acc;
},
{} as Record<string, number | null>
);
}
// return ALL pages
const pages = await db.select().from(page);
// return multiple pages
if (Array.isArray(slug)) {
const pages = await db.select().from(page).where(inArray(page.slug, slug));
return pages.reduce(
(acc, page) => {
acc[page.slug] = page.views;
(acc, page, index) => {
acc[slug[index]] = page.views;
return acc;
},
{} as Record<string, number>
{} as Record<string, number | null>
);
} catch (error) {
console.error("[server/views] fatal error:", error);
throw new Error("Failed to get views");
}
// return ALL pages
const pages = await db.select().from(page);
return pages.reduce(
(acc, page) => {
acc[page.slug] = page.views;
return acc;
},
{} as Record<string, number>
);
} catch (error) {
console.error("[server/views] fatal error:", error);
throw new Error("Failed to get views");
}
);
};

View File

@ -56,7 +56,7 @@
"lucide-react": "0.511.0",
"next": "15.4.0-canary.38",
"react": "19.1.0",
"react-activity-calendar": "^2.7.11",
"react-activity-calendar": "^2.7.12",
"react-countup": "^6.5.3",
"react-dom": "19.1.0",
"react-lite-youtube-embed": "^2.5.1",
@ -89,7 +89,7 @@
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"unified": "^11.0.5",
"zod": "3.25.0-beta.20250516T005923"
"zod": "3.24.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",

31
pnpm-lock.yaml generated
View File

@ -73,7 +73,7 @@ importers:
version: 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@t3-oss/env-nextjs':
specifier: ^0.13.4
version: 0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.25.0-beta.20250516T005923)
version: 0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.24.4)
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.4.0-canary.38(@babel/core@7.26.10)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
@ -123,8 +123,8 @@ importers:
specifier: 19.1.0
version: 19.1.0
react-activity-calendar:
specifier: ^2.7.11
version: 2.7.11(react@19.1.0)
specifier: ^2.7.12
version: 2.7.12(react@19.1.0)
react-countup:
specifier: ^6.5.3
version: 6.5.3(react@19.1.0)
@ -222,8 +222,8 @@ importers:
specifier: ^11.0.5
version: 11.0.5
zod:
specifier: 3.25.0-beta.20250516T005923
version: 3.25.0-beta.20250516T005923
specifier: 3.24.4
version: 3.24.4
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.1
@ -4069,8 +4069,8 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-activity-calendar@2.7.11:
resolution: {integrity: sha512-JHJyvdYciBVPt1ZkE2pqnmg0ZMsmer09CXD1qWe2hpl3PyMosVqFOMZmDqjLxRWI6VD2ILH4RFk5dZCuWwnaPg==}
react-activity-calendar@2.7.12:
resolution: {integrity: sha512-OzVconQ5LA/uF2ZN3zDeWZb4UxjOmGr1ymaGCSEAMLSzwjzP7ojdyZs8DyV7jcV+rZ+lmwp6BTpBJqdW8ehXyw==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
@ -4922,9 +4922,6 @@ packages:
zod@3.24.4:
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
zod@3.25.0-beta.20250516T005923:
resolution: {integrity: sha512-pWO2WHnFfmKl9X/AY55DGqiyArXkOSNDyLAotrpQzfsnLOCgzLDk1tZRfFyIqd68UaBk32gIsksYpCHzMXqZqg==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -6206,21 +6203,21 @@ snapshots:
dependencies:
tslib: 2.8.1
'@t3-oss/env-core@0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.25.0-beta.20250516T005923)':
'@t3-oss/env-core@0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.24.4)':
dependencies:
arktype: 2.1.20
optionalDependencies:
typescript: 5.8.3
valibot: 1.1.0(typescript@5.8.3)
zod: 3.25.0-beta.20250516T005923
zod: 3.24.4
'@t3-oss/env-nextjs@0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.25.0-beta.20250516T005923)':
'@t3-oss/env-nextjs@0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.24.4)':
dependencies:
'@t3-oss/env-core': 0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.25.0-beta.20250516T005923)
'@t3-oss/env-core': 0.13.4(arktype@2.1.20)(typescript@5.8.3)(valibot@1.1.0(typescript@5.8.3))(zod@3.24.4)
optionalDependencies:
typescript: 5.8.3
valibot: 1.1.0(typescript@5.8.3)
zod: 3.25.0-beta.20250516T005923
zod: 3.24.4
transitivePeerDependencies:
- arktype
@ -9008,7 +9005,7 @@ snapshots:
queue-microtask@1.2.3: {}
react-activity-calendar@2.7.11(react@19.1.0):
react-activity-calendar@2.7.12(react@19.1.0):
dependencies:
date-fns: 4.1.0
react: 19.1.0
@ -10170,6 +10167,4 @@ snapshots:
zod@3.24.4: {}
zod@3.25.0-beta.20250516T005923: {}
zwitch@2.0.4: {}