Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f9e0531
feat(roadmap): add roadmap page with json data loading logic with sim…
adrianboros Apr 16, 2026
ae54895
feat(roadmap): bun format fix
adrianboros Apr 16, 2026
3c84220
fix(roadmap-page): add env var
adrianboros Apr 17, 2026
40744a8
feat(roadmap): update timeline to group projects by teams, and color …
adrianboros Apr 20, 2026
6f039c3
fix(roadmap): lint fix
adrianboros Apr 20, 2026
a6c6466
fix(roadmap): have teams name stay visible on the screen
adrianboros Apr 20, 2026
f4220eb
chore(deps): add netlify adapter, blobs, functions, and linear sdk
adrianboros Apr 20, 2026
44feb04
build(astro): switch to hybrid SSR mode with Netlify adapter
adrianboros Apr 20, 2026
f5fc50c
feat(linear): add Linear client and snapshot builder
adrianboros Apr 20, 2026
e020d0d
feat(functions): add scheduled Linear sync function (every 12h)
adrianboros Apr 20, 2026
ead28ca
feat(functions): add manual sync endpoint (POST /api/sync)
adrianboros Apr 20, 2026
60321f3
feat(roadmap): switch roadmap page to SSR, read from Netlify Blobs
adrianboros Apr 20, 2026
a47915b
chore(config): remove ROADMAP_API_URL, add Linear and Netlify env vars
adrianboros Apr 20, 2026
d3a284b
chore(deps): downgrade astro netlify adaptor to work with astro v5
adrianboros May 7, 2026
c9e7280
chore(deps): add deno.lock and .netlify folder
adrianboros May 7, 2026
a30cf31
build(astro): "hybrid" was removed in Astro 5.x and needs to be chang…
adrianboros May 7, 2026
087d867
Merge branch 'main' into feat/roadmap-page
adrianboros May 7, 2026
ddd5462
chore(deps): fix packages after merge from main
adrianboros May 7, 2026
502b774
chore(deps): lint issues
adrianboros May 7, 2026
161a3a1
fix(roadmap-page): lighten up the teams colors
adrianboros May 7, 2026
62178ec
fix(roadmap-page): mobile view with tap on milestones to view titles
adrianboros May 7, 2026
90b76fb
fix(linear): fix type error on build linear data snapshot
adrianboros May 7, 2026
0d180ea
fix(roadmap-page): remove logs, lint fixes and small styling update
adrianboros May 8, 2026
5c70d06
fix(roadmap-page): remove project links completely from the page
adrianboros May 8, 2026
5c75c3d
chore(deps): update netlify.toml to add the roadmap redirects before …
adrianboros May 8, 2026
2843382
chore(deps): netlify.toml redirect cleanup
adrianboros May 8, 2026
f5deb0f
chore(deps): netlify.toml redirect cleanup wip and bun format
adrianboros May 8, 2026
16fe966
chore(deps): fix lint issues
adrianboros May 8, 2026
426a64f
Merge branch 'main' into feat/roadmap-page
adrianboros May 8, 2026
3b73457
chore(deps): netlify redirect issue
adrianboros May 8, 2026
d2fb0db
chore(deps): netlify redirect issue
adrianboros May 8, 2026
0b243eb
chore(deps): netlify redirect issue, revert
adrianboros May 8, 2026
0384ea6
fix(roadmap-page): fix board over header menu issue
adrianboros May 8, 2026
0615999
fix(roadmap-page): refactor to have sticky board header
adrianboros May 8, 2026
d62f276
fix(linear-snapshot): filter out completed and cancelled projects
adrianboros May 8, 2026
194d9fd
fix(roadmap-page): team/projects column updates
adrianboros May 8, 2026
10bda5e
fix(roadmap-page): team/projects column updates
adrianboros May 8, 2026
256b57e
fix(linear): exclude projects based on non-public label
adrianboros May 8, 2026
787d6c8
chore(deps): netlify redirect issue
adrianboros May 19, 2026
092c6e4
fix(roadmap-page): date fallback fix
adrianboros May 19, 2026
ec84146
chore(deps): env params refactoring
adrianboros May 19, 2026
4d05eb3
fix(roadmap-page): header horizontal scrooling bidirectional sync wit…
adrianboros May 19, 2026
2adaa9e
fix(roadmap-page): set default team and project colors constants
adrianboros May 19, 2026
4202ea4
fix(roadmap-page): refactor roadmap board component break out utility…
adrianboros May 19, 2026
e38e3ed
fix(roadmap-page): add utility attributes for screen readers
adrianboros May 19, 2026
5f2514e
chore(deps): build fix
adrianboros May 19, 2026
9086f9c
chore(deps): lint fix
adrianboros May 19, 2026
56a2c34
fix(linear-snapshot): remove unused lastErr
adrianboros May 19, 2026
e32ee21
fix(linear-snapshot): remove initial/bad archived project filter
adrianboros May 19, 2026
a2fa793
fix(linear): requirement update - include only projects that have pub…
adrianboros May 19, 2026
c978210
chore(deps): lint fix
adrianboros May 19, 2026
e684349
chore(netlify): dev env settings fixes
adrianboros May 19, 2026
f3d3769
fix(linear): refactor type definitions merge Roadmap and Board types
adrianboros May 19, 2026
8a6b06d
chore(deps): lint fix
adrianboros May 19, 2026
c89fe2a
fix(linear): add generatedAt at the bottom of the page and remove las…
adrianboros May 19, 2026
031261c
fix(netliffy): extract purge cache utility function
adrianboros May 20, 2026
9dd3a1b
fix(netlify): add rate limiting on the endpoint and timing attacks im…
adrianboros May 20, 2026
a14d9d9
fix(roadmap-page): add a console error in case roadmap snapshot it no…
adrianboros May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Variables marked "required" will cause a build/runtime error if missing in production.
# To build locally without these keys, run: bun run astro build -- --mode development

# -----------------------------------------------------------------------------
# Linear (required)
# -----------------------------------------------------------------------------

# Linear API key — used by sync functions to fetch roadmap data from Linear
# Get from: Linear → Settings → API → Personal API keys
LINEAR_API_KEY=lin_api_...

# Linear custom view ID containing the roadmap projects (required)
LINEAR_CUSTOM_VIEW_ID=27df73bc-50ec-4fc1-bbb2-d906236a5bbc

# -----------------------------------------------------------------------------
# Sync API (required)
# -----------------------------------------------------------------------------

# Bearer token for POST /api/sync (manual sync trigger) — set to any strong secret
API_SECRET=

# -----------------------------------------------------------------------------
# Netlify (optional — used for CDN cache purging after sync)
# -----------------------------------------------------------------------------

# Netlify Personal Access Token
# Get from: Netlify UI → User Settings → Applications → Personal access tokens
NETLIFY_API_TOKEN=

# Netlify Site ID — auto-injected by Netlify in production and by `netlify dev` after `netlify link`
# Set manually only if running sync functions outside of the Netlify CLI
# NETLIFY_SITE_ID=
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ pnpm-debug.log*
# lock files
bun.lock
bun.lockb
deno.lock
pnpm-lock.yaml
package-lock.json

# webstorm
.idea/

# Local Netlify folder
.netlify
3 changes: 3 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import starlightLinksValidator from 'starlight-links-validator'
import starlightFullViewMode from 'starlight-fullview-mode'

import mdx from '@astrojs/mdx'
import netlify from '@astrojs/netlify'
import { PUBLISHED_RFC_SIDEBAR_ITEMS } from './src/data/rfcs.ts'

// https://astro.build/config
export default defineConfig({
output: 'static',
Comment thread
adrianboros marked this conversation as resolved.
adapter: netlify(),
site: 'https://interledger.org',
base: '/developers',
integrations: [
Expand Down
1,235 changes: 1,225 additions & 10 deletions bun.lock

Large diffs are not rendered by default.

22 changes: 19 additions & 3 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
[dev]
targetPort = 1103
command = "bun run start"
[build]
command = "npm install -g bun && bun install && bun run build && mkdir -p _netlify/builders && cp -r dist _netlify/builders/developers"
command = "npm install -g bun && bun install && bun run build && bun scripts/sync-blob.ts && mkdir -p _netlify/builders && cp -r dist _netlify/builders/developers"
publish = "_netlify/builders"
environment = { NETLIFY = "true" }

[context.production]
command = "npm install -g bun && bun install && bun run build && mkdir -p _netlify/builders && cp -r dist _netlify/builders/developers"
command = "npm install -g bun && bun install && bun run build && bun scripts/sync-blob.ts && mkdir -p _netlify/builders && cp -r dist _netlify/builders/developers"
publish = "_netlify/builders"
environment = { NETLIFY = "true" }
environment = { NETLIFY = "true" }

[context.preview]
command = "npm install -g bun && bun install && bun run build && mkdir -p _netlify/builders && cp -r dist _netlify/builders/developers"
Expand All @@ -18,6 +21,19 @@
publish = "_netlify/builders"
environment = { NETLIFY = "true" }

# Route /api/sync to the sync-now function (dev only)
[[context.dev.redirects]]
from = "/api/sync"
to = "/.netlify/functions/sync-now"
status = 200
force = true

# Route roadmap to Netlify SSR function (reads from blob store on-demand)
[[redirects]]
from = "/developers/roadmap"
to = "/.netlify/functions/ssr"
status = 200

# Rewrite everything to /developers/index.html for client-side routing
[[redirects]]
from = "/developers/*"
Expand Down
76 changes: 76 additions & 0 deletions netlify/functions/sync-now.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { timingSafeEqual } from 'crypto'
import { getStore } from '@netlify/blobs'
import { buildSnapshot } from '../../src/linear/build-snapshot.js'
import type { Context } from '@netlify/functions'
import { API_SECRET } from '../../src/config.js'
import { purgeRoadmapCache } from './utils/purge-roadmap-cache.mts'

const FIVE_MINUTES_MS = 5 * 60 * 1000
const RL_KEY = 'sync-rate-limit'

export default async function handler(
req: Request,
_ctx: Context
): Promise<Response> {
const authHeader = req.headers.get('authorization') ?? ''
const token = authHeader.replace(/^Bearer\s+/i, '')

if (!API_SECRET) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}
Comment thread
adrianboros marked this conversation as resolved.

const secretBuf = Buffer.from(API_SECRET)
const tokenBuf = Buffer.from(token)
const authorized =
tokenBuf.length === secretBuf.length && timingSafeEqual(tokenBuf, secretBuf)

if (!authorized) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}

const store = getStore('roadmap')

const lastSync = (await store.get(RL_KEY, { type: 'json' })) as {
ts: number
} | null
if (lastSync && Date.now() - lastSync.ts < FIVE_MINUTES_MS) {
const retryAfter = Math.ceil(
(FIVE_MINUTES_MS - (Date.now() - lastSync.ts)) / 1000
)
return new Response(
JSON.stringify({ error: 'Too Many Requests', retryAfter }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': String(retryAfter)
}
}
)
}
await store.setJSON(RL_KEY, { ts: Date.now() })

const snapshot = await buildSnapshot()

await store.setJSON('roadmap-snapshot', snapshot)

await purgeRoadmapCache()

return new Response(
JSON.stringify({ ok: true, generatedAt: snapshot.generatedAt }),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
)
}

export const config = {
path: '/api/sync'
}
16 changes: 16 additions & 0 deletions netlify/functions/sync.mts
Comment thread
adrianboros marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getStore } from '@netlify/blobs'
import { buildSnapshot } from '../../src/linear/build-snapshot.js'
import { purgeRoadmapCache } from './utils/purge-roadmap-cache.mts'

export default async function handler() {
const snapshot = await buildSnapshot()

const store = getStore('roadmap')
await store.setJSON('roadmap-snapshot', snapshot)

await purgeRoadmapCache()
}

export const config = {
schedule: '0 */12 * * *'
}
17 changes: 17 additions & 0 deletions netlify/functions/utils/purge-roadmap-cache.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NETLIFY_SITE_ID, NETLIFY_API_TOKEN } from '../../../src/config.js'

export async function purgeRoadmapCache(): Promise<void> {
if (!NETLIFY_SITE_ID || !NETLIFY_API_TOKEN) return

await fetch('https://api.netlify.com/api/v1/purge', {
method: 'POST',
headers: {
Authorization: `Bearer ${NETLIFY_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
site_id: NETLIFY_SITE_ID,
paths: ['/developers/roadmap']
})
})
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
},
"dependencies": {
"@astrojs/mdx": "^5.0.3",
"@astrojs/netlify": "^7.0.7",
"@astrojs/node": "^10.0.5",
"@astrojs/starlight": "^0.38.3",
"@interledger/docs-design-system": "^0.12.0",
"@linear/sdk": "^82.0.0",
"@netlify/blobs": "^10.7.4",
"@netlify/functions": "^5.2.0",
"@types/showdown": "^2.0.6",
"astro": "^6.1.8",
"html-to-text": "^9.0.5",
Expand Down
17 changes: 17 additions & 0 deletions scripts/sync-blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { buildSnapshot } from '../src/linear/build-snapshot.ts'
import { getStore } from '@netlify/blobs'
import { NETLIFY_SITE_ID, NETLIFY_API_TOKEN } from '../src/config.js'

if (!NETLIFY_SITE_ID || !NETLIFY_API_TOKEN) {
/* eslint-disable-next-line no-console */
console.log('Skipping blob sync: missing env vars')
process.exit(0)
}

const snapshot = await buildSnapshot()
const store = getStore({
name: 'roadmap',
siteID: NETLIFY_SITE_ID,
token: NETLIFY_API_TOKEN
})
await store.setJSON('roadmap-snapshot', snapshot)
Loading
Loading