mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 15:28:28 -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
|
- Server Actions
|
||||||
- Parallel and Intercepting Routes
|
- Parallel and Intercepting Routes
|
||||||
- CSS Modules
|
- CSS Modules
|
||||||
- Prisma as a Database ORM
|
- Redis for KV storage
|
||||||
- MDX for blog content
|
- MDX for blog content
|
||||||
- Zod for runtime type validation
|
- Zod for runtime type validation
|
||||||
|
|
||||||
@ -23,8 +23,7 @@ CODE ARCHITECTURE:
|
|||||||
│ ├── config/ # Configuration constants
|
│ ├── config/ # Configuration constants
|
||||||
│ ├── helpers/ # Utility functions
|
│ ├── helpers/ # Utility functions
|
||||||
├── notes/ # Blog posts in markdown/MDX format
|
├── notes/ # Blog posts in markdown/MDX format
|
||||||
├── static/ # Static files such as images and videos
|
└── static/ # Static files such as images and videos
|
||||||
└── prisma/ # Database schema in Prisma format
|
|
||||||
|
|
||||||
2. Component Organization:
|
2. Component Organization:
|
||||||
- Keep reusable components in ./components/.
|
- Keep reusable components in ./components/.
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
"extensions": [
|
"extensions": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"prisma.prisma",
|
|
||||||
"unifiedjs.vscode-mdx",
|
"unifiedjs.vscode-mdx",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"stylelint.vscode-stylelint"
|
"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.
|
# required. redis storage credentials for hit counter's server component (app/notes/[slug]/counter.tsx) and API
|
||||||
# currently set automatically by Vercel's Neon integration, but this can be changed in prisma/schema.prisma.
|
# endpoint. currently set automatically by Vercel's Upstash integration.
|
||||||
# https://www.prisma.io/docs/postgres/overview
|
# https://upstash.com/docs/redis/sdks/ts/getstarted
|
||||||
# https://vercel.com/marketplace/neon
|
# https://vercel.com/marketplace/upstash
|
||||||
DATABASE_URL=
|
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
|
# 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.
|
||||||
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -22,10 +22,6 @@ updates:
|
|||||||
- "@types/react-is"
|
- "@types/react-is"
|
||||||
- "babel-plugin-react-compiler"
|
- "babel-plugin-react-compiler"
|
||||||
- "eslint-plugin-react-compiler"
|
- "eslint-plugin-react-compiler"
|
||||||
prisma:
|
|
||||||
patterns:
|
|
||||||
- "prisma"
|
|
||||||
- "@prisma/*"
|
|
||||||
mdx:
|
mdx:
|
||||||
patterns:
|
patterns:
|
||||||
- "remark-*"
|
- "remark-*"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/jakejarvis/jarv.is)
|
[](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!
|
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 { NextResponse } from "next/server";
|
||||||
import { prisma } from "../../../lib/helpers/prisma";
|
import redis from "../../../lib/helpers/redis";
|
||||||
import type { hits as Hits } from "@prisma/client";
|
|
||||||
|
|
||||||
export const revalidate = 900; // 15 mins
|
export const revalidate = 900; // 15 mins
|
||||||
|
|
||||||
export const GET = async (): Promise<
|
export const GET = async (): Promise<
|
||||||
NextResponse<{
|
NextResponse<{
|
||||||
total: Pick<Hits, "hits">;
|
total: {
|
||||||
pages: Hits[];
|
hits: number;
|
||||||
|
};
|
||||||
|
pages: Array<{
|
||||||
|
slug: string;
|
||||||
|
hits: number;
|
||||||
|
}>;
|
||||||
}>
|
}>
|
||||||
> => {
|
> => {
|
||||||
// fetch all rows from db sorted by most hits
|
const slugs = await redis.scan(0, {
|
||||||
const pages = await prisma.hits.findMany({
|
count: 50,
|
||||||
orderBy: [
|
|
||||||
{
|
|
||||||
hits: "desc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
// calculate total hits
|
||||||
pages.forEach((page) => {
|
const total = { hits: 0 };
|
||||||
|
data.forEach((page) => {
|
||||||
// add these hits to running tally
|
// add these hits to running tally
|
||||||
total.hits += page.hits;
|
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 { connection } from "next/server";
|
||||||
import commaNumber from "comma-number";
|
import commaNumber from "comma-number";
|
||||||
import { prisma } from "../../../lib/helpers/prisma";
|
import redis from "../../../lib/helpers/redis";
|
||||||
|
|
||||||
const HitCounter = async ({ slug }: { slug: string }) => {
|
const HitCounter = async ({ slug }: { slug: string }) => {
|
||||||
await connection();
|
await connection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { hits } = await prisma.hits.upsert({
|
const hits = await redis.incr(slug);
|
||||||
where: { slug },
|
|
||||||
create: { slug },
|
|
||||||
update: {
|
|
||||||
hits: {
|
|
||||||
increment: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// we have data!
|
// we have data!
|
||||||
return <span title={`${commaNumber(hits)} ${hits === 1 ? "view" : "views"}`}>{commaNumber(hits)}</span>;
|
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
|
## 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)
|
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",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc"
|
||||||
"postinstall": "npx prisma generate"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@date-fns/tz": "^1.2.0",
|
"@date-fns/tz": "^1.2.0",
|
||||||
@ -29,7 +28,7 @@
|
|||||||
"@next/third-parties": "15.3.0-canary.24",
|
"@next/third-parties": "15.3.0-canary.24",
|
||||||
"@octokit/graphql": "^8.2.1",
|
"@octokit/graphql": "^8.2.1",
|
||||||
"@octokit/graphql-schema": "^15.26.0",
|
"@octokit/graphql-schema": "^15.26.0",
|
||||||
"@prisma/client": "^6.5.0",
|
"@upstash/redis": "^1.34.6",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"comma-number": "^2.1.0",
|
"comma-number": "^2.1.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
@ -76,7 +75,7 @@
|
|||||||
"@jakejarvis/eslint-config": "^4.0.7",
|
"@jakejarvis/eslint-config": "^4.0.7",
|
||||||
"@types/comma-number": "^2.1.2",
|
"@types/comma-number": "^2.1.2",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "^22.13.13",
|
"@types/node": "^22.13.14",
|
||||||
"@types/prop-types": "^15.7.14",
|
"@types/prop-types": "^15.7.14",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
@ -96,7 +95,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.5.0",
|
|
||||||
"schema-dts": "^1.1.5",
|
"schema-dts": "^1.1.5",
|
||||||
"simple-git-hooks": "^2.12.1",
|
"simple-git-hooks": "^2.12.1",
|
||||||
"stylelint": "^16.17.0",
|
"stylelint": "^16.17.0",
|
||||||
@ -130,10 +128,6 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@prisma/client",
|
|
||||||
"@prisma/engines",
|
|
||||||
"esbuild",
|
|
||||||
"prisma",
|
|
||||||
"sharp",
|
"sharp",
|
||||||
"simple-git-hooks"
|
"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
|
- Next.js
|
||||||
- Vercel
|
- Vercel
|
||||||
- Neon Postgres
|
- Upstash Redis
|
||||||
- Prisma
|
|
||||||
- Umami
|
|
||||||
- Giscus
|
- Giscus
|
||||||
- Resend
|
- Resend
|
||||||
|
- Umami
|
||||||
- ...and more: https://jarv.is/uses
|
- ...and more: https://jarv.is/uses
|
||||||
|
|
||||||
# VIEW SOURCE
|
# VIEW SOURCE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user