Add Sanity CMS integration
- Install @sanity/client + @portabletext/to-html - studio/ — Sanity v3 project with post schema (title, slug, date, category, summary, cover image with hotspot, Portable Text body). Run with `cd studio && npm install && sanity dev` after setting SANITY_STUDIO_PROJECT_ID. - src/lib/sanity.ts — typed client, GROQ queries, Portable Text → HTML conversion - src/lib/news.ts — adds getArticles() / getArticle() async functions that prefer Sanity when SANITY_PROJECT_ID is set; fall back to local markdown otherwise - events/index.astro, events/[slug].astro, events/page/[page].astro — switch to async getArticles()/getArticle(); [slug].astro drops getStaticPaths() in favour of SSR request-time fetch with redirect-on-not-found - .env.example — documents SANITY_PROJECT_ID / SANITY_DATASET / SANITY_API_TOKEN Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51fb8c1553
commit
fc644510af
12 changed files with 497 additions and 22 deletions
14
.env.example
Normal file
14
.env.example
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# ── Sanity CMS ───────────────────────────────────────────────────────────────
|
||||||
|
# Get these values from https://www.sanity.io/manage after creating a project.
|
||||||
|
# Without SANITY_PROJECT_ID the site falls back to the local markdown files
|
||||||
|
# in src/content/news/.
|
||||||
|
|
||||||
|
# Your Sanity project ID (required to enable Sanity)
|
||||||
|
SANITY_PROJECT_ID=
|
||||||
|
|
||||||
|
# Dataset name — "production" is the default created by `sanity init`
|
||||||
|
SANITY_DATASET=production
|
||||||
|
|
||||||
|
# Read token — only needed for draft/preview content.
|
||||||
|
# Leave blank to serve published content without authentication.
|
||||||
|
SANITY_API_TOKEN=
|
||||||
197
package-lock.json
generated
197
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"@neondatabase/serverless": "^0.10.4",
|
"@neondatabase/serverless": "^0.10.4",
|
||||||
|
"@portabletext/to-html": "^5.0.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||||
|
|
@ -40,6 +41,7 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "^1.1.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
|
"@sanity/client": "^7.22.1",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"astro": "^5.16.6",
|
"astro": "^5.16.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
@ -2111,6 +2113,40 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@portabletext/to-html": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-w59PcErj5JXUCv9tbV2npqJmcnORTAftCMLp0vc9FnWrXL3C9qYvuB2MQbdHsZEOesF3VmwqUsYUgjm7PX4JTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@portabletext/toolkit": "^5.0.2",
|
||||||
|
"@portabletext/types": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19 <22 || >=22.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@portabletext/toolkit": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-Njc1LE1PMJkTx/wEPqZ6sOWGgFgX2B47fxpOQ/Ia4ByhsZoA5Sq8dNvvV5F052j/xE8TbOLiBEjS848FkKADDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@portabletext/types": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19 <22 || >=22.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@portabletext/types": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@portabletext/types/-/types-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-djfIGU9n6DRrunlvj2nIDAp17URo/nA4jSXGvf+Gupx8NLLy9fmJBZ3GL8yhqn9lSVc+cKCharjOa3aOBnWbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19 <22 || >=22.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
|
@ -3736,6 +3772,33 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@sanity/client": {
|
||||||
|
"version": "7.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sanity/client/-/client-7.22.1.tgz",
|
||||||
|
"integrity": "sha512-DrQqLAes7W9Ek2iKgTZ+ffY1v3DURJ6//iXJZMEQSUeRbxls2ifRTdV1gRS26v8nMkL0OCH+WCoUUseD4vhuQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sanity/eventsource": "^5.0.2",
|
||||||
|
"get-it": "^8.7.2",
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"rxjs": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sanity/eventsource": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sanity/eventsource/-/eventsource-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-/B9PMkUvAlUrpRq0y+NzXgRv5lYCLxZNsBJD2WXVnqZYOfByL9oQBV7KiTaARuObp5hcQYuPfOAVjgXe3hrixA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/event-source-polyfill": "1.0.5",
|
||||||
|
"@types/eventsource": "1.1.15",
|
||||||
|
"event-source-polyfill": "1.0.31",
|
||||||
|
"eventsource": "2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@shikijs/core": {
|
"node_modules/@shikijs/core": {
|
||||||
"version": "3.20.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.20.0.tgz",
|
||||||
|
|
@ -4269,6 +4332,18 @@
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/event-source-polyfill": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/eventsource": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
|
|
@ -5810,6 +5885,21 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-6IvPrADQyyPGLpMnUh6kfKiqy7SrbXbjoUuZ90WMBJKErzv2pCiwlGEXjRX9/54OnTq+XFVnkOnOMzclLI5aEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
|
@ -6389,12 +6479,27 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-source-polyfill": {
|
||||||
|
"version": "1.0.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
|
||||||
|
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
|
|
@ -6782,6 +6887,21 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-it": {
|
||||||
|
"version": "8.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-it/-/get-it-8.7.3.tgz",
|
||||||
|
"integrity": "sha512-Fcv6n05iKMY/obxWb7wL0T8xOiUN/5ptBlg/Vf2WAvfs175wAV7Qf+UtWKp7Jdneu+KbUe6MIL0js2WBzoAoyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^7.0.0",
|
||||||
|
"is-retry-allowed": "^2.2.0",
|
||||||
|
"through2": "^4.0.2",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-nonce": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
|
@ -7314,6 +7434,18 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-retry-allowed": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||||
|
|
@ -8664,6 +8796,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
|
@ -9858,6 +10002,20 @@
|
||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
|
@ -10250,6 +10408,15 @@
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
|
@ -10592,6 +10759,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
|
@ -10854,6 +11030,15 @@
|
||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/through2": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-inflate": {
|
"node_modules/tiny-inflate": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
|
|
@ -11015,6 +11200,18 @@
|
||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"@neondatabase/serverless": "^0.10.4",
|
"@neondatabase/serverless": "^0.10.4",
|
||||||
|
"@portabletext/to-html": "^5.0.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "^1.1.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
|
"@sanity/client": "^7.22.1",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"astro": "^5.16.6",
|
"astro": "^5.16.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
import { isSanityConfigured, getSanityPosts, getSanityPost } from "./sanity";
|
||||||
|
|
||||||
export type NewsArticle = {
|
export type NewsArticle = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -76,3 +77,27 @@ export const formatNewsDate = (date: string): string => {
|
||||||
if (Number.isNaN(parsed)) return date;
|
if (Number.isNaN(parsed)) return date;
|
||||||
return format(new Date(parsed), "MMM yyyy");
|
return format(new Date(parsed), "MMM yyyy");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Async data access (prefers Sanity when configured) ────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all articles. Uses Sanity when SANITY_PROJECT_ID is set,
|
||||||
|
* otherwise falls back to the local markdown files.
|
||||||
|
*/
|
||||||
|
export async function getArticles(): Promise<NewsArticle[]> {
|
||||||
|
if (isSanityConfigured()) {
|
||||||
|
return getSanityPosts();
|
||||||
|
}
|
||||||
|
return newsArticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single article by slug. Uses Sanity when SANITY_PROJECT_ID is set,
|
||||||
|
* otherwise falls back to the local markdown files.
|
||||||
|
*/
|
||||||
|
export async function getArticle(slug: string): Promise<NewsArticle | undefined> {
|
||||||
|
if (isSanityConfigured()) {
|
||||||
|
return getSanityPost(slug);
|
||||||
|
}
|
||||||
|
return getNewsArticle(slug);
|
||||||
|
}
|
||||||
|
|
|
||||||
89
src/lib/sanity.ts
Normal file
89
src/lib/sanity.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { createClient } from '@sanity/client';
|
||||||
|
import { toHTML } from '@portabletext/to-html';
|
||||||
|
import type { NewsArticle } from './news';
|
||||||
|
|
||||||
|
// ── Client ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const sanityClient = createClient({
|
||||||
|
projectId: import.meta.env.SANITY_PROJECT_ID ?? '',
|
||||||
|
dataset: import.meta.env.SANITY_DATASET ?? 'production',
|
||||||
|
apiVersion: '2024-01-01',
|
||||||
|
// useCdn in production; always false during dev so edits show immediately
|
||||||
|
useCdn: import.meta.env.PROD ?? false,
|
||||||
|
// Token only needed for draft previews; public published content works without one
|
||||||
|
token: import.meta.env.SANITY_API_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function isSanityConfigured(): boolean {
|
||||||
|
return Boolean(import.meta.env.SANITY_PROJECT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GROQ queries ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const POST_FIELDS = /* groq */ `
|
||||||
|
_id,
|
||||||
|
title,
|
||||||
|
"slug": slug.current,
|
||||||
|
date,
|
||||||
|
category,
|
||||||
|
summary,
|
||||||
|
"image": image.asset->url,
|
||||||
|
body
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ALL_POSTS_QUERY = /* groq */ `*[_type == "post"] | order(date desc) { ${POST_FIELDS} }`;
|
||||||
|
const POST_BY_SLUG_QUERY = /* groq */ `*[_type == "post" && slug.current == $slug][0] { ${POST_FIELDS} }`;
|
||||||
|
|
||||||
|
// ── Raw Sanity shape ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface SanityPost {
|
||||||
|
_id: string;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
date: string;
|
||||||
|
category: string;
|
||||||
|
summary: string;
|
||||||
|
image?: string | null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
body?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Converters ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function toNewsArticle(post: SanityPost): NewsArticle {
|
||||||
|
const html = post.body
|
||||||
|
? toHTML(post.body, {
|
||||||
|
components: {
|
||||||
|
marks: {
|
||||||
|
link: ({ children, value }) => {
|
||||||
|
const target = value?.blank ? ' target="_blank" rel="noreferrer"' : '';
|
||||||
|
return `<a href="${value?.href ?? '#'}"${target}>${children}</a>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.title,
|
||||||
|
date: post.date ?? '',
|
||||||
|
summary: post.summary ?? '',
|
||||||
|
image: post.image ?? undefined,
|
||||||
|
slug: post.slug,
|
||||||
|
category: post.category ?? 'News',
|
||||||
|
body: '', // raw markdown not used when coming from Sanity
|
||||||
|
html,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getSanityPosts(): Promise<NewsArticle[]> {
|
||||||
|
const posts = await sanityClient.fetch<SanityPost[]>(ALL_POSTS_QUERY);
|
||||||
|
return posts.map(toNewsArticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSanityPost(slug: string): Promise<NewsArticle | undefined> {
|
||||||
|
const post = await sanityClient.fetch<SanityPost | null>(POST_BY_SLUG_QUERY, { slug });
|
||||||
|
return post ? toNewsArticle(post) : undefined;
|
||||||
|
}
|
||||||
|
|
@ -3,16 +3,14 @@ import Base from "../../layouts/Base.astro";
|
||||||
import SiteHeader from "../../components/SiteHeader.astro";
|
import SiteHeader from "../../components/SiteHeader.astro";
|
||||||
import SiteFooter from "../../components/SiteFooter.astro";
|
import SiteFooter from "../../components/SiteFooter.astro";
|
||||||
import { navigationItems } from "../../lib/navigation";
|
import { navigationItems } from "../../lib/navigation";
|
||||||
import { newsArticles, formatNewsDate } from "../../lib/news";
|
import { getArticle, formatNewsDate } from "../../lib/news";
|
||||||
|
|
||||||
export function getStaticPaths() {
|
const { slug } = Astro.params;
|
||||||
return newsArticles.map((article) => ({
|
const article = await getArticle(slug!);
|
||||||
params: { slug: article.slug },
|
|
||||||
props: { article },
|
if (!article) {
|
||||||
}));
|
return Astro.redirect("/events", 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { article } = Astro.props;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base
|
<Base
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||||
import SiteHeader from "../../components/SiteHeader.astro";
|
import SiteHeader from "../../components/SiteHeader.astro";
|
||||||
import SiteFooter from "../../components/SiteFooter.astro";
|
import SiteFooter from "../../components/SiteFooter.astro";
|
||||||
import { navigationItems } from "../../lib/navigation";
|
import { navigationItems } from "../../lib/navigation";
|
||||||
import { newsArticles } from "../../lib/news";
|
import { getArticles } from "../../lib/news";
|
||||||
|
|
||||||
const featured = newsArticles[0];
|
const allArticles = await getArticles();
|
||||||
const rest = newsArticles.slice(1);
|
const featured = allArticles[0];
|
||||||
|
const rest = allArticles.slice(1);
|
||||||
|
|
||||||
const pageSeo = {
|
const pageSeo = {
|
||||||
title: "News",
|
title: "News",
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,14 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||||
import SiteHeader from "../../../components/SiteHeader.astro";
|
import SiteHeader from "../../../components/SiteHeader.astro";
|
||||||
import SiteFooter from "../../../components/SiteFooter.astro";
|
import SiteFooter from "../../../components/SiteFooter.astro";
|
||||||
import { navigationItems } from "../../../lib/navigation";
|
import { navigationItems } from "../../../lib/navigation";
|
||||||
import { newsArticles } from "../../../lib/news";
|
import { getArticles } from "../../../lib/news";
|
||||||
|
|
||||||
export function getStaticPaths() {
|
|
||||||
const PER_PAGE = 6;
|
|
||||||
const totalPages = Math.ceil(newsArticles.length / PER_PAGE);
|
|
||||||
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
||||||
.filter((page) => page > 1)
|
|
||||||
.map((page) => ({ params: { page: String(page) } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const allArticles = await getArticles();
|
||||||
const currentPage = Number(Astro.params.page ?? "1");
|
const currentPage = Number(Astro.params.page ?? "1");
|
||||||
const PER_PAGE = 6;
|
const PER_PAGE = 6;
|
||||||
const totalPages = Math.ceil(newsArticles.length / PER_PAGE);
|
const totalPages = Math.ceil(allArticles.length / PER_PAGE);
|
||||||
const start = (currentPage - 1) * PER_PAGE;
|
const start = (currentPage - 1) * PER_PAGE;
|
||||||
const pagedArticles = newsArticles.slice(start, start + PER_PAGE);
|
const pagedArticles = allArticles.slice(start, start + PER_PAGE);
|
||||||
const pageHref = (page: number) => (page === 1 ? "/events" : `/events/page/${page}`);
|
const pageHref = (page: number) => (page === 1 ? "/events" : `/events/page/${page}`);
|
||||||
|
|
||||||
const pageSeo = {
|
const pageSeo = {
|
||||||
|
|
|
||||||
15
studio/package.json
Normal file
15
studio/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "rda-studio",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "sanity dev",
|
||||||
|
"build": "sanity build",
|
||||||
|
"deploy": "sanity deploy"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"sanity": "^3.66.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
studio/sanity.config.ts
Normal file
22
studio/sanity.config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { defineConfig } from 'sanity';
|
||||||
|
import { structureTool } from 'sanity/structure';
|
||||||
|
import { visionTool } from '@sanity/vision';
|
||||||
|
import { schemaTypes } from './schemaTypes';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
name: 'rda-v3',
|
||||||
|
title: 'Highland Group RDA',
|
||||||
|
|
||||||
|
// Fill these in after running `sanity init` or creating a project at sanity.io/manage
|
||||||
|
projectId: process.env.SANITY_STUDIO_PROJECT_ID ?? '',
|
||||||
|
dataset: process.env.SANITY_STUDIO_DATASET ?? 'production',
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
structureTool(),
|
||||||
|
visionTool(), // GROQ query playground — remove in production if desired
|
||||||
|
],
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
types: schemaTypes,
|
||||||
|
},
|
||||||
|
});
|
||||||
3
studio/schemaTypes/index.ts
Normal file
3
studio/schemaTypes/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { postType } from './postType';
|
||||||
|
|
||||||
|
export const schemaTypes = [postType];
|
||||||
116
studio/schemaTypes/postType.ts
Normal file
116
studio/schemaTypes/postType.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { defineField, defineType } from 'sanity';
|
||||||
|
|
||||||
|
export const postType = defineType({
|
||||||
|
name: 'post',
|
||||||
|
title: 'Post',
|
||||||
|
type: 'document',
|
||||||
|
fields: [
|
||||||
|
defineField({
|
||||||
|
name: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
type: 'string',
|
||||||
|
validation: (r) => r.required(),
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'slug',
|
||||||
|
title: 'Slug',
|
||||||
|
type: 'slug',
|
||||||
|
options: { source: 'title', maxLength: 96 },
|
||||||
|
validation: (r) => r.required(),
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'date',
|
||||||
|
title: 'Date',
|
||||||
|
type: 'date',
|
||||||
|
options: { dateFormat: 'YYYY-MM-DD' },
|
||||||
|
validation: (r) => r.required(),
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'category',
|
||||||
|
title: 'Category',
|
||||||
|
type: 'string',
|
||||||
|
options: {
|
||||||
|
list: [
|
||||||
|
{ title: 'News', value: 'News' },
|
||||||
|
{ title: 'Events', value: 'Events' },
|
||||||
|
{ title: 'Fundraising', value: 'Fundraising' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
initialValue: 'News',
|
||||||
|
validation: (r) => r.required(),
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'summary',
|
||||||
|
title: 'Summary',
|
||||||
|
type: 'text',
|
||||||
|
rows: 3,
|
||||||
|
description: 'One or two sentences shown in article cards and meta descriptions.',
|
||||||
|
validation: (r) => r.required().max(300),
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'image',
|
||||||
|
title: 'Cover image',
|
||||||
|
type: 'image',
|
||||||
|
options: { hotspot: true },
|
||||||
|
fields: [
|
||||||
|
defineField({
|
||||||
|
name: 'alt',
|
||||||
|
title: 'Alt text',
|
||||||
|
type: 'string',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'body',
|
||||||
|
title: 'Body',
|
||||||
|
type: 'array',
|
||||||
|
of: [
|
||||||
|
{
|
||||||
|
type: 'block',
|
||||||
|
styles: [
|
||||||
|
{ title: 'Normal', value: 'normal' },
|
||||||
|
{ title: 'Heading 2', value: 'h2' },
|
||||||
|
{ title: 'Heading 3', value: 'h3' },
|
||||||
|
{ title: 'Quote', value: 'blockquote' },
|
||||||
|
],
|
||||||
|
marks: {
|
||||||
|
decorators: [
|
||||||
|
{ title: 'Bold', value: 'strong' },
|
||||||
|
{ title: 'Italic', value: 'em' },
|
||||||
|
],
|
||||||
|
annotations: [
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
type: 'object',
|
||||||
|
title: 'Link',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'href',
|
||||||
|
type: 'url',
|
||||||
|
title: 'URL',
|
||||||
|
validation: (r) =>
|
||||||
|
r.uri({ allowRelative: true, scheme: ['http', 'https', 'mailto', 'tel'] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blank',
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Open in new tab',
|
||||||
|
initialValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'image', options: { hotspot: true } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
preview: {
|
||||||
|
select: {
|
||||||
|
title: 'title',
|
||||||
|
subtitle: 'date',
|
||||||
|
media: 'image',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue