Content Management Studio for the Global Cardiovascular Research Alliance website Built with Sanity v5 β the structured content platform powering all editorial content across the GCRA website.
- Overview
- What This Studio Manages
- Project Structure
- Prerequisites
- Getting Started
- Environment Variables
- Available Scripts
- Schema Reference
- Studio Navigation Guide
- Content Workflow
- ISR Revalidation & Webhooks
- ImageKit Media Integration
- Deployment
- Contributing
- Troubleshooting
This repository is the Sanity Studio for the Global Cardiovascular Research Alliance (GCRA) website. It is a standalone content management interface that runs inside the Next.js frontend at /studio β meaning editors access it directly on the live website domain without needing a separate login or URL.
The Studio gives non-technical team members full control over all website content β research publications, events, blog posts, ambassador profiles, team members, and page-level content β through a structured, user-friendly editing interface. No developer is needed for routine content updates.
The frontend website (separate repository) consumes all content from this Studio via the Sanity Content Lake using GROQ queries over the Sanity CDN API.
| Content Type | Description | URL on Website |
|---|---|---|
| Home Page | Hero, stats strip, mission section, CTA banners | / |
| About Page | Mission, vision, values, history timeline, partners | /about |
| Membership Page | Tiers, training modules, testimonials, FAQ | /membership |
| Research Publications | Peer-reviewed papers, abstracts, PDFs, authors | /research, /research/[slug] |
| Events | Conferences, workshops, webinars, registration links | /events, /events/[slug] |
| Blog Posts | Articles, research spotlights, community news | /blog, /blog/[slug] |
| Ambassadors | Country ambassadors with map coordinates | /ambassadors |
| Team Members | Leadership and staff profiles | /about |
| Authors | Blog post author profiles | /blog/[slug] |
| Categories | Taxonomy for blog and research | Filters on /blog, /research |
| Site Settings | Org name, contact details, social links, SEO defaults | Global (all pages) |
| Navigation | Navbar links, footer columns, CTA button | Global (all pages) |
Gcralliance-Studio/
β
βββ sanity.config.ts # Root Studio configuration β registers all schemas,
β # plugins, and desk structure
β
βββ sanity/
β βββ structure/
β β βββ index.ts # Custom Studio sidebar navigation structure
β β
β βββ schemas/
β β βββ _shared.ts # Shared field helpers and reusable object types
β β β # (CTA, SEO, StatItem, Testimonial, FAQ, etc.)
β β β
β β βββ documents/
β β β βββ index.ts # All document schemas:
β β β β # post, research, event, ambassador,
β β β β # teamMember, category, author
β β β βββ singletons.ts # Singleton page schemas:
β β β # homePage, aboutPage, membershipPage,
β β β # siteSettings, navigationSettings
β β β
β β βββ objects/ # (Optional) standalone object schema files
β β
β βββ lib/
β βββ fetch.ts # Server-side Sanity fetch helper with cache tags
β
βββ src/
β βββ app/
β β βββ studio/
β β β βββ [[...tool]]/
β β β βββ page.tsx # Next.js route that serves the Studio at /studio
β β β
β β βββ api/
β β βββ sanity/
β β βββ revalidate/
β β βββ route.ts # Webhook endpoint for ISR cache revalidation
β β
β βββ lib/
β βββ sanity/
β βββ index.ts # Sanity client instances + all GROQ queries
β # consumed by the Next.js frontend
β
βββ .env.example # Template for required environment variables
βββ .env.local # Local secrets β git-ignored, never commit
βββ package.json
Before setting up, ensure you have:
- Node.js
v18.17.0or later β check withnode --version - pnpm
v8or later β install withnpm install -g pnpm - A Sanity account β create one free at sanity.io
- Access to the GCRA Sanity project β request from the project lead
- Git installed and configured
git clone https://github.com/your-org/Gcralliance-Studio.git
cd Gcralliance-Studiopnpm installCopy the example file and fill in your values:
cp .env.example .env.localOpen .env.local and add the required values β see Environment Variables for details.
- Go to sanity.io/manage
- Select the GCRA project
- Navigate to API β CORS Origins
- Click Add CORS origin
- Add
http://localhost:3000with Allow credentials: β
pnpm devThe Studio is available at: http://localhost:3000/studio
Log in with your Sanity account. On first run, you will be prompted to authenticate via the browser.
Create a .env.local file in the project root. These variables are required for the Studio and frontend to work:
# ββ Sanity (PUBLIC β safe in browser) ββββββββββββββββββββββββββββββββββββββββ
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id_here
NEXT_PUBLIC_SANITY_DATASET=production
# ββ Sanity (PRIVATE β server only, never commit) ββββββββββββββββββββββββββββββ
# Viewer token β for draft previews
# Get from: sanity.io/manage β API β Tokens β Add API token (Viewer)
SANITY_API_READ_TOKEN=sk...
# Editor token β for server-side content writes
# Get from: sanity.io/manage β API β Tokens β Add API token (Editor)
SANITY_API_TOKEN=sk...
# Webhook secret β used to validate incoming revalidation webhooks
# Generate: openssl rand -base64 32
SANITY_WEBHOOK_SECRET=your_random_secret_here
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_DEPLOY_ENV=development
β οΈ Important:.env.localis listed in.gitignoreand must never be committed to Git. Share credentials securely with team members via a password manager.
To get your Project ID:
- Go to sanity.io/manage β select the GCRA project β the Project ID is shown at the top
To generate API tokens:
- Go to sanity.io/manage β GCRA project β API β Tokens β Add API token
| Script | Command | Description |
|---|---|---|
| Start dev server | pnpm dev |
Starts Next.js with Studio at /studio |
| Build | pnpm build |
Production build |
| Start production | pnpm start |
Serves the production build |
| Lint | pnpm lint |
ESLint check across all files |
| Type check | pnpm type-check |
TypeScript compilation check (no output) |
These are the main content types editors create and manage in the Studio. Each document type has its own list view in the Studio sidebar.
| Field | Type | Required | Notes |
|---|---|---|---|
title |
String | β | Post headline |
slug |
Slug | β | Auto-generated from title |
status |
Select | β | draft or published |
publishedAt |
Datetime | Set when publishing | |
author |
Reference β author |
β | |
categories |
Reference[] β category |
||
tags |
String[] | Tag array | |
excerpt |
Text | Max 200 chars β used in card views | |
featuredImage |
Image | With alt text | |
body |
Portable Text | β | Supports images and callout blocks |
readingTime |
Number | Minutes β auto-estimated if blank | |
relatedPosts |
Reference[] β post |
Max 3 | |
seo |
SEO object | Meta title, description, OG image |
| Field | Type | Required | Notes |
|---|---|---|---|
title |
String | β | Full publication title |
slug |
Slug | β | Auto-generated |
researchStatus |
Select | β | ongoing or completed |
categories |
Reference[] β category |
||
authors |
ResearchAuthor[] | β | Name, institution, corresponding flag |
abstract |
Text | β | 300β500 words |
keyFindings |
String[] | 3β5 plain-language findings | |
pdfFile |
File | Full paper PDF upload | |
pageCount |
Number | ||
journal |
String | Journal / publication name | |
doi |
String | e.g. 10.1234/example.2024 |
|
citation |
Text | APA format citation | |
relatedResearch |
Reference[] β research |
Max 3 | |
seo |
SEO object |
| Field | Type | Required | Notes |
|---|---|---|---|
title |
String | β | |
slug |
Slug | β | |
eventType |
Select | β | conference, workshop, webinar, regional, onboarding |
startDate |
Datetime | β | |
endDate |
Datetime | ||
timezone |
String | e.g. GMT, WAT, EST |
|
isOnline |
Boolean | Hides location fields when checked | |
location |
Object | venueName, city, country, address | |
shortDescription |
Text | Max 150 chars β used in cards | |
description |
Portable Text | β | Full event description |
speakers |
Speaker[] | Name, title, photo, bio | |
registrationUrl |
URL | Calendly or custom form URL | |
isMembersOnly |
Boolean | ||
isFree |
Boolean | ||
travelGrantsAvailable |
Boolean |
| Field | Type | Required | Notes |
|---|---|---|---|
name |
String | β | |
photo |
Image | β | Used in map popup and directory card |
role |
String | e.g. Country Ambassador | |
bio |
Text | Max 150 words | |
contactEmail |
String | Displayed publicly | |
country |
String | β | |
region |
Select | β | West Africa, East Africa, South Asia, etc. |
coordinates |
Object | lat and lng β used for Mapbox map pin |
|
ambassadorSince |
Date | ||
isActive |
Boolean | Uncheck to hide without deleting | |
displayOrder |
Number | Lower = appears first in directory |
| Field | Type | Required | Notes |
|---|---|---|---|
name |
String | β | |
title |
String | β | Job title |
department |
Select | Leadership, Research, Programmes, Operations, Communications | |
bio |
Text | Max 200 words β shown on About page | |
photo |
Image | β | Headshot |
email |
String | ||
location |
String | e.g. London, UK | |
linkedin |
URL | ||
displayOrder |
Number | Lower = appears first | |
isVisible |
Boolean | Uncheck to hide from website |
Used to tag blog posts, research publications, and events for filtering.
| Field | Type | Notes |
|---|---|---|
name |
String | e.g. Epidemiology |
slug |
Slug | Auto-generated |
type |
Select | posts, research, events, all |
color |
Select | blue, green, purple, amber, red, teal |
| Field | Type | Notes |
|---|---|---|
name |
String | Full name |
slug |
Slug | Auto-generated |
photo |
Image | Profile photo |
title |
String | e.g. Senior Epidemiologist, LSHTM |
bio |
Text | Max 100 words β shown below blog posts |
linkedin |
URL | |
twitter |
URL |
These documents exist exactly once in the dataset. They are accessed directly from the Pages and Settings sections in the Studio sidebar β not as a list.
| Singleton | Studio Location | Controls |
|---|---|---|
homePage |
Pages β Home Page | Hero, stats, featured research, mission section, membership banner, newsletter section |
aboutPage |
Pages β About Page | Hero, mission & vision, core values, history timeline, partners, join CTA |
membershipPage |
Pages β Membership Page | Hero, tier cards, training modules, testimonials, FAQ, apply CTA |
siteSettings |
Settings β Site Settings | Org name, contact details, office addresses, social links, default SEO |
navigationSettings |
Settings β Navigation | Navbar links, dropdowns, footer columns, footer tagline, legal links |
These are building blocks used inside documents and singletons. They do not appear as their own list in the Studio.
| Object | Used in | Description |
|---|---|---|
cta |
homePage, navigationSettings | Button label, URL, variant, new tab toggle |
seo |
All document types | Meta title, meta description, OG image, noIndex |
statItem |
homePage | Value, label, sub-label (e.g. "2,400+ / Active Members") |
timelineItem |
aboutPage | Year, title, description for the history timeline |
partner |
aboutPage | Name, logo, URL, partnership type |
testimonial |
membershipPage | Quote, attribution, member since, photo |
faqItem |
membershipPage | Question and answer |
membershipTier |
membershipPage | Name, description, features list, eligibility, CTA |
trainingModule |
membershipPage | Title, description, duration, availability, platform |
socialLinks |
siteSettings | LinkedIn, Twitter/X, Facebook, YouTube, ResearchGate URLs |
office |
siteSettings | Office name, address, isPrimary flag |
speaker |
event | Name, title, photo, bio |
researchAuthor |
research | Name, institution, isCorresponding flag |
calloutBlock |
post, event (rich text) | Type (info/warning/success/danger) and text |
The Studio sidebar is organised into logical sections for editors:
GCRA Content
β
βββ π Pages
β βββ π Home Page β edit hero, stats, mission section, banners
β βββ βΉοΈ About Page β edit mission, team section, timeline, partners
β βββ π₯ Membership Page β edit tiers, training modules, FAQ
β
βββ βοΈ Blog Posts β create and manage blog articles
βββ π¬ Research Publications β manage research publications and PDFs
βββ π
Events β create and manage events
β
βββ π Ambassadors β manage ambassador profiles and map coordinates
βββ π€ Team Members β manage leadership and staff profiles
βββ ποΈ Authors β manage blog author profiles
β
βββ π·οΈ Categories β manage blog, research, and event categories
β
βββ βοΈ Settings
βββ π Site Settings β org details, contact info, social links
βββ π§ Navigation β navbar links, footer columns
- Open Blog Posts in the sidebar
- Click New Blog Post (top right)
- Fill in: Title β click Generate on the slug field β select an Author β set Categories
- Write the Excerpt (max 200 chars β this appears in card views and as the default meta description)
- Upload a Featured Image and fill in the Alt Text field
- Write the article in the Body field
- Scroll to SEO and fill in a custom meta title and description if needed
- Change Status from
DrafttoPublished - Set the Published Date
- Click Publish (top right)
The website will automatically update within 60 seconds via the webhook revalidation system.
- Open Research Publications β New Research Publication
- Fill in Title, Abstract, and Key Findings
- Add all Authors (mark the corresponding author)
- Upload the PDF in the Full Paper field
- Set Research Status (
OngoingorCompleted) - Set Categories
- Add DOI and journal name if available
- Set Status to
Publishedand click Publish
- Open Events β New Event
- Fill in Title, Event Type, Start Date/Time, and Timezone
- Toggle Online Event if applicable β the location fields will hide automatically
- Fill in Short Description (shown in cards) and Full Description (shown on detail page)
- Add Speakers if applicable
- Paste the Registration URL (Calendly link or custom form)
- Set Status to
Publishedand click Publish
- Go to Settings β Navigation
- Edit the Main Navigation Links array to add, remove, or reorder links
- Add dropdown items to any link by expanding it and adding Dropdown Items
- Update the Footer Link Columns array for footer navigation
- Click Publish
The website uses Incremental Static Regeneration (ISR) with on-demand revalidation. When you publish or update content in the Studio, a Sanity webhook automatically triggers a cache flush on the Next.js frontend so the updated content appears on the live site within seconds β without a full redeploy.
Editor publishes in Studio
β
Sanity sends POST request to webhook URL
β
/api/sanity/revalidate validates the request secret
β
Next.js calls revalidateTag('posts') / revalidatePath('/blog')
β
Affected pages rebuild in the background
β
Updated content served to visitors
| Document Type | Cache Tags Invalidated |
|---|---|
post |
posts, blog |
research |
research |
event |
events |
ambassador |
ambassadors |
teamMember |
team, about |
homePage |
home |
aboutPage |
about |
membershipPage |
membership |
siteSettings |
site-settings, layout |
navigationSettings |
navigation, layout |
- Go to sanity.io/manage β GCRA project β API β Webhooks
- Click Create webhook
- Configure:
- Name: ISR Revalidation
- URL:
https://yourdomain.com/api/sanity/revalidate - Dataset:
production - Trigger on: Create, Update, Delete
- Filter: (leave blank β triggers on all document types)
- Projection: (leave blank)
- HTTP method: POST
- HTTP Headers: (none needed)
- Secret: paste the value of
SANITY_WEBHOOK_SECRETfrom your environment variables
- Click Save
All images uploaded through the Studio are stored in ImageKit and delivered via their global CDN. ImageKit handles resizing, format conversion (WebP/AVIF), and optimisation automatically via URL transforms β no image processing happens on the server.
All Studio uploads are routed to the following folder hierarchy based on the Media Type field:
ghro/
βββ production/ β live website uploads
β βββ site/ β logos, favicons, OG images
β β βββ logos/
β β βββ og/
β βββ team/ β staff and leadership headshots
β βββ ambassadors/ β ambassador photos, organised by region
β β βββ west-africa/
β β βββ east-africa/
β β βββ ...
β βββ blog/ β blog post thumbnails and hero images
β β βββ 2025/
β β βββ 2024/
β βββ research/ β research publication cover images
β βββ events/ β event banners and speaker photos
β β βββ banners/
β βββ partners/ β partner and affiliate logos
β βββ membership/ β membership tier and testimonial images
β
βββ staging/ β preview/staging environment uploads
βββ dev/ β local development uploads
β οΈ Note: Research PDFs and application documents are not stored in ImageKit. PDFs upload directly to AWS S3 (HIPAA-eligible storage with a Business Associate Agreement). Only images go through ImageKit.
All folder names and filenames use:
- Lowercase only
- Hyphens instead of spaces β
west-africa/notWest Africa/ - No special characters
- Descriptive names β
helena-whitmore.jpgnotIMG_4521.jpg
The Studio is deployed as part of the Next.js website on Vercel. There is no separate Studio deployment β it is served from the same origin as the frontend at /studio.
Deployments are triggered automatically on every merge to main via the GitHub Actions CI/CD pipeline.
Before deploying to production:
- Ensure all required environment variables are set in the Vercel dashboard (not just
.env.local) - Confirm the production domain is added to CORS Origins in sanity.io/manage
- Confirm the production webhook URL is configured (see ISR Revalidation)
Set these in Vercel Dashboard β Project β Settings β Environment Variables:
| Variable | Environment |
|---|---|
NEXT_PUBLIC_SANITY_PROJECT_ID |
Production, Preview, Development |
NEXT_PUBLIC_SANITY_DATASET |
Production, Preview, Development |
SANITY_API_READ_TOKEN |
Production, Preview |
SANITY_API_TOKEN |
Production |
SANITY_WEBHOOK_SECRET |
Production |
NEXT_PUBLIC_APP_URL |
Production, Preview |
NEXT_PUBLIC_DEPLOY_ENV |
Production (production), Preview (staging) |
| Branch | Purpose |
|---|---|
main |
Production β auto-deploys to live site |
dev |
Active development β merges go here first |
feature/schema-name |
Schema additions and changes |
fix/issue-description |
Bug fixes |
Schema changes affect the data structure that editors work with. Follow this process:
-
Create a feature branch from
dev:git checkout dev git pull origin dev git checkout -b feature/add-webinar-schema
-
Make your changes to the relevant schema files in
sanity/schemas/ -
Test locally β start
pnpm devand verify the Studio renders correctly with no console errors -
Check for breaking changes β if you are removing or renaming a field that already has data in production, coordinate with the content team before merging to avoid data loss
-
Open a Pull Request to
devwith a clear description of what changed and why -
After merge to
mainβ if you added new field types or changed field names, notify the content team so they can update any existing documents
- All schema files use TypeScript with Sanity's
defineTypeanddefineFieldhelpers - Field descriptions should be written in plain English for editors β not developers
- Every image field must include a nested
alttext field for WCAG compliance - Preview functions should return a meaningful
titleandsubtitleso editors can identify documents in list views
- Check that
NEXT_PUBLIC_SANITY_PROJECT_IDis set correctly in.env.local - Check that
http://localhost:3000is added to CORS Origins at sanity.io/manage with Allow credentials: β - Open browser DevTools β Console and check for network errors
- Try a hard refresh:
Ctrl+Shift+R/Cmd+Shift+R
- Log out of sanity.io in your browser and log back in
- Ensure your Sanity account has access to the GCRA project β ask the project lead to add you
- Re-add
localhost:3000to CORS Origins if it was accidentally removed
- Confirm
cdn.sanity.iois added tonext.config.tsunderimages.remotePatterns - Check that the image field includes the
asset->projection in the GROQ query - For ImageKit images, confirm the
NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINTenv var is set
- Check that
SANITY_WEBHOOK_SECRETin.env.localmatches the secret set in the Sanity dashboard webhook - Check Vercel function logs for errors at
/api/sanity/revalidate - Confirm the webhook URL in Sanity uses
https://β nothttp:// - Test the webhook manually: Sanity dashboard β API β Webhooks β click the webhook β Test
- ISR revalidation can take up to 60 seconds β wait and hard refresh
- Check the webhook is configured and enabled (green dot in Sanity dashboard)
- If revalidation is still not working, trigger a manual Vercel redeploy as a fallback
- Run
pnpm type-checkto see all errors at once - Ensure all
defineFieldcalls have anameandtypeβ these are required - For reference fields, confirm the target document type slug is spelled correctly in
to: [{ type: 'post' }]
This repository is private and proprietary. All rights reserved β Global Cardiovascular Research Alliance Β© 2025.
Maintained by the GCRA engineering team. For content questions, contact the editorial team. For technical issues, open an issue in this repository.