500
+A wire came loose.
++ Something gave out on this page. Try again, it might catch. +
+ +diff --git a/.env.example b/.env.example
index b235786..a1cf2c6 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,8 @@
-IMMICH_API_URL=
-IMMICH_API_KEY=
-IMMICH_ALBUM_ID=
+# Build-time: URL of the rate-my-shots API consumed by the photography section
+RATE_MY_SHOTS_URL=
+
+# Host port the app is published on
+HOST_PORT=3000
+
+# Host directory bind-mounted to /app/data (where the SQLite DB lives)
+DATA_DIR=./data
diff --git a/.gitignore b/.gitignore
index 3d07a4d..0cdb431 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,9 @@
# production
/build
+# runtime data (SQLite counter)
+/data
+
# misc
.DS_Store
*.pem
diff --git a/Dockerfile b/Dockerfile
index 122253a..763cc91 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,11 @@
-FROM oven/bun:1 AS deps
+FROM oven/bun:1-alpine AS deps
WORKDIR /app
+RUN apk add --no-cache python3 make g++ nodejs npm
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
+RUN npm rebuild better-sqlite3
-FROM oven/bun:1 AS builder
+FROM oven/bun:1-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
@@ -12,8 +14,9 @@ ENV RATE_MY_SHOTS_URL=$RATE_MY_SHOTS_URL
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
-FROM node:24-slim AS runner
+FROM oven/bun:1-alpine AS runner
WORKDIR /app
+RUN apk add --no-cache nodejs
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
diff --git a/bun.lock b/bun.lock
index 1ca2ab6..ad720a5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -31,6 +31,7 @@
},
},
"trustedDependencies": [
+ "better-sqlite3",
"sharp",
],
"packages": {
diff --git a/docker-compose.yml b/docker-compose.yml
index e1e4582..27da33e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,6 @@ services:
ports:
- "${HOST_PORT}:3000"
environment:
- DATA_DIR: /app/data
+ DB_PATH: /app/data/db.sqlite
volumes:
- "${DATA_DIR:-./data}:/app/data"
diff --git a/next.config.ts b/next.config.ts
index 285375b..c00ef7d 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -12,6 +12,19 @@ const nextConfig: NextConfig = {
},
},
},
+ async headers() {
+ return [
+ {
+ source: "/:path*.pdf",
+ headers: [
+ {
+ key: "Content-Disposition",
+ value: "inline",
+ },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/package.json b/package.json
index 078001d..fd3729e 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"unrs-resolver"
],
"trustedDependencies": [
+ "better-sqlite3",
"sharp",
"unrs-resolver"
]
diff --git a/public/luisdralves-cv.pdf b/public/luisdralves-cv.pdf
new file mode 100644
index 0000000..7e61140
Binary files /dev/null and b/public/luisdralves-cv.pdf differ
diff --git a/public/og-image.png b/public/og-image.png
new file mode 100644
index 0000000..e1577cd
Binary files /dev/null and b/public/og-image.png differ
diff --git a/src/app/api/counter/route.ts b/src/app/api/counter/route.ts
new file mode 100644
index 0000000..de27522
--- /dev/null
+++ b/src/app/api/counter/route.ts
@@ -0,0 +1,43 @@
+import { type NextRequest, NextResponse } from "next/server";
+import { footer } from "@/content/footer";
+import { getCounter, incrementCounter } from "@/lib/db";
+import { checkRateLimit, getClientIp } from "@/lib/rate-limit";
+
+const LIMIT = 10;
+const WINDOW_MS = 60_000;
+
+export const dynamic = "force-dynamic";
+export const runtime = "nodejs";
+
+export const GET = () => {
+ try {
+ return NextResponse.json({ count: getCounter() });
+ } catch (err) {
+ console.error("[counter] GET failed:", err);
+ return NextResponse.json({ error: footer.errorMessage }, { status: 500 });
+ }
+};
+
+export const POST = (request: NextRequest) => {
+ const ip = getClientIp(request.headers);
+ const result = checkRateLimit(`counter:${ip}`, LIMIT, WINDOW_MS);
+
+ if (!result.allowed) {
+ return NextResponse.json(
+ { error: footer.rateLimitMessage },
+ {
+ status: 429,
+ headers: {
+ "Retry-After": Math.ceil(result.retryAfterMs / 1000).toString(),
+ },
+ },
+ );
+ }
+
+ try {
+ return NextResponse.json({ count: incrementCounter() });
+ } catch (err) {
+ console.error("[counter] POST failed:", err);
+ return NextResponse.json({ error: footer.errorMessage }, { status: 500 });
+ }
+};
diff --git a/src/app/error.tsx b/src/app/error.tsx
new file mode 100644
index 0000000..dc74e5b
--- /dev/null
+++ b/src/app/error.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { useEffect } from "react";
+
+type ErrorPageProps = {
+ error: Error & { digest?: string };
+ reset: () => void;
+};
+
+export default function ErrorPage({ error, reset }: ErrorPageProps) {
+ useEffect(() => {
+ console.error(error);
+ }, [error]);
+
+ return (
+ 500
+ Something gave out on this page. Try again, it might catch.
+ A wire came loose.
+