mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-05-15 18:34:27 -04:00
a bit more cleanup
This commit is contained in:
+16
-9
@@ -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=
|
||||||
|
|||||||
@@ -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" }),
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 => {
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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".`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user