diff --git a/.agent/skills/news-system/SKILL.md b/.agent/skills/news-system/SKILL.md
index 7cc547a3b..a20b07dad 100644
--- a/.agent/skills/news-system/SKILL.md
+++ b/.agent/skills/news-system/SKILL.md
@@ -53,10 +53,10 @@ Simply remove `news: true` from the post's frontmatter (or set it to `false`). T
**`GET /api/news`**
-| Param | Default | Description |
-|------------|---------|------------------------------------|
-| `page` | `1` | Page number (1-indexed) |
-| `pageSize` | `6` | Items per page (max 12) |
+| Param | Default | Description |
+| ---------- | ------- | ----------------------- |
+| `page` | `1` | Page number (1-indexed) |
+| `pageSize` | `6` | Items per page (max 12) |
The endpoint decodes HTML entities from Substack RSS (e.g., `’` → `'`).
@@ -72,6 +72,7 @@ The endpoint decodes HTML entities from Substack RSS (e.g., `’` → `'`).
## Substack Integration
The Substack feed is fetched server-side from `https://pauseai.substack.com/feed`. Items are parsed from RSS XML using regex extraction for:
+
- `
` → card title
- `` → card subtitle
- `` → card href
diff --git a/package.json b/package.json
index 5d6d928ee..0596ec8c7 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
"@fontsource/roboto-slab": "^5.2.8",
"@fontsource/saira-condensed": "^5.2.8",
"@glidejs/glide": "~3.6.2",
+ "@notionhq/client": "^2.2.15",
"@number-flow/svelte": "^0.3.13",
"@pagefind/default-ui": "^1.4.0",
"@prgm/sveltekit-progress-bar": "^2.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf9bf9282..4ef83bad0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@glidejs/glide':
specifier: ~3.6.2
version: 3.6.2
+ '@notionhq/client':
+ specifier: ^2.2.15
+ version: 2.2.15
'@number-flow/svelte':
specifier: ^0.3.13
version: 0.3.13(svelte@4.2.20)
@@ -1233,6 +1236,10 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@notionhq/client@2.2.15':
+ resolution: {integrity: sha512-XhdSY/4B1D34tSco/GION+23GMjaS9S2zszcqYkMHo8RcWInymF6L1x+Gk7EmHdrSxNFva2WM8orhC4BwQCwgw==}
+ engines: {node: '>=12'}
+
'@number-flow/svelte@0.3.13':
resolution: {integrity: sha512-mvbxDeSFa1o/E4vGhrWuawAFCgcn5qTQ/s++FIoD88es5+JQa/aMQUypTy7qXIreTtTvncpIbkKdw9DMnweaSw==}
peerDependencies:
@@ -4403,6 +4410,13 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
+ '@notionhq/client@2.2.15':
+ dependencies:
+ '@types/node-fetch': 2.6.13
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
'@number-flow/svelte@0.3.13(svelte@4.2.20)':
dependencies:
esm-env: 1.2.2
diff --git a/src/lib/components/LanguageSwitcher.svelte b/src/lib/components/LanguageSwitcher.svelte
index a03381783..a02b1353f 100644
--- a/src/lib/components/LanguageSwitcher.svelte
+++ b/src/lib/components/LanguageSwitcher.svelte
@@ -165,6 +165,7 @@
diff --git a/src/lib/server/notion-people.ts b/src/lib/server/notion-people.ts
new file mode 100644
index 000000000..350f4ad7c
--- /dev/null
+++ b/src/lib/server/notion-people.ts
@@ -0,0 +1,77 @@
+import { getNotionClient } from './notion.js'
+import type { Person } from '$lib/types'
+import { defaultTitle } from '$lib/config'
+
+export async function fetchNotionPeople(): Promise {
+ const notion = getNotionClient()
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const results: any[] = []
+ let cursor: string | undefined = undefined
+
+ const dbId = '8948430b6a9940d5976581b71d9b3cd1'
+
+ try {
+ while (true) {
+ const response = await notion.databases.query({
+ database_id: dbId!,
+ start_cursor: cursor
+ })
+
+ // Append correctly based on the new SDK return type
+ if (response.results && Array.isArray(response.results)) {
+ results.push(...response.results)
+ }
+
+ if (!response.has_more) break
+ cursor = response.next_cursor || undefined
+ }
+
+ return results.map(notionPageToPerson)
+ } catch (error) {
+ console.error('Error fetching from Notion:', error)
+ throw error
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function notionPageToPerson(page: any): Person {
+ const props = page.properties
+
+ const getText = (propName: string) => {
+ const prop = props[propName]
+ if (!prop) return ''
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (prop.type === 'title') return prop.title.map((t: any) => t.plain_text).join('')
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (prop.type === 'rich_text') return prop.rich_text.map((t: any) => t.plain_text).join('')
+ if (prop.type === 'select') return prop.select?.name || ''
+ return ''
+ }
+
+ const getCheckbox = (propName: string) => {
+ return props[propName]?.checkbox || false
+ }
+
+ const getNumber = (propName: string) => {
+ return props[propName]?.number ?? undefined
+ }
+
+ const getImage = (propName: string) => {
+ const files = props[propName]?.files
+ if (!files || files.length === 0) return undefined
+ const file = files[0]
+ return file.type === 'external' ? file.external.url : file.file.url
+ }
+
+ return {
+ id: page.id,
+ name: getText('Full name'),
+ bio: '',
+ title: getText('Title') || defaultTitle,
+ image: getImage('Photo') || getImage('Image'),
+ privacy: getCheckbox('Privacy'),
+ checked: getCheckbox('About') || getCheckbox('ABout'),
+ duplicate: getCheckbox('duplicate'),
+ order: getNumber('About order') ?? 999
+ }
+}
diff --git a/src/lib/server/notion.ts b/src/lib/server/notion.ts
new file mode 100644
index 000000000..239322339
--- /dev/null
+++ b/src/lib/server/notion.ts
@@ -0,0 +1,14 @@
+import { Client } from '@notionhq/client'
+import { env } from '$env/dynamic/private'
+
+let notion: Client | null = null
+
+export function getNotionClient() {
+ if (!notion) {
+ const auth = env.NOTION_API_KEY?.trim().replace(/^["']|["']$/g, '')
+ notion = new Client({
+ auth: auth
+ })
+ }
+ return notion
+}
diff --git a/src/posts/australia-detail.md b/src/posts/australia-detail.md
index 7a74e65bb..d72ec56f3 100644
--- a/src/posts/australia-detail.md
+++ b/src/posts/australia-detail.md
@@ -1,5 +1,5 @@
---
-title: " PauseAI Australia (our campaigns)"
+title: ' PauseAI Australia (our campaigns)'
slug: australia-detail
description: More information about the Australian chapter of PauseAI
---
diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte
index e7cd99f50..9a5c059b4 100644
--- a/src/routes/about/+page.svelte
+++ b/src/routes/about/+page.svelte
@@ -106,6 +106,17 @@
{/each}
+
+
Essential Information
diff --git a/src/routes/api/about/+server.ts b/src/routes/api/about/+server.ts
index cc701db03..bbb4c69c1 100644
--- a/src/routes/api/about/+server.ts
+++ b/src/routes/api/about/+server.ts
@@ -1,8 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-export const prerender = false
-
-import { fetchAllPages } from '$lib/airtable'
-import { defaultTitle } from '$lib/config'
+import { fetchNotionPeople } from '$lib/server/notion-people'
import type { Person } from '$lib/types'
import { generateCacheControlRecord } from '$lib/utils'
import { json } from '@sveltejs/kit'
@@ -11,13 +7,13 @@ import { json } from '@sveltejs/kit'
export type AboutApiResponse = Record
/**
- * Fallback people data to use in development if Airtable fetch fails
+ * Fallback people data to use in development if Notion fetch fails
*/
const fallbackPeople: Person[] = [
{
id: 'fallback-stub1',
name: '[FALLBACK DATA] Example Person',
- bio: 'I hold places when Airtable API is unavailable.',
+ bio: '',
title: 'Placeholder',
image: 'https://api.dicebear.com/7.x/bottts/svg?seed=fallback1',
privacy: false,
@@ -27,7 +23,7 @@ const fallbackPeople: Person[] = [
{
id: 'fallback-stub2',
name: '[FALLBACK DATA] Holdor',
- bio: 'Thrown at games',
+ bio: '',
title: 'of Plays',
image: 'https://api.dicebear.com/7.x/bottts/svg?seed=fallback2',
privacy: false,
@@ -36,24 +32,8 @@ const fallbackPeople: Person[] = [
}
]
-function recordToPerson(record: any): Person {
- return {
- id: record.id || 'noId',
- name: record.fields['Full name'],
- bio: record.fields.Bio2,
- title: record.fields.Title || defaultTitle,
- image: (record.fields.Photo && record.fields.Photo[0].thumbnails.large.url) || undefined,
- privacy: record.fields.Privacy,
- checked: record.fields.About,
- duplicate: record.fields.duplicate,
- order: record.fields['About order'] || 999
- }
-}
-
-const AIRTABLE_FILTER = `{Title} != ""`
-
const filter = (p: Person) => {
- return p.checked && p.title?.trim() !== '' && p.title !== defaultTitle && !p.duplicate
+ return p.checked && !p.duplicate
}
const getGroupKey = (order: number | undefined): string => {
@@ -68,31 +48,13 @@ const getGroupKey = (order: number | undefined): string => {
return 'National Chapter Leads'
}
-export async function GET({ fetch, setHeaders }) {
- const url = `https://api.airtable.com/v0/appWPTGqZmUcs3NWu/tblL1icZBhTV1gQ9o`
+export async function GET({ setHeaders }) {
setHeaders(generateCacheControlRecord({ public: true, maxAge: 60 * 60 }))
try {
- // Create fallback records in the expected Airtable format
- const fallbackRecords = fallbackPeople.map((person) => ({
- id: person.id,
- fields: {
- 'Full name': person.name,
- Bio2: person.bio,
- Title: person.title,
- Photo: [{ thumbnails: { large: { url: person.image } } }],
- Privacy: person.privacy,
- About: person.checked, // Assuming 'About' maps to checked based on your code
- 'About order': person.order || 999
- }
- }))
-
- const records = await fetchAllPages(fetch, url, fallbackRecords, {
- filterByFormula: AIRTABLE_FILTER
- })
-
- const sortedPeople = records
- .map(recordToPerson)
+ const people = await fetchNotionPeople()
+
+ const sortedPeople = people
.filter(filter)
.sort((a, b) => {
// Primary sort: numerical order field