From 79c7aff48b48d3f740f001346e2e439e998ae96e Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 23 Jan 2026 04:10:51 +0000 Subject: [PATCH] feat: switch to Astro DB for guestbook - Add @astrojs/db integration - Define Guestbook schema in db/config.ts - Add seed data for development - Update db.ts to use astro:db - Add guestbook section to homepage with form - Update env vars to use ASTRO_DB_REMOTE_URL --- apps/blog/.env.example | 4 +- apps/blog/astro.config.mjs | 3 +- apps/blog/db/config.ts | 16 +++ apps/blog/db/seed.ts | 22 ++++ apps/blog/package-lock.json | 215 ++++++++++++++++++++++++++++++++ apps/blog/package.json | 2 +- apps/blog/src/lib/db.ts | 45 ++----- apps/blog/src/pages/index.astro | 50 ++++++++ apps/blog/src/styles/global.css | 45 +++++++ 9 files changed, 365 insertions(+), 37 deletions(-) create mode 100644 apps/blog/db/config.ts create mode 100644 apps/blog/db/seed.ts diff --git a/apps/blog/.env.example b/apps/blog/.env.example index c59c9b0..dc42cb1 100644 --- a/apps/blog/.env.example +++ b/apps/blog/.env.example @@ -1,5 +1,5 @@ -TURSO_DATABASE_URL=libsql://your-db.turso.io -TURSO_AUTH_TOKEN=your-token +ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io +ASTRO_DB_APP_TOKEN=your-token GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret ADMIN_GITHUB_ID=your-github-user-id diff --git a/apps/blog/astro.config.mjs b/apps/blog/astro.config.mjs index 75b484f..fc75032 100644 --- a/apps/blog/astro.config.mjs +++ b/apps/blog/astro.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel'; +import db from '@astrojs/db'; import auth from 'auth-astro'; import remarkDirective from 'remark-directive'; import remarkGfm from 'remark-gfm'; @@ -10,7 +11,7 @@ import remarkAside from './src/plugins/remark-aside.ts'; export default defineConfig({ output: 'static', adapter: vercel(), - integrations: [auth()], + integrations: [db(), auth()], markdown: { remarkPlugins: [ remarkGfm, diff --git a/apps/blog/db/config.ts b/apps/blog/db/config.ts new file mode 100644 index 0000000..2f492d1 --- /dev/null +++ b/apps/blog/db/config.ts @@ -0,0 +1,16 @@ +import { defineDb, defineTable, column } from 'astro:db'; + +const Guestbook = defineTable({ + columns: { + id: column.number({ primaryKey: true }), + name: column.text(), + message: column.text(), + url: column.text({ optional: true }), + createdAt: column.date({ default: new Date() }), + approved: column.boolean({ default: false }), + }, +}); + +export default defineDb({ + tables: { Guestbook }, +}); diff --git a/apps/blog/db/seed.ts b/apps/blog/db/seed.ts new file mode 100644 index 0000000..ec0eced --- /dev/null +++ b/apps/blog/db/seed.ts @@ -0,0 +1,22 @@ +import { db, Guestbook } from 'astro:db'; + +export default async function seed() { + await db.insert(Guestbook).values([ + { + id: 1, + name: 'alice', + message: 'love the site!', + url: 'https://example.com', + createdAt: new Date('2026-01-20'), + approved: true, + }, + { + id: 2, + name: 'bob', + message: 'great blog posts', + url: null, + createdAt: new Date('2026-01-18'), + approved: true, + }, + ]); +} diff --git a/apps/blog/package-lock.json b/apps/blog/package-lock.json index 7cf1527..2dfcb51 100644 --- a/apps/blog/package-lock.json +++ b/apps/blog/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@ily/blog", "dependencies": { + "@astrojs/db": "^0.19.0", "@astrojs/vercel": "^9.0.4", "@auth/core": "^0.37.4", "@libsql/client": "^0.17.0", @@ -318,6 +319,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@astrojs/db": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@astrojs/db/-/db-0.19.0.tgz", + "integrity": "sha512-YrVsqxwODr6Bid4nRgzGsF9K8K8xSoFd7j8bAU+4CxN3tSBx/1kmTE3BClwfVH2xO74wFVsyr7ucBzw/yEsEBw==", + "license": "MIT", + "dependencies": { + "@libsql/client": "^0.17.0", + "deep-diff": "^1.0.2", + "drizzle-orm": "^0.42.0", + "nanoid": "^5.1.6", + "piccolore": "^0.1.3", + "prompts": "^2.4.2", + "yargs-parser": "^21.1.1", + "zod": "^3.25.76" + } + }, "node_modules/@astrojs/internal-helpers": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz", @@ -1444,6 +1461,13 @@ } } }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1453,6 +1477,127 @@ "node": ">=8" } }, + "node_modules/drizzle-orm": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.42.0.tgz", + "integrity": "sha512-pS8nNJm2kBNZwrOjTHJfdKkaU+KuUQmV/vk5D57NojDq4FG+0uAYGMulXtYT///HfgsMF0hnFFvu1ezI3OwOkg==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1695,6 +1840,15 @@ "license": "MIT", "optional": true }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/libsql": { "version": "0.5.22", "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.22.tgz", @@ -1784,6 +1938,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -1901,6 +2073,12 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -1938,6 +2116,19 @@ "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", "license": "ISC" }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2024,6 +2215,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2338,6 +2535,24 @@ "engines": { "node": ">=18" } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/apps/blog/package.json b/apps/blog/package.json index a631989..8b1de3a 100644 --- a/apps/blog/package.json +++ b/apps/blog/package.json @@ -7,9 +7,9 @@ "preview": "astro preview" }, "dependencies": { + "@astrojs/db": "^0.19.0", "@astrojs/vercel": "^9.0.4", "@auth/core": "^0.37.4", - "@libsql/client": "^0.17.0", "astro": "^5.16.13", "auth-astro": "^4.2.0", "js-yaml": "^4.1.1" diff --git a/apps/blog/src/lib/db.ts b/apps/blog/src/lib/db.ts index f9d2ff2..e18054e 100644 --- a/apps/blog/src/lib/db.ts +++ b/apps/blog/src/lib/db.ts @@ -1,50 +1,29 @@ -import { createClient } from '@libsql/client'; +import { db, Guestbook, eq, desc } from 'astro:db'; -export const db = createClient({ - url: import.meta.env.TURSO_DATABASE_URL, - authToken: import.meta.env.TURSO_AUTH_TOKEN, -}); - -export interface GuestbookEntry { - id: number; - name: string; - message: string; - url: string | null; - created_at: string; - approved: number; -} +export type GuestbookEntry = typeof Guestbook.$inferSelect; export async function getApprovedEntries(): Promise { - const result = await db.execute( - 'SELECT * FROM guestbook WHERE approved = 1 ORDER BY created_at DESC' - ); - return result.rows as unknown as GuestbookEntry[]; + return db.select().from(Guestbook).where(eq(Guestbook.approved, true)).orderBy(desc(Guestbook.createdAt)); } export async function getPendingEntries(): Promise { - const result = await db.execute( - 'SELECT * FROM guestbook WHERE approved = 0 ORDER BY created_at DESC' - ); - return result.rows as unknown as GuestbookEntry[]; + return db.select().from(Guestbook).where(eq(Guestbook.approved, false)).orderBy(desc(Guestbook.createdAt)); } export async function createEntry(name: string, message: string, url: string | null): Promise { - await db.execute({ - sql: 'INSERT INTO guestbook (name, message, url) VALUES (?, ?, ?)', - args: [name, message, url], + await db.insert(Guestbook).values({ + name, + message, + url, + createdAt: new Date(), + approved: false, }); } export async function approveEntry(id: number): Promise { - await db.execute({ - sql: 'UPDATE guestbook SET approved = 1 WHERE id = ?', - args: [id], - }); + await db.update(Guestbook).set({ approved: true }).where(eq(Guestbook.id, id)); } export async function deleteEntry(id: number): Promise { - await db.execute({ - sql: 'DELETE FROM guestbook WHERE id = ?', - args: [id], - }); + await db.delete(Guestbook).where(eq(Guestbook.id, id)); } diff --git a/apps/blog/src/pages/index.astro b/apps/blog/src/pages/index.astro index 9711195..3e9eb65 100644 --- a/apps/blog/src/pages/index.astro +++ b/apps/blog/src/pages/index.astro @@ -5,6 +5,7 @@ import yaml from 'js-yaml'; import bookmarksRaw from '../data/bookmarks.yaml?raw'; import fs from 'node:fs'; import path from 'node:path'; +import { getApprovedEntries, type GuestbookEntry } from '../lib/db'; interface Bookmark { date: string; @@ -35,6 +36,13 @@ const txtFiles: TxtFile[] = fs.existsSync(txtDir) .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()) : []; +let guestbookEntries: GuestbookEntry[] = []; +try { + guestbookEntries = await getApprovedEntries(); +} catch { + // DB not available during dev without env vars +} + function formatDate(date: Date): string { const d = String(date.getDate()).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0'); @@ -78,5 +86,47 @@ function extractDomain(url: string): string { bookmarks
 `${formatBookmarkDate(b.date)}    ${b.title} (${extractDomain(b.url)})`).join('\n')} />
 
+
+
+ guestbook +
 0
+    ? guestbookEntries.map(e => `${formatDate(e.createdAt)}    ${e.url ? `${e.name}` : e.name}: ${e.message}`).join('\n')
+    : 'no entries yet'} />
+  
+ + + + +
+
+
+ + diff --git a/apps/blog/src/styles/global.css b/apps/blog/src/styles/global.css index b919912..727c884 100644 --- a/apps/blog/src/styles/global.css +++ b/apps/blog/src/styles/global.css @@ -46,3 +46,48 @@ summary::-webkit-details-marker { details pre { margin: 0; } + +#guestbook-form { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-left: 1rem; + margin-top: 0.5rem; +} + +#guestbook-form input, +#guestbook-form textarea { + font-family: monospace; + font-size: 1rem; + padding: 0.25rem; + border: 1px solid #888; + background: transparent; + color: inherit; +} + +#guestbook-form textarea { + min-height: 3rem; + resize: vertical; +} + +#guestbook-form button { + font-family: monospace; + font-size: 1rem; + padding: 0.25rem 0.5rem; + cursor: pointer; + background: transparent; + border: 1px solid #888; + color: inherit; + align-self: flex-start; +} + +#guestbook-form button:hover { + background: #888; + color: #000; +} + +#guestbook-status { + padding-left: 1rem; + color: #888; + font-family: monospace; +}