This repository contains the source code for the Interledger Foundation website, built with Astro, Starlight for documentation, and Strapi as a headless CMS.
It represents the fifth major iteration of interledger.org. For background on previous versions and the site’s evolution, see the project wiki.
-
Astro provides a modern static site framework for fast, flexible site building.
-
Starlight adds a ready-made documentation system, including layouts, navigation, and styling, making it easy to write and maintain docs.
-
Strapi is the headless CMS for content management. Custom lifecycle hooks have been added to automatically synchronize content with the Astro project.
- The frontend styling is built using Tailwind CSS.
- Design tokens, utility conventions, and custom styles are documented separately: Styles README
flowchart
subgraph gcp["☁️ GCP VM"]
direction TB
appclone[("Repo Clone<br/>(running Strapi app)")]
strapidb[("Strapi Database")]
strapi[Strapi Admin portal]
appclone -->|"runs from './cms' folder"| strapi
strapi -->|"reads/writes"| strapidb
end
subgraph github["📦 GitHub Repository"]
direction TB
staging["staging branch"]
main["main branch"]
end
subgraph netlify["🚀 Netlify"]
direction TB
preview["Preview Site"]
stagingsite["Staging Site"]
production["Production Site"]
end
editor["👤 Content Editor"]
dev["👨💻 Developer"]
feature["Feature Branch"]
editor ==>|"Publish"| strapi
dev ==>|"Code / Content PR"| feature
strapi -->|"lifecycle hooks generate MDX & <br/> push via GitHub App"| staging
appclone -.->|"sync:mdx script<br/>(after pulling staging)"| strapidb
feature ==>|"PR (approved)"| staging
feature ==>|"PR"| preview
staging ==>|"Auto-build"| stagingsite
staging -->|"PR"| main
staging -.->|"Pulls updates when <br/> './cms' folder changes"| appclone
main ==>|"Auto-build"| production
classDef gcpStyle fill:#4285f4,stroke:#1967d2,color:#fff
classDef portalStyle fill:#018501,stroke:#1967d2,color:#fff
classDef githubStyle fill:#24292e,stroke:#000,color:#fff
classDef netlifyStyle fill:#00c7b7,stroke:#008577,color:#fff
classDef userStyle fill:#ff6b6b,stroke:#d63031,color:#fff
classDef branchStyle fill:#6c5ce7,stroke:#5f3dc4,color:#fff
class strapi portalStyle
class appclone,stagingclone gcpStyle
class staging,main,feature githubStyle
class preview,production,stagingsite netlifyStyle
class editor,dev userStyle
.
├── .github/
│ ├── workflows/
│ └── copilot-instructions.md
├── cms/ # Strapi backend
│ ├── config/ # Strapi configuration files
│ │ ├── admin.ts
│ │ ├── database.ts
│ │ ├── middlewares.ts
│ │ ├── plugins.ts
│ │ └── server.ts
│ ├── database/ # Database files
│ │ └── migrations/
│ ├── scripts/ # e.g., sync:mdx, sync-navigation
│ ├── src/ # Astro frontend application
│ │ ├── admin/ # Admin UI customizations
│ │ ├── api/
│ │ │ ├──/{content-type} # e.g., blog-post, foundation-page
│ │ │ ├── content-types/
│ │ │ │ ├── schema.json
│ │ │ │ └── lifecycles.ts # MDX generation logic
│ │ │ ├── controllers/
│ │ │ ├── routes/
│ │ │ └── services/
│ │ │ └── utils.ts
│ │ ├── components/ # Reusable Strapi components
│ │ │ ├── blocks/ # Content block components
│ │ │ ├── navigation/
│ │ │ └── shared/ # Shared components
│ │ ├── serializers/ # MDX serialization logic
│ │ │ └── blocks/
│ │ ├── utils/
│ │ └── index.ts
│ └── types/ # TypeScript type definitions
│ │ └── generated/
│ ├── .env # Environment variables
│ ├── .gitignore
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── pnpm-workspace.yaml
│ ├── strapi-server.js
│ ├── tsconfig.json
│ ├── copy-schemas.js
│ └── README.md
├── public/ # Static assets (images, favicons, uploads)
│ └── uploads/ # User-uploaded media for Strapi local storage
├── src/ # Astro project
│ ├── components/ # Astro components
│ ├── config/ # JSON configs (navigation, etc.)
│ ├── content/ # Markdown/MDX content (blog, summit, docs)
│ │ ├── blog/
│ │ ├── developers/
│ │ ├── docs/
│ │ ├── foundation-pages/
│ │ └── summit/
│ ├── layouts/
│ ├── pages/ # Route pages
│ │ ├── blog/
│ │ ├── developers/
│ │ ├── summit/
│ │ ├── [...page].astro
│ │ └── index.astro
│ ├── schemas/
│ ├── styles/ # Global styles
│ ├── utils/ # Utility functions
│ ├── content.config.ts # Astro content collections config
│ ├── env.d.ts
│ └── middleware.ts
├── .env # Environment variables
├── .env.example
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── astro.config.mjs
├── eslint.config.js
├── netlify.toml
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── tailwind.config.mjs
└── tsconfig.json
- Clone the repository:
git clone https://github.com/interledger/interledger.org-v5.git- Install dependencies:
pnpm installNote on lockfiles: This repo has two
pnpm-lock.yamlfiles:
/pnpm-lock.yaml— root workspace lockfile, used locally and in CI/cms/pnpm-lock.yaml— standalone lockfile used by the GCP VM when deploying Strapi (cd cms && pnpm install)When
cms/package.jsonchanges (e.g. upgrading Strapi), regenerate both:pnpm install --no-frozen-lockfile # from repo root cd cms && pnpm install --no-frozen-lockfile # for GCP deployment
- Build and start the site:
# Build for production
pnpm run build
# Start dev server (localhost:1103)
pnpm run start- For Strapi Admin setup locally, refer to the /cms/README.md.
All commands are run from the root of the project, from a terminal:
| Command | Action |
|---|---|
pnpm install |
Installs dependencies |
pnpm run start |
Starts local dev server at localhost:1103 |
pnpm run build |
Build your production site to ./dist/ |
pnpm run preview |
Preview your build locally, before deploying |
pnpm run format |
Format code and fix linting issues |
pnpm run lint |
Check code formatting and linting |
pnpm run sync:sessionize -- <YEAR> |
Fetch Sessionize data (JSON + speaker images) for a given year |
This project uses ESLint for code linting and Prettier for code formatting. Before submitting a pull request, please ensure your code is properly formatted:
- Fix issues: Run
pnpm run formatto automatically format code and fix linting issues - Check before pushing: Run
pnpm run lintto verify everything passes (CI will also run this)
ESLint is configured to work with TypeScript and Astro files. The configuration extends recommended rules from ESLint, TypeScript ESLint, and Astro ESLint plugins, and integrates with Prettier to avoid conflicts.
GitHub Actions run automatically on pull requests and branch merges.
Workflows include:
- Linting (ESLint)
- Formatting validation (Prettier)
- Build validation
Pull requests must pass all checks before merging.
Astro is the source of truth for site content. Strapi lifecycles and synchronization scripts keep the CMS and Astro .mdx files in sync.
-
Strapi → Astro:
- Strapi lifecycle hooks trigger
.mdxfile creation, updates, and deletions. - Changes are automatically committed and pushed directly to the
stagingbranch, where Strapi acts as a contributor.
- Strapi lifecycle hooks trigger
-
Astro → Strapi:
- Merges in
stagingsync.mdxfiles back into the Strapi database. - Scripts like
sync:mdxhandle the synchronization.
- Merges in
- Editors can preview content from Strapi in real time before publishing.
- Page previews become available after saving content as a draft or after publishing.
- While the rest of the site is statically generated, preview pages use server-side rendering in Astro (
export const prerender = falseinpage-preview.astro). - Each content type is mapped to a corresponding preview route.
staging that can be accessed at: https://deploy-preview-{PR-number}--interledger-org-v5.netlify.app/. These previews are frontend-only and reflect the Astro build at that PR state.
For more information on Strapi lifecycles, synchronization scripts and preview functionality, see /cms/README.md.
-
main:- Serves the live production website.
- Merges to
maintrigger a Netlify rebuild of the production site.
-
staging:- Serves the live staging website (deployed via Netlify).
- Serves the Strapi Admin interface (running on the GCP VM).
- Any push to
stagingthat modifies files in/cmstriggers a rebuild of the Strapi Admin panel on the GCP VM. - Any push to
stagingthat modifies.mdor.mdxfiles insrc/content/foundation-pages,src/content/summit-pages,src/content/foundation-blog-posts, orsrc/content/ambassadorsalso triggerssync:mdx, including their localized mirrors undersrc/content/<locale>/....
- The Astro website (production and staging) is deployed and hosted via Netlify.
- Strapi (including the Admin panel) runs on a single Google Cloud VM.
- The Strapi instance on the VM tracks the
stagingbranch and pulls updates when/cmschanges are merged.
- Live website (built from
main): https://interledger-org-v5.netlify.app/ - Staging website (built from
staging): https://staging--interledger-org-v5.netlify.app/ - Strapi admin (controlled via
staging): https://strapi-admin.interledger.org/
Content can be added in two main ways:
- Editor workflow (via Strapi Admin)
- Developer workflow (via Astro
.mdxfiles)
The developer workflow also includes adding and maintaining documentation.
There are three contribution paths, depending on your role and the type of content.
- Editors create pages and blog posts via Strapi Admin.
- Each content type in Strapi has lifecycles configured to generate/update/delete
.mdxfiles in the Astro project automatically.- Example: Creating a foundation page writes MDX under
src/content/foundation-pages/using nested folders from the full path slug (see below): English uses the last segment as the filename; localized pages are written under the collection-level/{locale}/directory with the nested slug folders beneath it.
- Example: Creating a foundation page writes MDX under
- Content changes are automatically committed and pushed to the
stagingbranch by the GitHub AppInterledger Strapi.
staging branch on behalf of the editors.
All documentation for working with website content is available in the wiki. Please refer to the wiki for:
- Content creation and editing guidelines
- Adding blog posts and podcast episodes
- Managing multilingual content
- General site-building philosophy
Developers can add multiple types of content directly to the repository. Each content type has a specific folder and naming convention.
Astro automatically picks up these files, registers them in the appropriate content collection, and generates the correct routes using the associated templates.
This project has two related but separate pieces of configuration:
- Filesystem content paths: where MDX files live under
src/content/... - URL route bases: where those collections are exposed under
src/pages/...
These should not be treated as interchangeable.
Examples:
src/content/foundation-pagesmaps to site routes at/...src/content/foundation-blog-postsmaps to/blog/...src/content/developers-blog-postsmaps to/developers/blog/...src/content/summit-pagesmaps to/summit/...
The main source files for this setup are:
src/content.config.tsDefines Astro collection ids such as'foundation-pages','foundation-blog','developers-blog', and'summit-pages'.src/utils/paths.tsDefines filesystem paths and folder names used to load content from disk.src/utils/routes.tsDefinesROUTE_BASES, the URL base path for each content collection. Use this when building links, language-switcher URLs, or other route-aware behavior.src/utils/static-paths.tsBuilds localized static paths for collection-backed routes. EN is canonical; ES routes may render EN content when no ES translation exists.src/utils/i18.tsCentralizes locale definitions and language-switcher ordering.
Rule of thumb:
- If you are working with folders or files on disk, use
src/utils/paths.ts - If you are working with browser URLs or route generation, use
src/utils/routes.ts
When adding a new localized collection or changing route structure, review all of the files above together. They form the core configuration for how content is loaded and how URLs are generated.
Foundation Blog posts
- Location:
src/content/foundation-blog-posts - Localizations:
src/content/foundation-blog-posts/{locale} - Filename format:
YYYY-MM-DD-slug.mdx
Used for: Foundation news, updates, announcements, thought leadership.
Tech Blog posts
- Location:
src/content/developers-blog-posts - Localizations:
src/content/developers-blog-posts/{locale} - Filename format:
YYYY-MM-DD-slug.mdx
Used for: Technical deep dives, implementation updates, engineering insights.
Foundation Pages
- Location:
src/content/foundation-pages - Localizations:
src/content/foundation-pages/{locale}/{parent...}/(see path slug rules below) - Filename: last segment of the full path slug +
.mdx(nested segments become parent directories)
Used for: Static foundation pages such as About, Policy & Advocacy, Team, Grants, etc.
Summit Pages
- Location:
src/content/summit-pages - Localizations: same nesting pattern as foundation pages
- Filename: last segment of the full path slug +
.mdx
Used for: Summit landing pages, schedules, speaker lists, event resources.
In Strapi this is a single field (“Full Path Slug”): the full URL path of the page, without a leading slash. The same value is stored in MDX frontmatter as pathSlug. The live site URL is /{pathSlug} (normalized, no duplicate slashes).
Examples:
pathSlug (frontmatter / Strapi) |
Public URL |
|---|---|
about-us |
/about-us |
grant/grant-for-web |
/grant/grant-for-web |
On disk (English): split pathSlug on /; all segments except the last are folders; the last segment is the filename.
about-us→foundation-pages/about-us.mdxgrant/grant-for-web→foundation-pages/grant/grant-for-web.mdx
Localized pages live under one collection-level locale folder, with nested path segments after it (e.g. foundation-pages/es/grant/…mdx for Spanish).
Example (nested grant page):
---
pathSlug: 'grant/grant-for-web'
---→ public URL: /grant/grant-for-web
Key rules:
pathSlugis required on all foundation and summit pages (the build will fail without it).- Leading and trailing slashes on
pathSlugare stripped when parsing content. - There is no separate
pathfield in Strapi or frontmatter for these types; use one multi-segmentpathSlugfor nested URLs. - If
pathSlugis omitted from frontmatter (not allowed for a valid build), sync tooling may derive a default from the filename (without extension and without anyYYYY-MM-DD-date prefix); nested URLs should use explicit folders + filename that match the intendedpathSlug, or setpathSlugin frontmatter.
- Use correct frontmatter for each content type.
- Follow the required schema — invalid metadata will break the build.
- See:
src/schemas/content.tssrc/content.config.ts
to understand the required schema and validation rules for each content collection.
Each blog post includes frontmatter at the top of the file (title, description, date, authors, etc.), including a tags field used for filtering on the blog index.
Please only use the existing, approved tags unless you have aligned with the tech + comms team on adding a new one. This helps keep the tag filter focused and avoids fragmentation.
Current tags:
- Interledger Protocol
- Open Payments
- Web Monetization
- Rafiki
- Updates
- Releases
- Card Payments
If you believe your post needs a new tag, propose it in your PR description or in the #tech-team Slack channel so we can decide whether to add it and update this list.
Documentation pages are managed via Starlight.
Docs live in src/content/docs.
Starlight looks for .md or .mdx files in the src/content/docs/ directory. Each file is exposed as a route based on its file name.
Static assets, like favicons or images, can be placed in the public/ directory. When referencing these assets in your markdown, you do not have to include public/ in the file path, so an image would have a path like:
For more information about the way our documentation projects are set up, please refer to our documentation style guide.
- Add
.mdxcontent in Astro. - Open PRs against
staging. - Use frontmatter correctly — invalid metadata will break the build.
- Run
pnpm run buildandpnpm run formatbefore PR. - The PR must undergo review and pass all checks before it can be merged.
Consult Writing Guidelines for Developers below for more details on content structure, metadata, tags, and blog formatting.
Goal: Educate, drive adoption, and grow strategic influence.
Typical Target Audience:
- Technically-inclined users interested in Interledger development.
- Technically-inclined users interested in financial services technologies, innovations, or developments.
- Users keen on topics like APIs, data analytics, metrics, analysis, and quantitative assessment for digital networks.
- Users interested in privacy and related technologies.
Possible Content Framework:
If you're unsure how to structure your writing, you can use this as a guide.
- Introduction / main point
- Context - Interledger’s perspective / stance / commitment on the topic being written [broader categories like privacy, metrics for growth, Digital Financial Inclusion etc.]
- The Challenge (or) The Problem
- The Solution
- The How / implementation
- Roadmap - short-term / long-term
- Note: A call to action (CTA) will be included automatically at the bottom of every post.
Ideal Word Count: Between 1,000 and 2,500 words, with links to relevant documents/pages for a deeper understanding.
Discuss Ideas: Before starting, share your blog post ideas with the tech team to ensure alignment and awareness.
Copy the Template: Begin your draft using this Google Doc template to maintain a consistent format.
Review Process
Initial Reviews:
- Once your draft is ready, request specific reviewers or ask for feedback on the
#tech-teamSlack channel. - Incorporate feedback and refine the blog post.
Finalizing:
- When the draft is stable, create a pull request in the interledger.org GitHub repo against
staging. - Please add links where appropriate so people can easily click to learn more about the concepts you reference.
- Include all images used in the post in the PR.
- No-one is expected to know the ins and outs of Astro (the framework that powers our site), so please tag someone in the frontend team as a reviewer to ensure everything Astro-related is in order.
- The PR will be reviewed by the frontend team before being merged into
staging.
- If you need an illustration, submit a design request in advance to Madalina via the
#designSlack channel using the design request form. - Before uploading images to GitHub, run them through an image optimizer such as TinyPNG.
- Ensure images are appropriately sized; feel free to ask Madalina or Sarah for assistance.
- Note: Merging the pull request will not publish the blog post immediately. Changes from
stagingare merged intomaintwice a week. - Ensure the publishing date in the blog post frontmatter matches the intended release date.
- Check with Ioana to confirm the publishing date and keep a consistent posting schedule. Ioana will also handle social media promotion.
- Run
pnpm run buildlocally to verify that the page builds correctly. - Run
pnpm run formatandpnpm run lintto format your code and check for any issues before creating a pull request.
The Interledger Summit has taken place annually since 2022. Each edition has its own pages on the website — sessions(talks), speakers, and their individual detail pages — all scoped by year (e.g. /summit/2024/speakers, /summit/2024/talks).
All summit data originates from Sessionize. A sync script fetches that data and stores it locally in the project as JSON files. Utility functions then read those files to populate Astro components and generate all summit-related pages for every year automatically.
Run the following script to fetch summit data for a given year:
pnpm run sync:sessionize -- <YEAR>Example:
pnpm run sync:sessionize -- 2022
pnpm run sync:sessionize # defaults to currentSummitYearWhat is does:
- Defaults to
currentSummitYearif no year is provided - Downloads speaker and talk data into:
src/data/sessionize/{YEAR}-speakers.jsonsrc/data/sessionize/{YEAR}-talks.json
- Downloads speaker images into:
public/sessionize-speakers/img/{YEAR}
- Clears the image folder before downloading
- Validates the year against the allowed
YEARSlist
Once the JSON files are in place, two utility files handle all data access and page generation — no manual wiring is needed.
extractSessionize.ts
Responsible for:
- Reading local Sessionize JSON files
- Normalizing data into internal types (Talk, Speaker, etc.)
- Linking talks and speakers
- Handling translations
- Generating local image paths for speakers and adding a fallback image when missing
summit-talks-speakers.ts
Responsible for connecting processed data to Astro routing.
It generates:
- Paginated listing pages
- Talks →
/summit/{year}/talks - Speakers →
/summit/{year}/speakers
- Talks →
- Dynamic detail pages
- Talk pages →
/summit/{year}/talk/{talk-title} - Speaker pages →
/summit/{year}/speaker/{speaker-name}
- Talk pages →
All of these functions iterate over every year in the YEARS list automatically, so new summit data is picked up without any changes to page templates.
- Add a new entry to
sessionizeApiMapinsrc/utils/sessionize.ts, using the summit year as the key (e.g.'2026') and the corresponding Sessionize API URLs as values:
'2026': {
speakersUrl: 'https://sessionize.com/api/v2/.../view/Speakers',
talksUrl: 'https://sessionize.com/api/v2/.../view/Sessions'
}YEARSandcurrentSummitYearwill update automatically — no other changes needed.
- Run the script
pnpm run sync:sessionizeto fetch data and images for the new summit.
After syncing, it is recommended to check:
- That the fields in the new JSON files match those from previous years. In practice, they have always matched, but it is a good habit to verify.
- The hardcoded IDs used for translations (see Translations below), in case Sessionize has changed them.
From 2025 onwards, summit content includes Spanish translations. These are stored by Sessionize inside a questionAnswers array, present on both speaker and talk objects, using a structure like:
{
"id": 114105,
"answer": "Título en español"
}The following IDs are hardcoded in src/utils/extractSessionize.ts:
| Constant | ID | Used for |
|---|---|---|
| SPANISH_TITLE_ID | 114105 | Spanish title of a talk |
| SPANISH_DESC_ID | 114099 | Spanish description of a talk |
| SPANISH_BIO_ID | 114100 | Spanish bio of a speaker |
| TRANSLATION_ID | 107734 | Available translation languages for a talk |
When importing data for a new summit, verify that these IDs have not changed in the Sessionize export. If they have, update the constants in extractSessionize.ts accordingly.
To add a new language to the Sessionize data pipeline:
- Add the locale code to
SESSIONIZE_SUPPORTED_LOCALESinsrc/types/summit.ts:
export const SESSIONIZE_SUPPORTED_LOCALES = ['es', 'fr'] as const- Update the utility functions in
src/utils/extractSessionize.tsto extract the new language's fields fromquestionAnswers, following the same pattern used for Spanish. Each function should add a new key to the returned translations object (e.g.fr: { title, description }) alongside the existinges: {}entry. You will also need to add the corresponding Sessionize question IDs as constants (same asSPANISH_TITLE_ID,SPANISH_DESC_ID, etc.).
- Speaker images are downloaded locally during sync
- Stored under:
public/sessionize-speakers/img/{YEAR}/ - Filenames are generated using a slugified speaker name
- If no image is available, a fallback is used:
public/sessionize-speakers/img/no-photo.svg
Details on Strapi lifecycles, MDX syncing, and preview functionality, and how to set up a local Strapi instance are documented in /cms/README.md.
