Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4daec10
Fix: include hits in search JSON export and reset upload input on click
jamiefolsom May 18, 2026
ebf4351
Fix render-side crashes when rich-text blocks receive JSON data
jamiefolsom May 18, 2026
3c9b21c
Fix MDX attribute round-trip corruption in map embeds
jamiefolsom May 19, 2026
c7031fd
Require timeline.event_path for the <timeline> post embed
jamiefolsom May 19, 2026
3972cb5
Merge pull request #646 from performant-software/fix/timeline-upload-…
jamiefolsom May 19, 2026
2af5a22
Contain post-embed crashes so removing a map doesn't kill the editor
jamiefolsom May 20, 2026
95f8e90
Auto-recover the post preview after a contained embed crash
jamiefolsom May 20, 2026
4d54a43
Merge pull request #648 from performant-software/fix/post-map-removal…
jamiefolsom May 28, 2026
af01f26
Full view for paths (#677)
camdendotlol Jun 17, 2026
5cd0830
`gitignore` config file (#686)
camdendotlol Jun 17, 2026
8f72759
update map styles (#688)
camdendotlol Jun 18, 2026
37b8617
Add overlay option to Tina for paths (#687)
camdendotlol Jun 18, 2026
a45b251
Guard null place_geometry.properties in place map panel
jamiefolsom Jun 22, 2026
fb44944
disable layer menu on paths (#694)
camdendotlol Jun 22, 2026
fbe59a9
Merge pull request #693 from performant-software/fix/place-certainty-…
jamiefolsom Jun 22, 2026
849cb5e
Copy branding to `/public` and add color options for maps (#695)
camdendotlol Jun 22, 2026
fdd3e20
Show all geometry on zoom paths (#696)
camdendotlol Jun 23, 2026
b6a50b4
`default_open` and initial values config for timeline (#701)
camdendotlol Jun 23, 2026
9dc3f5e
Add Dependabot config to batch security updates
jamiefolsom Jun 23, 2026
d37ff2d
Fix map search never fitting bounds to results or selection (#700)
jamiefolsom Jun 24, 2026
383a2f4
Bump protobufjs and posthog-js (#680)
dependabot[bot] Jun 24, 2026
eec646e
Add properties field to Tina (#705)
camdendotlol Jun 24, 2026
0a1e464
Bump vite from 6.4.1 to 6.4.3 (#681)
dependabot[bot] Jun 24, 2026
b232ec4
Bump uuid from 11.1.0 to 14.0.0 (#629)
dependabot[bot] Jun 24, 2026
4d0d083
Bump axios from 1.12.2 to 1.17.0 (#657)
dependabot[bot] Jun 24, 2026
29e9c57
Bump devalue from 5.6.2 to 5.8.1 (#641)
dependabot[bot] Jun 24, 2026
d3cc56f
Bump vitest from 4.0.16 to 4.1.0 (#653)
dependabot[bot] Jun 24, 2026
6bd3c65
Bump shell-quote from 1.8.3 to 1.8.4 (#675)
dependabot[bot] Jun 24, 2026
cd6cbc3
Bump form-data from 4.0.5 to 4.0.6 (#683)
dependabot[bot] Jun 24, 2026
e201bf2
upgrade react-components to fix georeference layer (#706)
camdendotlol Jun 24, 2026
a59a3b5
Merge pull request #702 from performant-software/chore/dependabot-config
jamiefolsom Jun 24, 2026
b50eddc
remove netlify-cli (#707)
camdendotlol Jun 25, 2026
26f71f2
Fix README drift: Node 24, Clerk SSO, drop removed users seeding (#697)
jamiefolsom Jun 25, 2026
8f920e1
Dependency upgrades and performance improvements (#721)
camdendotlol Jun 26, 2026
7d3d03b
Tina upgrades and caching (#723)
camdendotlol Jun 29, 2026
612a93b
More dependency upgrades (#725)
camdendotlol Jun 30, 2026
0068aa7
Tina/Clerk fixes (#727)
camdendotlol Jun 30, 2026
48a160a
Bump the npm-security group across 1 directory with 5 updates (#726)
dependabot[bot] Jun 30, 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
26 changes: 26 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Dependabot configuration.
#
# This repo previously ran with no config file, so it took GitHub's default
# behavior: one PR per security advisory, unbatched. That produced a large
# volume of overlapping PRs (e.g. several netlify-cli lockfile bumps that each
# carried one extra advisory and conflicted with each other).
#
# This config keeps the security safety net but consolidates it:
# - security updates are grouped into a single weekly PR per ecosystem
# - version-bump PRs are turned off (open-pull-requests-limit: 0) so we are
# not chasing non-security upgrades on Dependabot's schedule
#
# To also receive batched version-bump PRs later, raise the limit and add a
# second group with `applies-to: version-updates`.
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
groups:
npm-security:
applies-to: security-updates
patterns:
- "*"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ tmp/

# Development config
/public/config.dev*.json
/public/config.json
/public/branding.json

# Workspaces
*.code-workspace
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A map-centric website backed by a Core Data project and Typesense index.
## Getting Started

#### Requirements
- Node 20.x
- Node 24.x
- Netlify CLI
- Core Data Project
- Typesense Index
Expand All @@ -23,10 +23,6 @@ To start, run:
npm install && netlify dev
```

**Note:** Do not commit any project specific changes to `/public/config.json` in this repository.

**Note:** Changes to `config.json` will require a re-build of the site.

## Testing

#### Unit tests
Expand Down Expand Up @@ -83,10 +79,6 @@ content
│ ├── index.json
```

###### Users

Copy the `/data/users.json` file into your content repository to `/content/users/index.json`. This will seed TinaCMS with the initial set of user accounts, which can be used to setup accounts for other users, then removed. Skip this step if you are using an SSO provider or institutional IdP to manage users instead.

###### Branding

Create the following `/content/branding/branding.json` file:
Expand Down Expand Up @@ -181,7 +173,10 @@ Copy the `/public/config.json` file into your content repository to `/content/se
| search.table | Boolean | If `false`, will suppress the table view for search results |
| search.timeline | Object | Timeline configuration |
| search.timeline.date_range_facet | String | Path to the date range facet field in the Typesense document that will be used as the basis for the timeline. Required for the timeline to appear. |
| search.timeline.default_start | Number | The default start year for the timeline. If not provided, the timeline will start at the earliest date value in the records. |
| search.timeline.default_end | Number | The default end year for the timeline. If not provided, the timeline will end at the latest date value in the records. |
| search.timeline.event_path | String | Path to the event relation in the Typesense document. Required only if Event is not the primary model of this search index configuration. |
| search.timeline.default_open | Boolean | Whether to show the timeline on initial page load. Defaults to `false`. |
| search.type | String | If `map`, the map search component will be used. If `image`, `list`, or `grid`, the non-map search component will be used with the corresponding layout. Defaults to `map` when not present. |
| search.typesense | Object | Typesense index connection information |
| search.typesense.host | String | Typesense host URL |
Expand Down Expand Up @@ -236,9 +231,17 @@ After the user is created, use the "Security Credentials" tab to create an acces

Create a new site on Netlify deployed from the `core-data-places` repository. Set all of the environment variables in .env.example as appropriate. Currently, Core Data Places can only be hosted on Netlify in "server" mode, as the TinaCMS functions are dependent on Netlify functions.

#### Single Sign On
#### Single Sign On (Clerk)

As of v1.9.0, deployed sites authenticate the TinaCMS admin via **Clerk SSO**. The previous Keycloak and username/password (`tinacms-authjs`) paths have been removed — a non-local build now fails fast if the Clerk variables are missing. Set three environment variables on the Netlify site:

- `TINA_PUBLIC_CLERK_PUBLIC_KEY` — Clerk publishable key (`pk_live_…`)
- `TINA_PUBLIC_CLERK_ORG_ID` — the Clerk organization whose members may edit this site
- `CLERK_SECRET` — Clerk secret key (`sk_live_…`); used by the `tina` Netlify function for token verification and RBAC (mark it secret)

Editor access is membership in the org named by `TINA_PUBLIC_CLERK_ORG_ID`: role `org:admin` grants full access, `org:member` is restricted (per-collection rules are enforced in `netlify/functions/tina.ts`). Because Clerk's frontend API rejects bare `*.netlify.app` origins, the admin must be served from a domain the Clerk app trusts (typically a `*.performant.studio` custom domain).

See [Keycloak](docs/sso/keycloak-setup.md) documentation for single sign on.
For local development, set `TINA_PUBLIC_IS_LOCAL=true` to use the local auth provider instead — no Clerk required.

#### Static Build

Expand Down
3 changes: 2 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export default defineConfig({
defaultLocale,
locales,
routing: {
prefixDefaultLocale: true
prefixDefaultLocale: true,
redirectToDefaultLocale: true
}
},
output: STATIC_BUILD === 'true' ? 'static' : 'server',
Expand Down
3 changes: 1 addition & 2 deletions auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Keycloak from '@auth/core/providers/keycloak';
import { defineConfig } from 'auth-astro';
import dotenv from 'dotenv';

dotenv.config();
try { process.loadEnvFile() } catch {}

export default defineConfig({
providers: [
Expand Down
78 changes: 78 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 4 additions & 7 deletions netlify/edge-functions/rebuild.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Context } from '@netlify/edge-functions';
import { Clerk } from '@clerk/backend';
import { createClerkClient } from '@clerk/backend';

const BASE_URL = 'https://api.netlify.com/api/v1';
const NETLIFY_TOKEN = Netlify.env.get('NETLIFY_TOKEN')!;

const clerkClient = Clerk({
const clerkClient = createClerkClient({
secretKey: Netlify.env.get('CLERK_SECRET')!,
});

Expand All @@ -16,11 +16,8 @@ function buildResponse(statusCode: number, body: any | null): Response {
}

async function authenticate(req: Request): Promise<boolean> {
const token = req.headers.get('authorization');
const tokenWithoutBearer = token?.replace('Bearer ', '').trim();

const { toAuth } = await clerkClient.authenticateRequest({
headerToken: tokenWithoutBearer
const { toAuth } = await clerkClient.authenticateRequest(req, {
publishableKey: Netlify.env.get('TINA_PUBLIC_CLERK_PUBLIC_KEY')
});

const { sessionClaims } = toAuth();
Expand Down
66 changes: 45 additions & 21 deletions netlify/functions/tina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ import { databaseClient } from '@tina/databaseClient';
import { TinaNodeBackend, LocalBackendAuthProvider } from '@tinacms/datalayer';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import { createMediaHandler } from 'next-tinacms-s3/dist/handlers';
import ServerlessHttp from 'serverless-http';
import { Clerk } from '@clerk/backend';
import { createClerkClient } from '@clerk/backend'
import type { IncomingMessage, ServerResponse } from 'http';

dotenv.config();
try { process.loadEnvFile(); } catch {}

const app = express();

app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.use(cors());
app.use(express.json());
app.use(cookieParser());

const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === 'true';
Expand All @@ -27,6 +25,33 @@ if (!isLocal && !process.env.CLERK_SECRET) {
);
}

// convert an IncomingMessage to a Request to make Clerk happy
function toFetchRequest(req: IncomingMessage): Request {
const protocol = req.headers['x-forwarded-proto'] || 'https';
const host = req.headers.host || 'localhost';
const url = `${protocol}://${host}${req.url || '/'}`;

const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value != null) {
headers.set(key, Array.isArray(value) ? value.join(', ') : value);
}
}

return new Request(url, { method: req.method || 'GET', headers });
}

// next-tinacms-s3 expects a modified request object, but Express 5 officially removed
// the ability to mutate the request directly. this is a fairly ugly workaround.
function patchQuery(req: express.Request, overrides: Record<string, unknown>) {
const query = { ...req.query, ...overrides };
Object.defineProperty(req, 'query', {
value: query,
writable: true,
configurable: true,
});
}

const ClerkBackendAuthentication = ({
secretKey,
allowList,
Expand All @@ -38,17 +63,16 @@ const ClerkBackendAuthentication = ({
// Ensure the user is a member of the provided orgId
orgId?: string;
}) => {
const clerk = Clerk({
const clerk = createClerkClient({
secretKey,
});

return {
isAuthorized: async (req: IncomingMessage, _res: ServerResponse) => {
const token = req.headers['authorization'];
const tokenWithoutBearer = token?.replace('Bearer ', '').trim();
const requestState = await clerk.authenticateRequest({
headerToken: tokenWithoutBearer,
});
isAuthorized: async (req: { body: any} & IncomingMessage, _res: ServerResponse) => {
const requestState = await clerk.authenticateRequest(
toFetchRequest(req),
{ publishableKey: process.env.TINA_PUBLIC_CLERK_PUBLIC_KEY! }
);

if (requestState.status === 'signed-in') {
const user = await clerk.users.getUser(requestState.toAuth().userId);
Expand All @@ -60,7 +84,7 @@ const ClerkBackendAuthentication = ({
limit: 100 //come back to this when we have orgs with more than 100 members
})
);
const orgUser = membershipList?.find((mem) => (mem.publicUserData?.userId === user.id));
const orgUser = membershipList?.data?.find((mem) => (mem.publicUserData?.userId === user.id));
// if the user is not in the list, they are not authorized
if (!orgUser) {
return {
Expand Down Expand Up @@ -154,7 +178,7 @@ const ClerkBackendAuthentication = ({
const authProvider = isLocal
? LocalBackendAuthProvider()
: ClerkBackendAuthentication({
secretKey: process.env.CLERK_SECRET,
secretKey: process.env.CLERK_SECRET!,
orgId: process.env.TINA_PUBLIC_CLERK_ORG_ID
})

Expand All @@ -166,25 +190,25 @@ const tinaBackend = TinaNodeBackend({
const mediaHandler = createMediaHandler({
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},

region: process.env.S3_REGION,
},
bucket: process.env.S3_BUCKET,
bucket: process.env.S3_BUCKET!,
mediaRoot: process.env.S3_FOLDER,
authorized: async (req, _res) => {
const { isAuthorized } = await authProvider.isAuthorized(req, _res);
return isAuthorized;
}
});

app.post('/api/tina/*', async (req, res) => {
app.post('/api/tina/*splat', async (req, res) => {
tinaBackend(req, res);
});

app.get('/api/tina/*', async (req, res) => {
app.get('/api/tina/*splat', async (req, res) => {
tinaBackend(req, res);
});

Expand All @@ -194,17 +218,17 @@ app.get('/api/s3/media', mediaHandler);

// This route is necessary currently as a workaround for a bug in next-tinacms-s3
// where the folder is not prepended to the file name when uploading
app.get('/api/s3/media/*', (req, res, next) => {
app.get('/api/s3/media/*splat', (req, res, next) => {
if (req.query.key) {
req.query.key = process.env.S3_FOLDER + '/' + req.query.key;
patchQuery(req, { key: process.env.S3_FOLDER + '/' + req.query.key });
}
next();
}, mediaHandler);

app.post('/api/s3/media', mediaHandler);

app.delete('/api/s3/media/:media', (req, res) => {
req.query.media = ['media', req.params.media]
patchQuery(req, { media: ['media', req.params.media] });
return mediaHandler(req, res);
});

Expand Down
Loading