1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-05-15 20:34:25 -04:00

a bit more cleanup

This commit is contained in:
2025-03-07 14:56:49 -05:00
parent 354dade9aa
commit 9229f92c0c
23 changed files with 50 additions and 58 deletions
+16 -9
View File
@@ -1,25 +1,32 @@
# required. storage for hit counter endpoints at /api/count and /api/hits. # required. storage for hit counter's server component at app/notes/[slug]/counter.tsx and API endpoint at /api/hits.
# currently uses Postgres, but this can be changed in prisma/schema.prisma. # currently set automatically by Vercel's Neon integration, but this can be changed in prisma/schema.prisma.
# https://www.prisma.io/docs/postgres/overview # https://www.prisma.io/docs/postgres/overview
# https://vercel.com/marketplace/neon
DATABASE_URL= DATABASE_URL=
# requred. used for /projects grid, built with SSG. only needs the "public_repo" scope since we don't need/want to # required. used for /projects grid, built with ISR. only needs the "public_repo" scope since we don't need/want to
# showcase any private repositories, obviously. # showcase any private repositories, obviously.
# https://github.com/settings/tokens/new?scopes=public_repo # https://github.com/settings/tokens/new?scopes=public_repo
GH_PUBLIC_TOKEN= GITHUB_TOKEN=
# required for production. sends contact form submissions via /api/contact. # optional. enables comments on blog posts via GitHub discussions.
# https://giscus.app/
NEXT_PUBLIC_GISCUS_REPO_ID=
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
# required for production. sends contact form submissions via a server action (see app/contact/actions.ts).
# https://resend.com/api-keys # https://resend.com/api-keys
# currently set automatically by Vercel's Resend integration.
# https://vercel.com/integrations/resend
RESEND_API_KEY= RESEND_API_KEY=
# optional. send submissions from noreply@{RESEND_DOMAIN}; defaults to onboarding@resend.dev. sender's real email is # optional. send submissions from noreply@{RESEND_DOMAIN}; defaults to onboarding@resend.dev. sender's real email is
# passed via a Reply-To header. setting this makes zero difference to the user. # passed via a Reply-To header. setting this makes zero difference to the user.
# https://resend.com/docs/send-with-nodemailer-smtp # https://resend.com/docs/send-with-nodemailer-smtp
RESEND_DOMAIN= RESEND_DOMAIN=
# required for production. falls back to testing keys if not set or in dev environment: # required for production. site key must be prefixed with NEXT_PUBLIC_ since it is used to embed the captcha widget.
# falls back to testing keys if not set or in dev environment:
# https://developers.cloudflare.com/turnstile/troubleshooting/testing/ # https://developers.cloudflare.com/turnstile/troubleshooting/testing/
# site key must be prefixed with NEXT_PUBLIC_ since it is used to embed the actual captcha widget.
# https://developers.cloudflare.com/turnstile/
NEXT_PUBLIC_TURNSTILE_SITE_KEY= NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# used only for backend validation of contact form submissions on /api/contact. # used for backend validation of turnstile result.
TURNSTILE_SECRET_KEY= TURNSTILE_SECRET_KEY=
+1 -1
View File
@@ -3,7 +3,7 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
import { z } from "zod"; import { z } from "zod";
import { Resend } from "resend"; import { Resend } from "resend";
import config from "../../lib/config"; import config from "../../lib/config/constants";
const schema = z.object({ const schema = z.object({
name: z.string().min(1, { message: "Name is required" }), name: z.string().min(1, { message: "Name is required" }),
+1 -1
View File
@@ -3,7 +3,7 @@ import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static"; export const dynamic = "force-static";
export const GET = async () => { export const GET = async () => {
return new Response(await buildFeed({ type: "atom" }), { return new Response((await buildFeed()).atom1(), {
headers: { headers: {
"content-type": "application/atom+xml; charset=utf-8", "content-type": "application/atom+xml; charset=utf-8",
}, },
+1 -1
View File
@@ -3,7 +3,7 @@ import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static"; export const dynamic = "force-static";
export const GET = async () => { export const GET = async () => {
return new Response(await buildFeed({ type: "rss" }), { return new Response((await buildFeed()).rss2(), {
headers: { headers: {
"content-type": "application/rss+xml; charset=utf-8", "content-type": "application/rss+xml; charset=utf-8",
}, },
+1 -1
View File
@@ -4,7 +4,7 @@ import { ThemeProvider } from "../contexts/ThemeContext";
import Header from "../components/Header"; import Header from "../components/Header";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import { SkipToContentLink, SkipToContentTarget } from "../components/SkipToContent"; import { SkipToContentLink, SkipToContentTarget } from "../components/SkipToContent";
import config from "../lib/config"; import config from "../lib/config/constants";
import type { Metadata } from "next"; import type { Metadata } from "next";
import type { Person, WithContext } from "schema-dts"; import type { Person, WithContext } from "schema-dts";
+1 -1
View File
@@ -1,4 +1,4 @@
import config from "../lib/config"; import config from "../lib/config/constants";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
const manifest = (): MetadataRoute.Manifest => { const manifest = (): MetadataRoute.Manifest => {
+1 -1
View File
@@ -8,7 +8,7 @@ import Loading from "../../../components/Loading";
import HitCounter from "./counter"; import HitCounter from "./counter";
import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts"; import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import { metadata as defaultMetadata } from "../../layout"; import { metadata as defaultMetadata } from "../../layout";
import config from "../../../lib/config"; import config from "../../../lib/config/constants";
import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi"; import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi";
import type { Metadata, Route } from "next"; import type { Metadata, Route } from "next";
import type { Article, WithContext } from "schema-dts"; import type { Article, WithContext } from "schema-dts";
+1 -1
View File
@@ -2,7 +2,7 @@ import Content from "../../components/Content";
import Link from "../../components/Link"; import Link from "../../components/Link";
import Time from "../../components/Time"; import Time from "../../components/Time";
import { getAllPosts } from "../../lib/helpers/posts"; import { getAllPosts } from "../../lib/helpers/posts";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import { metadata as defaultMetadata } from "../layout"; import { metadata as defaultMetadata } from "../layout";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import type { Metadata, Route } from "next"; import type { Metadata, Route } from "next";
+4 -4
View File
@@ -4,7 +4,7 @@ import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link"; import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime"; import RelativeTime from "../../components/RelativeTime";
import commaNumber from "comma-number"; import commaNumber from "comma-number";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import { metadata as defaultMetadata } from "../layout"; import { metadata as defaultMetadata } from "../layout";
import { GoStar, GoRepoForked } from "react-icons/go"; import { GoStar, GoRepoForked } from "react-icons/go";
import { SiGithub } from "react-icons/si"; import { SiGithub } from "react-icons/si";
@@ -43,8 +43,8 @@ type Project = {
async function getRepos(): Promise<Project[] | null> { async function getRepos(): Promise<Project[] | null> {
// don't fail the entire site build if the required API key for this page is missing // don't fail the entire site build if the required API key for this page is missing
if (!process.env.GH_PUBLIC_TOKEN || process.env.GH_PUBLIC_TOKEN === "") { if (!process.env.GITHUB_TOKEN || process.env.GITHUB_TOKEN === "") {
console.warn(`ERROR: I can't fetch any GitHub projects without "GH_PUBLIC_TOKEN" set! Skipping for now...`); console.warn(`ERROR: I can't fetch any GitHub projects without "GITHUB_TOKEN" set! Skipping for now...`);
return null; return null;
} }
@@ -86,7 +86,7 @@ async function getRepos(): Promise<Project[] | null> {
limit: 12, limit: 12,
headers: { headers: {
accept: "application/vnd.github.v3+json", accept: "application/vnd.github.v3+json",
authorization: `token ${process.env.GH_PUBLIC_TOKEN}`, authorization: `token ${process.env.GITHUB_TOKEN}`,
}, },
} }
); );
+1 -1
View File
@@ -1,4 +1,4 @@
import config from "../lib/config"; import config from "../lib/config/constants";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
export const dynamic = "force-static"; export const dynamic = "force-static";
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"; import path from "path";
import glob from "fast-glob"; import glob from "fast-glob";
import { getAllPosts } from "../lib/helpers/posts"; import { getAllPosts } from "../lib/helpers/posts";
import config from "../lib/config"; import config from "../lib/config/constants";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
export const dynamic = "force-static"; export const dynamic = "force-static";
+6 -5
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Giscus from "@giscus/react"; import Giscus from "@giscus/react";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import type { GiscusProps } from "@giscus/react"; import type { GiscusProps } from "@giscus/react";
export type CommentsProps = { export type CommentsProps = {
@@ -10,9 +10,9 @@ export type CommentsProps = {
const Comments = ({ title }: CommentsProps) => { const Comments = ({ title }: CommentsProps) => {
// fail silently if giscus isn't configured // fail silently if giscus isn't configured
if (!config.giscusConfig) { if (!process.env.NEXT_PUBLIC_GISCUS_REPO_ID || !process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID) {
console.warn( console.warn(
"[giscus] not configured, ensure giscusConfig.repoId and giscusConfig.categoryId are set in lib/config/index.js" "[giscus] not configured, ensure 'NEXT_PUBLIC_GISCUS_REPO_ID' and 'NEXT_PUBLIC_GISCUS_CATEGORY_ID' environment variables are set."
); );
return null; return null;
@@ -22,14 +22,15 @@ const Comments = ({ title }: CommentsProps) => {
return ( return (
<Giscus <Giscus
repo={config.githubRepo as GiscusProps["repo"]} repo={config.githubRepo as GiscusProps["repo"]}
repoId={config.giscusConfig.repoId} repoId={process.env.NEXT_PUBLIC_GISCUS_REPO_ID}
term={title} term={title}
category="Comments" category="Comments"
categoryId={config.giscusConfig.categoryId} categoryId={process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID}
mapping="specific" mapping="specific"
reactionsEnabled="1" reactionsEnabled="1"
emitMetadata="0" emitMetadata="0"
inputPosition="top" inputPosition="top"
theme="preferred_color_scheme"
loading="lazy" loading="lazy"
/> />
); );
+1 -1
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import Link from "../Link"; import Link from "../Link";
import { GoHeartFill } from "react-icons/go"; import { GoHeartFill } from "react-icons/go";
import { SiNextdotjs } from "react-icons/si"; import { SiNextdotjs } from "react-icons/si";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Footer.module.css"; import styles from "./Footer.module.css";
+1 -1
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import Link from "../Link"; import Link from "../Link";
import Image from "../Image"; import Image from "../Image";
import Menu from "../Menu"; import Menu from "../Menu";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Header.module.css"; import styles from "./Header.module.css";
+1 -1
View File
@@ -1,7 +1,7 @@
import NextLink from "next/link"; import NextLink from "next/link";
import clsx from "clsx"; import clsx from "clsx";
import objStr from "obj-str"; import objStr from "obj-str";
import config from "../../lib/config"; import config from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css"; import styles from "./Link.module.css";
+2 -1
View File
@@ -35,10 +35,11 @@ const Video = ({ src, poster, autoplay = false, responsive = true, className, ..
> >
{src.map((file) => { {src.map((file) => {
const extension = file.split(".").pop(); const extension = file.split(".").pop();
if (extension === "vtt") { if (extension === "vtt") {
return <track key={file} kind="subtitles" src={file} srcLang="en" label="English" default />; return <track key={file} kind="subtitles" src={file} srcLang="en" label="English" default />;
} else { } else {
return <source key={file} src={file} type={`video/${file.split(".").pop()}`} />; return <source key={file} src={file} type={`video/${extension}`} />;
} }
})} })}
</video> </video>
@@ -1,6 +1,4 @@
// @ts-check const constants = {
const config = {
// Site info // Site info
siteName: "Jake Jarvis", siteName: "Jake Jarvis",
siteLocale: "en-US", siteLocale: "en-US",
@@ -20,11 +18,6 @@ const config = {
licenseUrl: "https://creativecommons.org/licenses/by/4.0/", licenseUrl: "https://creativecommons.org/licenses/by/4.0/",
copyrightYearStart: 2001, copyrightYearStart: 2001,
githubRepo: "jakejarvis/jarv.is", githubRepo: "jakejarvis/jarv.is",
giscusConfig: {
// https://github.com/giscus/giscus-component/tree/main/packages/react#readme
repoId: "MDEwOlJlcG9zaXRvcnk1MzM0MDgxMQ==",
categoryId: "DIC_kwDOAy3qi84CAsjS",
},
// Me info // Me info
authorName: "Jake Jarvis", authorName: "Jake Jarvis",
@@ -42,4 +35,4 @@ const config = {
}, },
}; };
export default config; export default constants;
+3 -13
View File
@@ -1,10 +1,10 @@
import { Feed } from "feed"; import { Feed } from "feed";
import { getAllPosts } from "./posts"; import { getAllPosts } from "./posts";
import config from "../config"; import config from "../config/constants";
import meJpg from "../../app/me.jpg"; import meJpg from "../../app/me.jpg";
export const buildFeed = async (options: { type: "rss" | "atom" | "json" }): Promise<string> => { export const buildFeed = async (): Promise<Feed> => {
// https://github.com/jpmonette/feed#example // https://github.com/jpmonette/feed#example
const feed = new Feed({ const feed = new Feed({
id: config.baseUrl, id: config.baseUrl,
@@ -43,15 +43,5 @@ export const buildFeed = async (options: { type: "rss" | "atom" | "json" }): Pro
}); });
}); });
if (options.type === "rss") { return feed;
return feed.rss2();
} else if (options.type === "atom") {
return feed.atom1();
} else if (options.type === "json") {
// rare but including as an option because why not...
// https://www.jsonfeed.org/
return feed.json1();
} else {
throw new TypeError(`Invalid feed type "${options.type}", must be "rss", "atom", or "json".`);
}
}; };
+1 -1
View File
@@ -5,7 +5,7 @@ import dayjsRelativeTime from "dayjs/plugin/relativeTime";
import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat"; import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat";
import dayjsAdvancedFormat from "dayjs/plugin/advancedFormat"; import dayjsAdvancedFormat from "dayjs/plugin/advancedFormat";
import "dayjs/locale/en"; import "dayjs/locale/en";
import config from "../config"; import config from "../config/constants";
const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => { const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => {
// plugins // plugins
+1 -1
View File
@@ -3,7 +3,7 @@ import glob from "fast-glob";
import pMap from "p-map"; import pMap from "p-map";
import pMemoize from "p-memoize"; import pMemoize from "p-memoize";
import { formatDate } from "./format-date"; import { formatDate } from "./format-date";
import config from "../config"; import config from "../config/constants";
// path to directory with .mdx files, relative to project root // path to directory with .mdx files, relative to project root
const POSTS_DIR = "notes"; const POSTS_DIR = "notes";
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import siteConfig from "./lib/config"; import siteConfig from "./lib/config/constants";
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const response = NextResponse.next(); const response = NextResponse.next();
+1 -1
View File
@@ -28,7 +28,7 @@ const nextConfig: NextConfig = {
}, },
eslint: { eslint: {
// https://nextjs.org/docs/basic-features/eslint#linting-custom-directories-and-files // https://nextjs.org/docs/basic-features/eslint#linting-custom-directories-and-files
dirs: ["app", "components", "contexts", "hooks", "lib"], dirs: ["app", "components", "contexts", "hooks", "lib", "notes"],
}, },
headers: async () => [ headers: async () => [
{ {
+1 -1
View File
@@ -101,7 +101,7 @@
"engines": { "engines": {
"node": ">=20.x" "node": ">=20.x"
}, },
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b", "packageManager": "pnpm@10.6.1+sha512.40ee09af407fa9fbb5fbfb8e1cb40fbb74c0af0c3e10e9224d7b53c7658528615b2c92450e74cfad91e3a2dcafe3ce4050d80bda71d757756d2ce2b66213e9a3",
"cacheDirectories": [ "cacheDirectories": [
"node_modules", "node_modules",
".next/cache" ".next/cache"