mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 03:45:22 -04:00
move database from postgres/prisma to redis ⚡️
This commit is contained in:
parent
e865d9d8e5
commit
bbf6e9dc66
@ -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/.
|
||||
|
@ -22,7 +22,6 @@
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"prisma.prisma",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint"
|
||||
|
11
.env.example
11
.env.example
@ -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.
|
||||
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -22,10 +22,6 @@ updates:
|
||||
- "@types/react-is"
|
||||
- "babel-plugin-react-compiler"
|
||||
- "eslint-plugin-react-compiler"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
- "@prisma/*"
|
||||
mdx:
|
||||
patterns:
|
||||
- "remark-*"
|
||||
|
@ -5,7 +5,7 @@
|
||||
[](LICENSE)
|
||||
[](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!
|
||||
|
||||
|
@ -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 });
|
||||
};
|
||||
|
@ -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>;
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
6
lib/helpers/redis.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
// Initialize Redis
|
||||
const redis = Redis.fromEnv();
|
||||
|
||||
export default redis;
|
12
package.json
12
package.json
@ -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
791
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "hits" (
|
||||
"slug" TEXT NOT NULL,
|
||||
"hits" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "hits_pkey" PRIMARY KEY ("slug")
|
||||
);
|
@ -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"
|
@ -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)
|
||||
}
|
@ -30,11 +30,10 @@
|
||||
|
||||
- Next.js
|
||||
- Vercel
|
||||
- Neon Postgres
|
||||
- Prisma
|
||||
- Umami
|
||||
- Upstash Redis
|
||||
- Giscus
|
||||
- Resend
|
||||
- Umami
|
||||
- ...and more: https://jarv.is/uses
|
||||
|
||||
# VIEW SOURCE
|
||||
|
Loading…
x
Reference in New Issue
Block a user