1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:25:22 -04:00

move database from postgres/prisma to redis

This commit is contained in:
Jake Jarvis 2025-03-27 09:21:22 -04:00
parent e865d9d8e5
commit bbf6e9dc66
16 changed files with 283 additions and 651 deletions

View File

@ -8,7 +8,7 @@ CORE EXPERTISE:
- Server Actions
- Parallel and Intercepting Routes
- CSS Modules
- Prisma as a Database ORM
- Redis for KV storage
- MDX for blog content
- Zod for runtime type validation
@ -23,8 +23,7 @@ CODE ARCHITECTURE:
│ ├── config/ # Configuration constants
│ ├── helpers/ # Utility functions
├── notes/ # Blog posts in markdown/MDX format
├── static/ # Static files such as images and videos
└── prisma/ # Database schema in Prisma format
└── static/ # Static files such as images and videos
2. Component Organization:
- Keep reusable components in ./components/.

View File

@ -22,7 +22,6 @@
"extensions": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"prisma.prisma",
"unifiedjs.vscode-mdx",
"esbenp.prettier-vscode",
"stylelint.vscode-stylelint"

View File

@ -1,8 +1,9 @@
# required. storage for hit counter's server component at app/notes/[slug]/counter.tsx and API endpoint at /api/hits.
# 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://vercel.com/marketplace/neon
DATABASE_URL=
# required. redis storage credentials for hit counter's server component (app/notes/[slug]/counter.tsx) and API
# endpoint. currently set automatically by Vercel's Upstash integration.
# https://upstash.com/docs/redis/sdks/ts/getstarted
# https://vercel.com/marketplace/upstash
KV_REST_API_URL=
KV_REST_API_TOKEN=
# 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.

View File

@ -22,10 +22,6 @@ updates:
- "@types/react-is"
- "babel-plugin-react-compiler"
- "eslint-plugin-react-compiler"
prisma:
patterns:
- "prisma"
- "@prisma/*"
mdx:
patterns:
- "remark-*"

View File

@ -5,7 +5,7 @@
[![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE)
[![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is)
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Neon Postgres](https://neon.tech/), [Prisma](https://www.prisma.io/postgres), [Umami](https://umami.is/), [and more](https://jarv.is/humans.txt).
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Upstash Redis](https://upstash.com/), [Giscus](https://giscus.app/), [Umami](https://umami.is/), [and more](https://jarv.is/humans.txt).
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!

View File

@ -1,31 +1,43 @@
import { NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma";
import type { hits as Hits } from "@prisma/client";
import redis from "../../../lib/helpers/redis";
export const revalidate = 900; // 15 mins
export const GET = async (): Promise<
NextResponse<{
total: Pick<Hits, "hits">;
pages: Hits[];
total: {
hits: number;
};
pages: Array<{
slug: string;
hits: number;
}>;
}>
> => {
// fetch all rows from db sorted by most hits
const pages = await prisma.hits.findMany({
orderBy: [
{
hits: "desc",
},
],
const slugs = await redis.scan(0, {
count: 50,
});
const total = { hits: 0 };
// fetch all rows from db sorted by most hits
const data = await Promise.all(
slugs[1].map(async (slug) => {
const hits = (await redis.get(slug)) as number;
return {
slug,
hits,
};
})
);
// sort by hits
data.sort((a, b) => b.hits - a.hits);
// calculate total hits
pages.forEach((page) => {
const total = { hits: 0 };
data.forEach((page) => {
// add these hits to running tally
total.hits += page.hits;
});
return NextResponse.json({ total, pages });
return NextResponse.json({ total, pages: data });
};

View File

@ -1,20 +1,12 @@
import { connection } from "next/server";
import commaNumber from "comma-number";
import { prisma } from "../../../lib/helpers/prisma";
import redis from "../../../lib/helpers/redis";
const HitCounter = async ({ slug }: { slug: string }) => {
await connection();
try {
const { hits } = await prisma.hits.upsert({
where: { slug },
create: { slug },
update: {
hits: {
increment: 1,
},
},
});
const hits = await redis.incr(slug);
// we have data!
return <span title={`${commaNumber(hits)} ${hits === 1 ? "view" : "views"}`}>{commaNumber(hits)}</span>;

View File

@ -22,9 +22,9 @@ For a likely excessive level of privacy and security, this website is also mirro
## Analytics
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Neon Postgres](https://neon.tech/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**.
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Upstash Redis](https://upstash.com/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**.
The [database schema](https://github.com/jakejarvis/jarv.is/blob/main/prisma/schema.prisma), [serverless function](https://github.com/jakejarvis/jarv.is/blob/main/app/api/hits/route.ts) and [client script](https://github.com/jakejarvis/jarv.is/blob/main/app/notes/%5Bslug%5D/counter.tsx) are open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public.
The [server component](https://github.com/jakejarvis/jarv.is/blob/main/app/notes/%5Bslug%5D/counter.tsx) is open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public.
A self-hosted [**Umami**](https://umami.is/) instance is also used to gain insights into referrers, search terms, etc. [without collecting anything identifiable](https://umami.is/blog/why-privacy-matters) about you. [The dashboard is even public!](/stats)

View File

@ -1,17 +0,0 @@
import { PrismaClient } from "@prisma/client";
// creating PrismaClient here prevents next.js from starting too many concurrent prisma instances and exhausting the
// number of connection pools available (especially when hot reloading from `next dev`).
// https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
const prismaClientSingleton = () => {
return new PrismaClient();
};
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;

6
lib/helpers/redis.ts Normal file
View File

@ -0,0 +1,6 @@
import { Redis } from "@upstash/redis";
// Initialize Redis
const redis = Redis.fromEnv();
export default redis;

View File

@ -14,8 +14,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc",
"postinstall": "npx prisma generate"
"typecheck": "tsc"
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
@ -29,7 +28,7 @@
"@next/third-parties": "15.3.0-canary.24",
"@octokit/graphql": "^8.2.1",
"@octokit/graphql-schema": "^15.26.0",
"@prisma/client": "^6.5.0",
"@upstash/redis": "^1.34.6",
"clsx": "^2.1.1",
"comma-number": "^2.1.0",
"copy-to-clipboard": "^3.3.3",
@ -76,7 +75,7 @@
"@jakejarvis/eslint-config": "^4.0.7",
"@types/comma-number": "^2.1.2",
"@types/mdx": "^2.0.13",
"@types/node": "^22.13.13",
"@types/node": "^22.13.14",
"@types/prop-types": "^15.7.14",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
@ -96,7 +95,6 @@
"eslint-plugin-react-hooks": "^5.2.0",
"lint-staged": "^15.5.0",
"prettier": "^3.5.3",
"prisma": "^6.5.0",
"schema-dts": "^1.1.5",
"simple-git-hooks": "^2.12.1",
"stylelint": "^16.17.0",
@ -130,10 +128,6 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@prisma/client",
"@prisma/engines",
"esbuild",
"prisma",
"sharp",
"simple-git-hooks"
],

791
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
-- CreateTable
CREATE TABLE "hits" (
"slug" TEXT NOT NULL,
"hits" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "hits_pkey" PRIMARY KEY ("slug")
);

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -1,14 +0,0 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model hits {
slug String @id
hits Int @default(1)
}

View File

@ -30,11 +30,10 @@
- Next.js
- Vercel
- Neon Postgres
- Prisma
- Umami
- Upstash Redis
- Giscus
- Resend
- Umami
- ...and more: https://jarv.is/uses
# VIEW SOURCE