first version which seems to run

This commit is contained in:
Gerhard Scheikl
2026-04-01 19:15:22 +02:00
parent 80e965aa57
commit fc107d576c
25 changed files with 963 additions and 40 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
.git
node_modules
.next
.env
.env.*
!.env.production.example
*.md
!content/**/*.md
!content/**/*.mdx
.vscode
tests
playwright.config.ts
playwright-report
test-results
*.tsbuildinfo

26
.env.production.example Normal file
View File

@@ -0,0 +1,26 @@
# Self-hosted TinaCMS Production Environment
# Copy this to .env and fill in the values
# Authentication
NEXTAUTH_SECRET=change-me-to-a-random-secret
NEXTAUTH_URL=https://docs.linumiq.com
# Gitea Git Provider
GITEA_TOKEN=your-gitea-api-token
GITEA_OWNER=LinumIQ
GITEA_REPO=docs
GITEA_URL=https://git.linumiq.com
TINA_GIT_BRANCH=main
# Redis (internal docker network)
KV_REST_API_TOKEN=change-me-to-a-random-token
REDIS_PASSWORD=change-me-to-a-random-password
# Site
NEXT_PUBLIC_SITE_URL=https://docs.linumiq.com
# Optional
# NEXT_PUBLIC_GTM_ID=
# GITHUB_TOKEN=
# GITHUB_REPO=
# GITHUB_OWNER=

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
.pnpm-store
# testing
/coverage

61
Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
FROM node:22-alpine AS base
RUN corepack enable
RUN apk add --no-cache libc6-compat
# --- Dependencies stage ---
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# --- Builder stage ---
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV DOCKER_BUILD=true
ENV NEXT_TELEMETRY_DISABLED=1
ENV TINA_PUBLIC_IS_LOCAL=false
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
RUN npx tinacms build && npx next build
RUN npx pagefind --site .next --output-path .next/static/pagefind
# --- Runner stage ---
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV TINA_PUBLIC_IS_LOCAL=false
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY --from=builder /app/content ./content
COPY --from=builder /app/tina ./tina
# Copy the startup indexing script
COPY --from=builder /app/scripts/index-database.mjs ./scripts/index-database.mjs
# Pagefind static search index
COPY --from=builder /app/.next/static/pagefind ./.next/static/pagefind
# Create cache directory with correct permissions
RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next/cache
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Entrypoint: index content into Redis, then start Next.js
CMD ["sh", "-c", "node scripts/index-database.mjs && node server.js"]

13
content/users/index.json Normal file
View File

@@ -0,0 +1,13 @@
{
"users": [
{
"name": "Admin",
"email": "admin@linumiq.com",
"username": "admin",
"password": {
"value": "admin",
"passwordChangeRequired": true
}
}
]
}

56
docker-compose.yml Normal file
View File

@@ -0,0 +1,56 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- TINA_PUBLIC_IS_LOCAL=false
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
- GITEA_TOKEN=${GITEA_TOKEN}
- GITEA_OWNER=${GITEA_OWNER:-LinumIQ}
- GITEA_REPO=${GITEA_REPO:-docs}
- GITEA_URL=${GITEA_URL:-https://git.linumiq.com}
- TINA_GIT_BRANCH=${TINA_GIT_BRANCH:-main}
- KV_REST_API_URL=http://redis-http:80
- KV_REST_API_TOKEN=${KV_REST_API_TOKEN}
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://docs.linumiq.com}
depends_on:
redis-http:
condition: service_started
redis:
condition: service_started
networks:
- tinadocs
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
networks:
- tinadocs
restart: unless-stopped
redis-http:
image: hiett/serverless-redis-http:latest
environment:
- SRH_MODE=env
- SRH_TOKEN=${KV_REST_API_TOKEN}
- SRH_CONNECTION_STRING=redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
redis:
condition: service_started
networks:
- tinadocs
restart: unless-stopped
volumes:
redis-data:
networks:
tinadocs:
driver: bridge

View File

@@ -7,12 +7,16 @@ const isStatic = process.env.EXPORT_MODE === "static";
const basePath = process.env.NEXT_PUBLIC_BASE_PATH;
const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX || basePath;
const isDocker = process.env.DOCKER_BUILD === "true";
const extraConfig = {};
if (isStatic) {
extraConfig.output = "export";
extraConfig.trailingSlash = true;
extraConfig.skipTrailingSlashRedirect = true;
} else if (isDocker) {
extraConfig.output = "standalone";
}
module.exports = {

View File

@@ -4,7 +4,8 @@
"private": true,
"scripts": {
"predev": "node scripts/check-pagefind.js",
"dev": "node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev --turbopack\"",
"dev": "TINA_PUBLIC_IS_LOCAL=true node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev --turbopack\"",
"dev:prod": "TINA_PUBLIC_IS_LOCAL=false node --dns-result-order=ipv4first ./node_modules/.bin/tinacms dev -c \"next dev\"",
"build": "echo 'Starting TinaCMS build...' && node --dns-result-order=ipv4first ./node_modules/.bin/tinacms build && echo 'TinaCMS build completed. Starting Next.js build...' && next build",
"postbuild": "npx pagefind --site .next --output-path .next/static/pagefind && next-sitemap",
"build-local-pagefind": "node --dns-result-order=ipv4first ./node_modules/.bin/tinacms build && next build && npx pagefind --site .next --output-subdir ../public/pagefind",
@@ -22,15 +23,19 @@
"@next/third-parties": "^16.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-tabs": "^1.1.12",
"@tinacms/datalayer": "^2.0.14",
"@upstash/redis": "^1.37.0",
"copy-to-clipboard": "^3.3.3",
"date-fns": "^4.1.0",
"fast-glob": "^3.3.3",
"html-to-md": "^0.8.8",
"js-base64": "^3.7.8",
"lodash": "^4.17.21",
"mermaid": "^11.6.0",
"monaco-editor": "^0.52.2",
"motion": "^12.15.0",
"next": "^15.4.10",
"next-auth": "^4.24.13",
"next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1",
"prismjs": "^1.30.0",
@@ -43,8 +48,10 @@
"rehype-pretty-code": "^0.14.1",
"shiki": "^3.6.0",
"tinacms": "^3.4.1",
"tinacms-authjs": "^21.0.1",
"title-case": "^4.3.2",
"typescript": "5.8.3"
"typescript": "5.8.3",
"upstash-redis-level": "^1.1.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",

View File

@@ -0,0 +1,4 @@
import NextAuth from "next-auth";
import { authOptions } from "../tina/[...routes]";
export default NextAuth(authOptions);

View File

@@ -0,0 +1,124 @@
import {
TinaNodeBackend,
LocalBackendAuthProvider,
} from "@tinacms/datalayer";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getServerSession } from "next-auth/next";
import databaseClient from "../../../tina/__generated__/databaseClient";
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";
const TINA_CREDENTIALS_PROVIDER_NAME = "TinaCredentials";
const authenticate = async (
dbClient: typeof databaseClient,
username: string,
password: string
) => {
try {
const result = await dbClient.authenticate({ username, password });
return result.data?.authenticate || null;
} catch (e) {
return null;
}
};
const authOptions = {
callbacks: {
jwt: async ({ token: jwt, account }: any) => {
if (account) {
try {
if (jwt?.sub) {
const result = await databaseClient.authorize({ sub: jwt.sub });
jwt.role = result.data?.authorize ? "user" : "guest";
jwt.passwordChangeRequired =
result.data?.authorize?._password?.passwordChangeRequired ||
false;
}
} catch {
// ignore auth errors
}
if (jwt.role === undefined) {
jwt.role = "guest";
}
}
return jwt;
},
session: async ({ session, token: jwt }: any) => {
session.user.role = jwt.role;
session.user.passwordChangeRequired = jwt.passwordChangeRequired;
session.user.sub = jwt.sub;
return session;
},
},
session: { strategy: "jwt" as const },
secret: process.env.NEXTAUTH_SECRET as string,
providers: [
CredentialsProvider({
name: TINA_CREDENTIALS_PROVIDER_NAME,
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials: any) =>
authenticate(databaseClient, credentials.username, credentials.password),
}),
],
};
const handler = TinaNodeBackend({
authProvider: isLocal
? LocalBackendAuthProvider()
: {
initialize: async () => {},
isAuthorized: async (req: any, res: any) => {
const session = await getServerSession(req, res, authOptions);
if (!req.session) {
Object.defineProperty(req, "session", {
value: session,
writable: false,
});
}
if (!(session as any)?.user) {
return {
errorCode: 401,
errorMessage: "Unauthorized",
isAuthorized: false,
};
}
if ((session as any)?.user?.role !== "user") {
return {
errorCode: 403,
errorMessage: "Forbidden",
isAuthorized: false,
};
}
return { isAuthorized: true };
},
extraRoutes: {
auth: {
secure: false,
handler: async (req: any, res: any, opts: any) => {
const url = new URL(
req.url,
`http://${req.headers?.host || "localhost"}`
);
const authSubRoutes = url.pathname
?.replace(`${opts.basePath}auth/`, "")
?.split("/");
req.query.nextauth = authSubRoutes;
await NextAuth(authOptions)(req, res);
},
},
},
},
databaseClient,
});
export default (req: any, res: any) => {
return handler(req, res);
};
export { authOptions };

367
pnpm-lock.yaml generated
View File

@@ -16,13 +16,19 @@ importers:
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@next/third-parties':
specifier: ^16.1.1
version: 16.1.1(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
version: 16.1.1(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.15
version: 2.1.15(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-tabs':
specifier: ^1.1.12
version: 1.1.12(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tinacms/datalayer':
specifier: ^2.0.14
version: 2.0.14(react@19.2.3)(typescript@5.8.3)
'@upstash/redis':
specifier: ^1.37.0
version: 1.37.0
copy-to-clipboard:
specifier: ^3.3.3
version: 3.3.3
@@ -35,6 +41,9 @@ importers:
html-to-md:
specifier: ^0.8.8
version: 0.8.8
js-base64:
specifier: ^3.7.8
version: 3.7.8
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -49,7 +58,10 @@ importers:
version: 12.23.1(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next:
specifier: ^15.4.10
version: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-auth:
specifier: ^4.24.13
version: 4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -86,12 +98,18 @@ importers:
tinacms:
specifier: ^3.4.1
version: 3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3))
tinacms-authjs:
specifier: ^21.0.1
version: 21.0.1(next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)))(yup@1.7.1)
title-case:
specifier: ^4.3.2
version: 4.3.2
typescript:
specifier: 5.8.3
version: 5.8.3
upstash-redis-level:
specifier: ^1.1.1
version: 1.1.1
devDependencies:
'@biomejs/biome':
specifier: ^1.9.4
@@ -122,7 +140,7 @@ importers:
version: 7.1.0(monaco-editor@0.52.2)(webpack@5.100.0)
next-sitemap:
specifier: ^4.2.3
version: 4.2.3(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
version: 4.2.3(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
pagefind:
specifier: ^1.3.0
version: 1.3.0
@@ -800,24 +818,28 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
@@ -1509,8 +1531,8 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
'@graphql-codegen/plugin-helpers@6.1.0':
resolution: {integrity: sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==}
'@graphql-codegen/plugin-helpers@6.2.0':
resolution: {integrity: sha512-TKm0Q0+wRlg354Qt3PyXc+sy6dCKxmNofBsgmHoFZNVHtzMQSSgNT+rUWdwBwObQ9bFHiUVsDIv8QqxKMiKmpw==}
engines: {node: '>=16'}
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
@@ -1601,6 +1623,12 @@ packages:
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
'@graphql-tools/utils@11.0.0':
resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==}
engines: {node: '>=16.0.0'}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
'@graphql-tools/utils@9.2.1':
resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==}
peerDependencies:
@@ -1674,89 +1702,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1912,24 +1956,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.4.8':
resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.4.8':
resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.4.8':
resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.4.8':
resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==}
@@ -1986,6 +2034,9 @@ packages:
cpu: [x64]
os: [win32]
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -2782,24 +2833,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==}
@@ -2859,12 +2914,21 @@ packages:
react: '>=18.3.1 <20.0.0'
react-dom: '>=18.3.1 <20.0.0'
'@tinacms/datalayer@2.0.14':
resolution: {integrity: sha512-5PQX6+4RNFQ8Rh9ISMsJ8pj4Lqm81FLmVHEOyU7+ziJCyF4TT/v3B89BW4VqVmVr0iRqowVfW68Ap+7m8J+RZQ==}
'@tinacms/graphql@2.1.1':
resolution: {integrity: sha512-lX2taaOdSEmgEiCEogN8dkf3RH4yxd3RG4bqj4lL4gHD47QPcaFI5/TDNdEzw6y0Zmkg58/KClQpWya4o84wxw==}
'@tinacms/graphql@2.2.2':
resolution: {integrity: sha512-xwjM7WTUtUWodRnIyRozP9qJvqvwW1ZUZPoq8fHuq1kBaJyaEWbq+KWnd6IEG8fzX4w0aLhfwvAA19/9Ucoq/Q==}
'@tinacms/mdx@2.0.5':
resolution: {integrity: sha512-IyzUB/9ep+z1VI+ap2ZpqdDYtlMABG40wwoIQviI4iS+bB5ka7B2bTeK1VhN+1FoKjXfPv5e7Qmev6es/hE1Eg==}
'@tinacms/mdx@2.1.0':
resolution: {integrity: sha512-3A8fDUhhRqtchU0M5vmDM9pnvG4sXy4FG2hdiUlfyeytAF0HwBi7EZq7n0Cv7qvgd5E0vXSKqOZ9H/lOl8QQXQ==}
'@tinacms/metrics@2.0.1':
resolution: {integrity: sha512-CKRzs3fbuwcwobNOAFGdo94hLfGzLkKeREpKoAmVj/zFJZ40NJQ+p3tEUm/o1xPA4Zwgfq0QYmN3gNNLHymgGA==}
peerDependencies:
@@ -2876,6 +2940,12 @@ packages:
react: '>=16.14.0'
yup: ^1.0.0
'@tinacms/schema-tools@2.7.0':
resolution: {integrity: sha512-uc6XL8xtldoFQ3xHnXkijJTALEB+gEsyvsf7q/1Gs1uvG5Alm6xPmBAdWxM4fUZv1WDQZwH46SW+cXK1Ej+N8w==}
peerDependencies:
react: '>=16.14.0'
yup: ^1.0.0
'@tinacms/search@1.2.2':
resolution: {integrity: sha512-5KbhocVAZvSWj6xoZbAUqju37kZKm1RWlc8xC3L0j6Rg3RGyTyL8NWYTyflw7rjgZrlUMaIxO6Q3CwDpX1fjZQ==}
@@ -3262,6 +3332,12 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@upstash/redis@1.24.3':
resolution: {integrity: sha512-gw6d4IA1biB4eye5ESaXc0zOlVQI94aptsBvVcTghYWu1kRmOrJFoMFEDCa8p5uzluyYAOFCuY2GWLR6O4ZoIw==}
'@upstash/redis@1.37.0':
resolution: {integrity: sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==}
'@vitejs/plugin-react@3.1.0':
resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -4806,6 +4882,9 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
jotai-optics@0.4.0:
resolution: {integrity: sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==}
peerDependencies:
@@ -4836,6 +4915,9 @@ packages:
react:
optional: true
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-cookie@2.2.1:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
@@ -4893,6 +4975,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
jsonpath-plus@10.4.0:
resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==}
engines: {node: '>=18.0.0'}
hasBin: true
katex@0.16.22:
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
hasBin: true
@@ -4980,24 +5067,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@@ -5075,6 +5166,10 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.424.0:
resolution: {integrity: sha512-x2Nj2aytk1iOyHqt4hKenfVlySq0rYxNeEf8hE0o+Yh0iE36Rqz0rkngVdv2uQtjZ70LAE73eeplhhptYt9x4Q==}
peerDependencies:
@@ -5410,6 +5505,9 @@ packages:
micromark-util-types@1.0.2:
resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==}
micromark-util-types@1.1.0:
resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
micromark-util-types@2.0.2:
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
@@ -5561,6 +5659,20 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
next-auth@4.24.13:
resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==}
peerDependencies:
'@auth/core': 0.34.3
next: ^12.2.5 || ^13 || ^14 || ^15 || ^16
nodemailer: ^7.0.7
react: ^17.0.2 || ^18 || ^19
react-dom: ^17.0.2 || ^18 || ^19
peerDependenciesMeta:
'@auth/core':
optional: true
nodemailer:
optional: true
next-sitemap@4.2.3:
resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==}
engines: {node: '>=14.18'}
@@ -5641,10 +5753,17 @@ packages:
nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@@ -5660,6 +5779,10 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
oidc-token-hash@5.2.0:
resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==}
engines: {node: ^10.13.0 || >=12.0.0}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -5676,6 +5799,9 @@ packages:
oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
optics-ts@2.4.1:
resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==}
@@ -6044,6 +6170,14 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
preact@10.29.0:
resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@@ -6054,6 +6188,9 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
@@ -6814,6 +6951,15 @@ packages:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
tinacms-authjs@21.0.1:
resolution: {integrity: sha512-BGmaO6fnkj6YLRoATFv0rSgwSxi++cZrygMivarBiK67MDjXXX5Vw11Eumy66ClYDc2xIG8WsKgDqbSN5ltjbQ==}
peerDependencies:
next: ^12.2.5 || ^13 || ^14 || ^15
next-auth: ^4.22.1
react: ^17.0.2 || ^18 || ^19
react-dom: ^17.0.2 || ^18 || ^19
tinacms: 3.7.1
tinacms@3.4.1:
resolution: {integrity: sha512-vHzBP7Lt0wj7wUZoMsRLvekyLTuPoTA/wGn2BnormLSOj9AXRrxrPwH8qRbz1EyUsrVFbBG2xFCBjlxJh7kj9A==}
peerDependencies:
@@ -6940,6 +7086,9 @@ packages:
resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==}
engines: {node: '>=0.10.0'}
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
@@ -7028,6 +7177,9 @@ packages:
upper-case@2.0.2:
resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==}
upstash-redis-level@1.1.1:
resolution: {integrity: sha512-CAtuEJ/t0xaK7ZLY0OKMG5EUYXWx+q+BRPb+mT+1+Dy4x22gzI6wBN28gLnyCwC3NDaEeJifuKx0oohc2UbBQQ==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -7094,6 +7246,10 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -7245,6 +7401,9 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
@@ -8776,9 +8935,9 @@ snapshots:
lodash: 4.17.21
tslib: 2.6.3
'@graphql-codegen/plugin-helpers@6.1.0(graphql@15.8.0)':
'@graphql-codegen/plugin-helpers@6.2.0(graphql@15.8.0)':
dependencies:
'@graphql-tools/utils': 10.11.0(graphql@15.8.0)
'@graphql-tools/utils': 11.0.0(graphql@15.8.0)
change-case-all: 1.0.15
common-tags: 1.8.2
graphql: 15.8.0
@@ -8921,6 +9080,14 @@ snapshots:
graphql: 15.8.0
tslib: 2.8.1
'@graphql-tools/utils@11.0.0(graphql@15.8.0)':
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0)
'@whatwg-node/promise-helpers': 1.3.2
cross-inspect: 1.0.1
graphql: 15.8.0
tslib: 2.8.1
'@graphql-tools/utils@9.2.1(graphql@15.8.0)':
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0)
@@ -9231,9 +9398,9 @@ snapshots:
'@next/swc-win32-x64-msvc@15.4.8':
optional: true
'@next/third-parties@16.1.1(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
'@next/third-parties@16.1.1(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
dependencies:
next: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
third-party-capital: 1.0.20
@@ -9264,6 +9431,8 @@ snapshots:
'@pagefind/windows-x64@1.3.0':
optional: true
'@panva/hkdf@1.2.1': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -10168,7 +10337,7 @@ snapshots:
'@tinacms/cli@2.1.5(@codemirror/language@6.0.0)(@types/node@24.2.1)(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(lightningcss@1.30.1)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@3.29.5)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(terser@5.44.1)(use-sync-external-store@1.6.0(react@19.2.3))(yaml@2.8.1)':
dependencies:
'@graphql-codegen/core': 2.6.8(graphql@15.8.0)
'@graphql-codegen/plugin-helpers': 6.1.0(graphql@15.8.0)
'@graphql-codegen/plugin-helpers': 6.2.0(graphql@15.8.0)
'@graphql-codegen/typescript': 4.1.6(graphql@15.8.0)
'@graphql-codegen/typescript-operations': 4.6.1(graphql@15.8.0)
'@graphql-codegen/visitor-plugin-common': 4.1.2(graphql@15.8.0)
@@ -10249,6 +10418,15 @@ snapshots:
- use-sync-external-store
- yaml
'@tinacms/datalayer@2.0.14(react@19.2.3)(typescript@5.8.3)':
dependencies:
'@tinacms/graphql': 2.2.2(react@19.2.3)(typescript@5.8.3)
transitivePeerDependencies:
- react
- react-native-b4a
- supports-color
- typescript
'@tinacms/graphql@2.1.1(react@19.2.3)(typescript@5.8.3)':
dependencies:
'@iarna/toml': 2.2.5
@@ -10277,6 +10455,34 @@ snapshots:
- supports-color
- typescript
'@tinacms/graphql@2.2.2(react@19.2.3)(typescript@5.8.3)':
dependencies:
'@iarna/toml': 2.2.5
'@tinacms/mdx': 2.1.0(react@19.2.3)(typescript@5.8.3)(yup@1.7.1)
'@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1)
abstract-level: 1.0.4
date-fns: 2.30.0
es-toolkit: 1.42.0
fast-glob: 3.3.3
fs-extra: 11.3.2
glob-parent: 6.0.2
graphql: 15.8.0
gray-matter: 4.0.3
isomorphic-git: 1.35.0
js-sha1: 0.6.0
js-yaml: 3.14.2
jsonpath-plus: 10.4.0
many-level: 2.0.0
micromatch: 4.0.8
normalize-path: 3.0.0
readable-stream: 4.7.0
yup: 1.7.1
transitivePeerDependencies:
- react
- react-native-b4a
- supports-color
- typescript
'@tinacms/mdx@2.0.5(react@19.2.3)(typescript@5.8.3)(yup@1.7.1)':
dependencies:
'@tinacms/schema-tools': 2.5.0(react@19.2.3)(yup@1.7.1)
@@ -10314,6 +10520,43 @@ snapshots:
- typescript
- yup
'@tinacms/mdx@2.1.0(react@19.2.3)(typescript@5.8.3)(yup@1.7.1)':
dependencies:
'@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1)
acorn: 8.8.2
ccount: 2.0.1
estree-util-is-identifier-name: 2.1.0
mdast-util-compact: 4.1.1
mdast-util-directive: 2.2.4
mdast-util-from-markdown: 1.3.0
mdast-util-gfm: 2.0.2
mdast-util-mdx-jsx: 2.1.2
mdast-util-to-markdown: 1.5.0
micromark-extension-gfm: 2.0.3
micromark-factory-mdx-expression: 1.0.7
micromark-factory-space: 1.0.0
micromark-factory-whitespace: 1.0.0
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.1.0
parse-entities: 4.0.1
prettier: 2.8.8
remark: 14.0.2
remark-gfm: 2.0.0
remark-mdx: 2.3.0
stringify-entities: 4.0.3
typedoc: 0.26.11(typescript@5.8.3)
unist-util-source: 4.0.2
unist-util-stringify-position: 3.0.3
unist-util-visit: 4.1.2
uvu: 0.5.6
vfile-message: 3.1.4
transitivePeerDependencies:
- react
- supports-color
- typescript
- yup
'@tinacms/metrics@2.0.1(fs-extra@11.3.2)':
dependencies:
fs-extra: 11.3.2
@@ -10326,6 +10569,14 @@ snapshots:
yup: 1.7.1
zod: 3.25.76
'@tinacms/schema-tools@2.7.0(react@19.2.3)(yup@1.7.1)':
dependencies:
picomatch-browser: 2.2.6
react: 19.2.3
url-pattern: 1.0.3
yup: 1.7.1
zod: 3.25.76
'@tinacms/search@1.2.2(abstract-level@1.0.4)(react@19.2.3)(sucrase@3.35.0)(typescript@5.8.3)(yup@1.7.1)':
dependencies:
'@tinacms/graphql': 2.1.1(react@19.2.3)(typescript@5.8.3)
@@ -10820,6 +11071,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@upstash/redis@1.24.3':
dependencies:
crypto-js: 4.2.0
'@upstash/redis@1.37.0':
dependencies:
uncrypto: 0.1.3
'@vitejs/plugin-react@3.1.0(vite@4.5.14(@types/node@24.2.1)(lightningcss@1.30.1)(terser@5.44.1))':
dependencies:
'@babel/core': 7.28.5
@@ -12510,6 +12769,8 @@ snapshots:
jiti@2.4.2: {}
jose@4.15.9: {}
jotai-optics@0.4.0(jotai@2.8.4(@types/react@19.2.9)(react@19.2.3))(optics-ts@2.4.1):
dependencies:
jotai: 2.8.4(@types/react@19.2.9)(react@19.2.3)
@@ -12527,6 +12788,8 @@ snapshots:
'@types/react': 19.2.9
react: 19.2.3
js-base64@3.7.8: {}
js-cookie@2.2.1: {}
js-sha1@0.6.0: {}
@@ -12572,6 +12835,12 @@ snapshots:
'@jsep-plugin/regex': 1.0.4(jsep@1.4.0)
jsep: 1.4.0
jsonpath-plus@10.4.0:
dependencies:
'@jsep-plugin/assignment': 1.3.0(jsep@1.4.0)
'@jsep-plugin/regex': 1.0.4(jsep@1.4.0)
jsep: 1.4.0
katex@0.16.22:
dependencies:
commander: 8.3.0
@@ -12718,6 +12987,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
lucide-react@0.424.0(react@19.2.3):
dependencies:
react: 19.2.3
@@ -13446,6 +13719,8 @@ snapshots:
micromark-util-types@1.0.2: {}
micromark-util-types@1.1.0: {}
micromark-util-types@2.0.2: {}
micromark@3.2.0:
@@ -13610,20 +13885,35 @@ snapshots:
neo-async@2.6.2: {}
next-sitemap@4.2.3(next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@babel/runtime': 7.28.4
'@panva/hkdf': 1.2.1
cookie: 0.7.1
jose: 4.15.9
next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.29.0
preact-render-to-string: 5.2.6(preact@10.29.0)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
uuid: 8.3.2
next-sitemap@4.2.3(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
dependencies:
'@corex/deepmerge': 4.0.43
'@next/env': 13.5.11
fast-glob: 3.3.3
minimist: 1.2.8
next: 15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
next@15.4.10(@babel/core@7.28.0)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 15.4.10
'@swc/helpers': 0.5.15
@@ -13631,7 +13921,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.0)(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 15.4.8
'@next/swc-darwin-x64': 15.4.8
@@ -13682,8 +13972,12 @@ snapshots:
nullthrows@1.1.1: {}
oauth@0.9.15: {}
object-assign@4.1.1: {}
object-hash@2.2.0: {}
object-hash@3.0.0: {}
object-inspect@1.12.3: {}
@@ -13692,6 +13986,8 @@ snapshots:
object-inspect@1.13.4: {}
oidc-token-hash@5.2.0: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
@@ -13714,6 +14010,13 @@ snapshots:
regex: 6.0.1
regex-recursion: 6.0.2
openid-client@5.7.1:
dependencies:
jose: 4.15.9
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.2.0
optics-ts@2.4.1: {}
p-limit@3.1.0:
@@ -14144,6 +14447,13 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact-render-to-string@5.2.6(preact@10.29.0):
dependencies:
preact: 10.29.0
pretty-format: 3.8.0
preact@10.29.0: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
@@ -14161,6 +14471,8 @@ snapshots:
prettier@2.8.8: {}
pretty-format@3.8.0: {}
prism-react-renderer@2.4.1(react@19.2.3):
dependencies:
'@types/prismjs': 1.26.5
@@ -14988,12 +15300,12 @@ snapshots:
hey-listen: 1.0.8
tslib: 2.8.1
styled-jsx@5.1.6(@babel/core@7.28.0)(react@19.2.3):
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.0
'@babel/core': 7.28.5
stylis@4.3.6: {}
@@ -15125,6 +15437,17 @@ snapshots:
throttle-debounce@3.0.1: {}
tinacms-authjs@21.0.1(next-auth@4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)))(yup@1.7.1):
dependencies:
'@tinacms/schema-tools': 2.7.0(react@19.2.3)(yup@1.7.1)
next: 15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-auth: 4.24.13(next@15.4.10(@babel/core@7.28.5)(@playwright/test@1.54.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
tinacms: 3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3))
transitivePeerDependencies:
- yup
tinacms@3.4.1(@types/react@19.2.9)(abstract-level@1.0.4)(immer@10.2.0)(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.9))(@types/node@24.2.1)(@types/react@19.2.9)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(slate-dom@0.114.0(slate@0.114.0))(slate@0.114.0)(sucrase@3.35.0)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@19.2.3)):
dependencies:
'@ariakit/react': 0.4.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -15320,6 +15643,8 @@ snapshots:
unc-path-regex@0.1.2: {}
uncrypto@0.1.3: {}
undici-types@7.10.0: {}
undici-types@7.16.0: {}
@@ -15434,6 +15759,12 @@ snapshots:
dependencies:
tslib: 2.8.1
upstash-redis-level@1.1.1:
dependencies:
'@upstash/redis': 1.24.3
abstract-level: 1.0.4
module-error: 1.0.2
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -15481,6 +15812,8 @@ snapshots:
uuid@11.1.0: {}
uuid@8.3.2: {}
uuid@9.0.1: {}
uvu@0.5.6:
@@ -15646,6 +15979,8 @@ snapshots:
yallist@3.1.1: {}
yallist@4.0.0: {}
yallist@5.0.0: {}
yaml@2.8.1: {}

3
run_dev_container.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker run --rm --user 1000:1000 -v $PWD:/files -p 3000:3000 -ti node:lts bash -c "cd /files ; npm run dev"

4
run_in_container.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
docker run --rm --user 1000:1000 -v $PWD:/files -p 3000:3000 -ti node:lts \
bash -c 'cd /files; "$@"' -- "$@"

View File

@@ -0,0 +1,87 @@
/**
* Startup script: Indexes TinaCMS content into the database (Redis).
*
* In a self-hosted setup, the database (Redis) starts empty.
* This script reads the generated schema files from the filesystem,
* creates a database connection, and indexes all content into Redis.
*
* Run this BEFORE starting the Next.js server.
*/
import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import {
createDatabase,
FilesystemBridge,
} from "@tinacms/datalayer";
const require = createRequire(import.meta.url);
const { RedisLevel } = require("upstash-redis-level");
const branch = process.env.TINA_GIT_BRANCH || "main";
const kvUrl = process.env.KV_REST_API_URL;
const kvToken = process.env.KV_REST_API_TOKEN;
if (!kvUrl || !kvToken) {
console.error(
"[index-database] KV_REST_API_URL and KV_REST_API_TOKEN are required"
);
process.exit(1);
}
const generatedDir = path.join(process.cwd(), "tina", "__generated__");
const graphqlPath = path.join(generatedDir, "_graphql.json");
const schemaPath = path.join(generatedDir, "_schema.json");
const lookupPath = path.join(generatedDir, "_lookup.json");
// Verify generated files exist
for (const filePath of [graphqlPath, schemaPath, lookupPath]) {
if (!fs.existsSync(filePath)) {
console.error(`[index-database] Missing generated file: ${filePath}`);
process.exit(1);
}
}
const graphQLSchema = JSON.parse(fs.readFileSync(graphqlPath, "utf-8"));
const tinaSchema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
const lookup = JSON.parse(fs.readFileSync(lookupPath, "utf-8"));
// A no-op git provider since we only need to index, not push
const noopGitProvider = {
onPut: async () => { },
onDelete: async () => { },
};
const database = createDatabase({
gitProvider: noopGitProvider,
databaseAdapter: new RedisLevel({
redis: { url: kvUrl, token: kvToken },
debug: process.env.DEBUG === "true" || false,
}),
bridge: new FilesystemBridge(process.cwd()),
namespace: branch,
});
async function indexContent() {
console.log("[index-database] Starting content indexing...");
const startTime = Date.now();
try {
await database.indexContent({
graphQLSchema,
tinaSchema: { schema: tinaSchema },
lookup,
});
const elapsed = Date.now() - startTime;
console.log(
`[index-database] Content indexed successfully in ${elapsed}ms`
);
} catch (error) {
console.error("[index-database] Failed to index content:", error);
process.exit(1);
}
}
indexContent();

View File

@@ -1,4 +1,4 @@
import { client } from "@/tina/__generated__/client";
import { client } from "@/tina/__generated__/databaseClient";
import { type NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {

View File

@@ -3,7 +3,7 @@ import settings from "@/content/siteConfig.json";
import { fetchTinaData } from "@/services/tina/fetch-tina-data";
import { GitHubMetadataProvider } from "@/src/components/page-metadata/github-metadata-context";
import GithubConfig from "@/src/utils/github-client";
import client from "@/tina/__generated__/client";
import client from "@/tina/__generated__/databaseClient";
import { getTableOfContents } from "@/utils/docs";
import { getSeo } from "@/utils/metadata/getSeo";
import Document from ".";

View File

@@ -3,7 +3,7 @@ import settings from "@/content/siteConfig.json";
import { fetchTinaData } from "@/services/tina/fetch-tina-data";
import { GitHubMetadataProvider } from "@/src/components/page-metadata/github-metadata-context";
import GitHubClient from "@/src/utils/github-client";
import client from "@/tina/__generated__/client";
import client from "@/tina/__generated__/databaseClient";
import { getTableOfContents } from "@/utils/docs";
import { getSeo } from "@/utils/metadata/getSeo";
import Document from "./[...slug]";

View File

@@ -3,7 +3,7 @@ import AdminLink from "@/components/ui/admin-link";
import { TailwindIndicator } from "@/components/ui/tailwind-indicator";
import { ThemeSelector } from "@/components/ui/theme-selector";
import settings from "@/content/settings/config.json";
import client from "@/tina/__generated__/client";
import client from "@/tina/__generated__/databaseClient";
import { GoogleTagManager } from "@next/third-parties/google";
import { ThemeProvider } from "next-themes";
import { Inter, Roboto_Flex } from "next/font/google";
@@ -12,6 +12,10 @@ import { TabsLayout } from "@/components/docs/layout/tab-layout";
import type React from "react";
import { TinaClient } from "./tina-client";
// Force all pages to be server-rendered (not statically generated at build time)
// Required because databaseClient needs a running database connection
export const dynamic = "force-dynamic";
const isDev = process.env.NODE_ENV === "development";
const body = Inter({ subsets: ["latin"], variable: "--body-font" });
@@ -76,9 +80,13 @@ const Content = ({ children }: { children?: React.ReactNode }) => (
const DocsMenu = async ({ children }: { children?: React.ReactNode }) => {
// Fetch navigation data that will be shared across all docs pages
const navigationData = await client.queries.minimisedNavigationBarFetch({
const navigationData = JSON.parse(
JSON.stringify(
await client.queries.minimisedNavigationBarFetch({
relativePath: "docs-navigation-bar.json",
});
})
)
);
return (
<div className="relative flex flex-col w-full pb-2">

View File

@@ -165,7 +165,7 @@ export const BreadCrumbs = ({
const currentPath = usePathname();
const breadcrumbs = findBreadcrumbTrail(navigationDocsData, currentPath);
const breadcrumbs = findBreadcrumbTrail(navigationDocsData, currentPath ?? "");
if (!navigationDocsData || breadcrumbs.length === 0) {
return null;

View File

@@ -24,7 +24,9 @@ export async function fetchTinaData<T, V>(
const response = await queryFunction(variables);
return response;
// Ensure all data is plain objects (databaseClient may return
// objects with null prototypes that can't be passed to Client Components)
return JSON.parse(JSON.stringify(response));
} catch {
notFound();
}

View File

@@ -1,5 +1,5 @@
import siteConfig from "@/content/siteConfig.json";
import client from "@/tina/__generated__/client";
import client from "@/tina/__generated__/databaseClient";
/**
* A single navigation item

View File

@@ -1,22 +1,23 @@
import { defineConfig } from "tinacms";
import { defineConfig, LocalAuthProvider } from "tinacms";
import { UsernamePasswordAuthJSProvider } from "tinacms-authjs/dist/tinacms";
import { schema } from "./schema";
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";
export const config = defineConfig({
telemetry: 'disabled',
contentApiUrlOverride: "/api/tina/gql",
authProvider: isLocal
? new LocalAuthProvider()
: new UsernamePasswordAuthJSProvider(),
schema,
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
branch:
process.env.TINA_GIT_BRANCH ||
process.env.NEXT_PUBLIC_TINA_BRANCH || // custom branch env override
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF || // Vercel branch env
process.env.HEAD, // Netlify branch env
process.env.HEAD || // Netlify branch env
"main",
token: process.env.TINA_TOKEN,
media: {
// If you wanted cloudinary do this
// loadCustomStore: async () => {
// const pack = await import("next-tinacms-cloudinary");
// return pack.TinaCloudCloudinaryMediaStore;
// },
// this is the config for the tina cloud media store
tina: {
publicFolder: "public",
mediaRoot: "",
@@ -24,8 +25,8 @@ export const config = defineConfig({
accept: ["image/*", "video/*", "application/json", ".json"],
},
build: {
publicFolder: "public", // The public asset folder for your framework
outputFolder: "admin", // within the public folder
publicFolder: "public",
outputFolder: "admin",
basePath: process.env.TINA_BASE_PATH || "",
},
});

47
tina/database.ts Normal file
View File

@@ -0,0 +1,47 @@
import {
createDatabase,
createLocalDatabase,
FilesystemBridge,
} from "@tinacms/datalayer";
import { RedisLevel } from "upstash-redis-level";
import { Redis } from "@upstash/redis";
import { GiteaGitProvider } from "./gitea-git-provider";
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";
const branch = process.env.TINA_GIT_BRANCH || "main";
function createProductionDatabase() {
const giteaUrl = process.env.GITEA_URL;
const giteaToken = process.env.GITEA_TOKEN;
const giteaOwner = process.env.GITEA_OWNER;
const giteaRepo = process.env.GITEA_REPO;
const kvUrl = process.env.KV_REST_API_URL;
const kvToken = process.env.KV_REST_API_TOKEN;
if (!giteaUrl || !giteaToken || !giteaOwner || !giteaRepo) {
// During tinacms build (schema generation), env vars may not be available.
// Fall back to local database for the build step.
return createLocalDatabase();
}
return createDatabase({
gitProvider: new GiteaGitProvider({
owner: giteaOwner,
repo: giteaRepo,
token: giteaToken,
branch,
baseUrl: giteaUrl,
}),
databaseAdapter: new RedisLevel<string, Record<string, unknown>>({
redis: new Redis({
url: kvUrl || "http://localhost:8079",
token: kvToken || "example_token",
}) as any,
debug: process.env.DEBUG === "true" || false,
}),
bridge: new FilesystemBridge(process.cwd()),
namespace: branch,
});
}
export default isLocal ? createLocalDatabase() : createProductionDatabase();

123
tina/gitea-git-provider.ts Normal file
View File

@@ -0,0 +1,123 @@
import { Base64 } from "js-base64";
import type { GitProvider } from "@tinacms/datalayer";
export interface GiteaGitProviderOptions {
owner: string;
repo: string;
token: string;
branch: string;
baseUrl: string;
commitMessage?: string;
rootPath?: string;
}
export class GiteaGitProvider implements GitProvider {
owner: string;
repo: string;
branch: string;
baseUrl: string;
commitMessage: string;
rootPath?: string;
private token: string;
constructor(args: GiteaGitProviderOptions) {
this.owner = args.owner;
this.repo = args.repo;
this.branch = args.branch;
this.baseUrl = args.baseUrl.replace(/\/$/, "");
this.commitMessage = args.commitMessage || "Edited with TinaCMS";
this.rootPath = args.rootPath;
this.token = args.token;
}
private getApiUrl(path: string): string {
return `${this.baseUrl}/api/v1/repos/${this.owner}/${this.repo}/contents/${path}`;
}
private getHeaders(): Record<string, string> {
return {
Authorization: `token ${this.token}`,
"Content-Type": "application/json",
Accept: "application/json",
};
}
private getKeyWithPath(key: string): string {
return this.rootPath ? `${this.rootPath}/${key}` : key;
}
private async getFileSha(
keyWithPath: string
): Promise<string | undefined> {
try {
const url = `${this.getApiUrl(keyWithPath)}?ref=${encodeURIComponent(this.branch)}`;
const response = await fetch(url, {
method: "GET",
headers: this.getHeaders(),
});
if (!response.ok) {
return undefined;
}
const data = await response.json();
return data.sha;
} catch {
return undefined;
}
}
async onPut(key: string, value: string): Promise<void> {
const keyWithPath = this.getKeyWithPath(key);
const sha = await this.getFileSha(keyWithPath);
const body: Record<string, unknown> = {
message: this.commitMessage,
content: Base64.encode(value),
branch: this.branch,
};
if (sha) {
body.sha = sha;
}
const response = await fetch(this.getApiUrl(keyWithPath), {
method: sha ? "PUT" : "POST",
headers: this.getHeaders(),
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to save ${keyWithPath}: ${response.status} ${errorText}`
);
}
}
async onDelete(key: string): Promise<void> {
const keyWithPath = this.getKeyWithPath(key);
const sha = await this.getFileSha(keyWithPath);
if (!sha) {
throw new Error(
`Could not find file ${keyWithPath} in repo ${this.owner}/${this.repo}`
);
}
const response = await fetch(this.getApiUrl(keyWithPath), {
method: "DELETE",
headers: this.getHeaders(),
body: JSON.stringify({
message: this.commitMessage,
branch: this.branch,
sha,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to delete ${keyWithPath}: ${response.status} ${errorText}`
);
}
}
}

View File

@@ -1,4 +1,5 @@
import { type Collection, defineSchema } from "tinacms";
import { TinaUserCollection } from "tinacms-authjs/dist/tinacms";
import API_Schema_Collection from "./collections/API-schema";
import docsCollection from "./collections/docs";
import docsNavigationBarCollection from "./collections/navigation-bar";
@@ -11,5 +12,6 @@ export const schema = defineSchema({
//TODO: Investigate why casting as unknown works
API_Schema_Collection as unknown as Collection,
Settings as unknown as Collection,
TinaUserCollection as Collection,
],
});