Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1acb666
chore: rename repo
tefkah Oct 14, 2026
c652be5
feat(sveltia-gateway): single sign-on gateway for Sveltia CMS
tefkah Oct 14, 2026
0a4acff
feat(site): trialanderror.org, built and deployed from the monorepo
tefkah Oct 14, 2026
78e7e3e
style: format scripts/split.ts
tefkah Oct 14, 2026
d9b21fa
docs(site): note that a local deploy needs a reinstall afterwards
tefkah Oct 14, 2026
517109b
chore: point package repository fields at TrialAndErrorOrg/trialanderror
tefkah Oct 14, 2026
fa80676
feat(site): read content from site-content and secrets from 1Password
tefkah Oct 14, 2026
234f237
docs: link to TrialAndErrorOrg/trialanderror instead of parsers
tefkah Oct 14, 2026
58d7a18
fix(site): know the public address behind a proxy, rebuild without wr…
tefkah Oct 14, 2026
fafa6ba
chore(site): use the 1Password item names for the Strapi and Underlay…
tefkah Oct 14, 2026
2f64cf3
feat(site): the server reads its secrets from 1Password at start
tefkah Oct 14, 2026
c02426a
docs(site): turn the readme into a map, move instructions to docs/
tefkah Oct 14, 2026
2a76d87
docs(site): add the docs the readme points to
tefkah Oct 14, 2026
0724169
feat(site): sign-in button says Microsoft, no access token option
tefkah Oct 14, 2026
4673e65
fix(sveltia-gateway): tell Sveltia editors may push
tefkah Oct 14, 2026
1949c85
fix: add deploy url
tefkah Oct 14, 2026
0a9de57
build(site): slim the image from 565 to 297 MB
tefkah Oct 14, 2026
1bc38b2
feat(site): the blog comes from site-content, edited in /admin
tefkah Oct 14, 2026
a47f351
feat(site): read the Unsplash access key from 1Password
tefkah Oct 14, 2026
88083d2
fix(sveltia-gateway): allow the editorial workflow's createRef and la…
tefkah Oct 14, 2026
606ddd9
feat(site): live preview of blog posts in /admin
tefkah Oct 14, 2026
36ba8ec
fix(site): store /admin's dates in UTC and leave empty fields out
tefkah Oct 14, 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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "TrialAndErrorOrg/parsers" }],
"changelog": ["@changesets/changelog-github", { "repo": "TrialAndErrorOrg/trialanderror" }],
"commit": false,
"fixed": [],
"linked": [],
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ jobs:

- run: pnpm format:check

- run: pnpm turbo run build typecheck test --cache-dir=.turbo/cache
# The site is built and deployed by site.yml: its build reads Strapi, Underlay and
# site-content. Here it's only type-checked.
- run: pnpm turbo run build typecheck test --cache-dir=.turbo/cache --filter='!site'

- run: pnpm turbo run typecheck --cache-dir=.turbo/cache --filter=site

# Non-blocking reminder: list packages that changed without a changeset.
- name: Changeset status
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# -> build, pack and publish them via npm trusted publishing (OIDC)
#
# No npm token is used anywhere. Each package must have a trusted publisher configured on npmjs.com
# pointing at TrialAndErrorOrg/parsers + this file (release.yml). See docs/releasing.md.
# pointing at TrialAndErrorOrg/trialanderror + this file (release.yml). See docs/releasing.md.
#
# Layout follows https://changesets.dev/guide/automating (trusted publishing variant): only the
# `publish` job gets `id-token: write`.
Expand Down
142 changes: 142 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Builds the website (apps/site) with the content from TrialAndErrorOrg/site-content, publishes it
# as ghcr.io/trialanderrororg/site and asks Coolify to deploy it. Fast because it keeps every cache
# between runs: pnpm's store, turbo's cache (the libraries the site uses) and the site's own caches
# of resized images, share images and journal article HTML. The image only packages the built
# site, see apps/site/Dockerfile.
#
# Secret: OP_TOKEN, a 1Password service account token. varlock reads everything else from the
# Website vault (apps/site/.env.schema). See apps/site/docs/deploying.md.
name: site

on:
push:
branches: [main]
paths:
- apps/site/**
- libs/sveltia-gateway/**
- pnpm-lock.yaml
- .github/workflows/site.yml
# Sent by Strapi when something is published. site-content starts the workflow directly instead
repository_dispatch:
types: [content-updated]
# New journal articles reach Underlay without telling anyone
schedule:
- cron: '17 4 * * *'
workflow_dispatch:

permissions: {}

# Only the newest content matters: a save cancels the build of the one before
concurrency:
group: site
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/trialanderrororg/site
CONTENT_DIR: ${{ github.workspace }}/site-content
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

# What editors change in /admin, see apps/site/src/integrations/content.ts
- uses: actions/checkout@v7
with:
repository: TrialAndErrorOrg/site-content
path: site-content
persist-credentials: false

- uses: pnpm/action-setup@v6
with:
version: 11.3.0

- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Restore turbo cache
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-site-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-site-${{ runner.os }}-
turbo-${{ runner.os }}-main-

# Content-addressed, so restoring the newest copy is always safe; each run saves a fresh one
- name: Restore the site's image and article caches
uses: actions/cache@v6
with:
path: |
apps/site/node_modules/.astro
apps/site/node_modules/.cache
key: site-assets-${{ github.run_id }}
restore-keys: site-assets-

# The site's build script runs through varlock, which reads the Strapi and Underlay tokens
# from 1Password and hides them in the log
- name: Build
run: pnpm turbo run build --filter=site... --cache-dir=.turbo/cache
env:
OP_TOKEN: ${{ secrets.OP_TOKEN }}

# The built site plus only its production dependencies, with the musl builds of native
# packages for the Alpine image
- name: Package
run: pnpm --filter site deploy --prod --legacy --os=linux --cpu=x64 --libc=musl "$RUNNER_TEMP/site"

- name: Build the image
run: |
docker build "$RUNNER_TEMP/site" -f apps/site/Dockerfile \
--label org.opencontainers.image.source=https://github.com/${{ github.repository }} \
--label org.opencontainers.image.revision=${{ github.sha }} \
-t "$IMAGE:latest" -t "$IMAGE:${{ github.sha }}"

# Every route the server renders itself, so a runtime package missing from the image's
# dependencies fails here instead of on the live site
- name: Smoke test the image
run: |
docker run -d --name smoke -p 4321:4321 -e ONEPASSWORD=false -e ADMIN_AUTH=microsoft \
-e ADMIN_SESSION_SECRET="$(openssl rand -hex 32)" "$IMAGE:${{ github.sha }}"
trap 'docker logs smoke; docker rm -f smoke >/dev/null' EXIT
for _ in $(seq 1 30); do curl -sf -o /dev/null localhost:4321/ && break; sleep 1; done
expect() {
status=$(curl -s -o /dev/null -w '%{http_code}' "localhost:4321$2")
[ "$status" = "$1" ] || { echo "::error::$2 answered $status, expected $1"; exit 1; }
}
expect 200 /
expect 200 /admin/
expect 200 /admin/config.yml
expect 401 /admin/api/v3/user
expect 401 /preview/blog/x
expect 401 /preview/page/x
expect 401 '/api/draft?secret=x&slug=x'
expect 200 '/_image?href=%2Ffavicon-32x32.png&w=16&h=16&f=webp'

- name: Push the image
run: |
echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
docker push --all-tags "$IMAGE"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Skipped until COOLIFY_DEPLOY_URL is set in apps/site/.env.schema
- name: Deploy
working-directory: apps/site
run: |
# shellcheck disable=SC2016 # expanded by the inner shell, after varlock has filled them in
pnpm exec varlock run -- sh -c '
if [ -z "$COOLIFY_DEPLOY_URL" ]; then echo "No COOLIFY_DEPLOY_URL, not deploying"; exit 0; fi
curl --fail --silent --show-error -H "Authorization: Bearer $COOLIFY_TOKEN" "$COOLIFY_DEPLOY_URL"
'
env:
OP_TOKEN: ${{ secrets.OP_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,14 @@ thebiggestfile.ts

.env
.env.*
# Examples and varlock schemas hold no values, so they belong in the repository
!.env.example
!.env.schema
graph
# typedoc scratch output (scripts/readme.ts); hand-written docs/*.md are tracked
**/docs/*
!/docs/*.md
!/apps/site/docs/*.md

.sveltekit

Expand Down
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"sortPackageJson": false,
"ignorePatterns": [
"dist",
"apps/site",
"deprecated",
"coverage",
".next",
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Packages marked _private_ are not published.

| Package | Description |
|---|---|
| [`crossref-json`](libs/citations/crossref-json) | Defined in: [index.ts:38](https://github.com/TrialAndErrorOrg/parsers/blob/main/libs/citations/crossref-json/src/index.ts |
| [`crossref-json`](libs/citations/crossref-json) | Defined in: [index.ts:38](https://github.com/TrialAndErrorOrg/trialanderror/blob/main/libs/citations/crossref-json/src/index.ts |
| [`crossref-to-csl`](libs/citations/crossref-to-csl) | crossrefToCsl(item: CrossrefJSON): CSL; |
| [`csl-consolidate`](libs/citations/csl-consolidate) | Try to resolve a list of CSL data with crossref metadata |
| [`csl-to-biblatex`](libs/citations/csl-to-biblatex) | Somewhat jank CSL-JSON to biblatex converter |
Expand Down Expand Up @@ -192,13 +192,13 @@ Plugin tries to locate an "abstract" section in a `jast` tree and moves it to th
[unifiedgh]: https://github.com/unifiedjs/unified
[xast-from-xml]: https://github.com/syntax-tree/xast-util-from-xml
[rehype]: https://github.com/rehypejs/rehype
[rejour]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/rejour
[rejour-parse]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/rejour-parse
[rejour-stringify]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/rejour-stringify
[rejour-move-abstract]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/rejour-move-abstract
[rejour-meta]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/rejour-meta
[jast]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/jast
[jastscript]: https://github.com/TrialAndErrorOrg/parsers/tree/main/libs/jastscript
[rejour]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/rejour
[rejour-parse]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/rejour-parse
[rejour-stringify]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/rejour-stringify
[rejour-move-abstract]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/rejour-move-abstract
[rejour-meta]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/rejour-meta
[jast]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/jast
[jastscript]: https://github.com/TrialAndErrorOrg/trialanderror/tree/main/libs/jastscript
[hast]: https://github.com/syntax-tree/hast
[xast]: https://github.com/syntax-tree/xast
[mdast]: https://github.com/syntax-tree/mdast
87 changes: 87 additions & 0 deletions apps/site/.env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Environment for the website. Read by varlock (https://varlock.dev), which fills the secrets from
# 1Password. Nothing secret is stored here, only references to the Website vault.
#
# Locally, `pnpm dev` and `pnpm build` run through `varlock run`, and the 1Password desktop app
# asks once per session. Without access to the vault, put ONEPASSWORD=false and your own values
# in .env.local (gitignored), which wins over this file.
#
# In CI and where the site runs (Coolify), OP_TOKEN is the only secret: a 1Password service account
# token that can read the Website vault and nothing else. The image starts the server through
# `varlock run`, so it reads its own secrets at start. See docs/deploying.md.
# Astro checks every value again in astro.config.mjs (`env.schema`).
#
# @plugin(@varlock/1password-plugin)
# @initOp(token=$OP_TOKEN, allowAppAuth=not($VARLOCK_IS_CI), account=trialanderror)
# @defaultSensitive=true
# @defaultRequired=false
# ---

# 1Password service account token, for CI. Locally the desktop app is used instead.
# @type=opServiceAccountToken
OP_TOKEN=

# Read secrets from 1Password. Set to false in .env.local to use your own values instead.
# @sensitive=false @type=boolean
ONEPASSWORD=true

# Where site-content is checked out, relative to apps/site. Defaults to ../../../site-content, next
# to this repository (src/integrations/content.ts).
# @sensitive=false
CONTENT_DIR=

# --- Build: where blog posts, authors, team and journal articles come from ---

# @sensitive=false
STRAPI_URL=https://cms.trialanderror.org
# Read-only Strapi API token. Needed for drafts, optional for published content.
STRAPI_TOKEN=if($ONEPASSWORD, op("op://Website/Strapi TrialAndError CI Read Token/credential"))

# @sensitive=false
UNDERLAY_URL=https://www.underlay.org
# Without it Underlay reads are anonymous and rate limited to 60 requests a minute.
UNDERLAY_TOKEN=if($ONEPASSWORD, op("op://Website/Underlay/credential"))

# Where the blog comes from: content (site-content, edited in /admin), strapi, or underlay
# (UNDERLAY_BLOG_COLLECTION, filled by scripts/blog-to-underlay.ts).
# @sensitive=false @type=enum(content, strapi, underlay)
BLOG_SOURCE=content
# @sensitive=false
UNDERLAY_BLOG_COLLECTION=centeroftrialanderror/blog

# --- Runtime: what the server needs where the site runs ---

# Shared secret for Strapi draft previews. Must equal DRAFT_SECRET in Strapi.
DRAFT_SECRET=

# How editors sign in to /admin: github (their own GitHub account), microsoft (Entra, no GitHub
# account needed) or dev (a test editor, pnpm dev only). See docs/admin-sign-in.md. The sign-in
# secrets below are only read with microsoft.
# @sensitive=false @type=enum(github, microsoft, dev)
ADMIN_AUTH=github
# @sensitive=false @type=boolean
ADMIN_READ_ONLY=false

ADMIN_SESSION_SECRET=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/trialanderror admin Entra app/session secret"))
# @sensitive=false
ENTRA_TENANT_ID=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/trialanderror admin Entra app/tenant id"))
# @sensitive=false
ENTRA_CLIENT_ID=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/trialanderror admin Entra app/username"))
ENTRA_CLIENT_SECRET=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/trialanderror admin Entra app/credential"))
# @sensitive=false
GITHUB_APP_ID=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/Admin CMS Write Github App/username"))
GITHUB_APP_PRIVATE_KEY=if(and($ONEPASSWORD, eq($ADMIN_AUTH, microsoft)), op("op://Website/Admin CMS Write Github App/credential"))
# Local testing only: a token that stands in for the GitHub App, e.g. $(gh auth token)
GITHUB_CONTENT_TOKEN=

# Unsplash in /admin's image picker: the Access Key of the Unsplash app (unsplash.com/oauth/applications).
# It ends up in every editor's browser, as Unsplash intends; the Secret Key (credential) is never needed.
# @sensitive=false
UNSPLASH_ACCESS_KEY=if($ONEPASSWORD, op("op://Website/Unsplash app/username"))

# --- CI only: telling Coolify to deploy the new image ---

# The app's deploy webhook (Coolify, the app, Webhooks). Deploying is skipped while it's empty.
# @sensitive=false
COOLIFY_DEPLOY_URL=https://app.coolify.io/api/v1/deploy?uuid=avytsqsicyojwuklygs7jiyw&force=false
# A Coolify API token with deploy rights
COOLIFY_TOKEN=if(and($VARLOCK_IS_CI, not(isEmpty($COOLIFY_DEPLOY_URL))), op("op://Website/Coolify Website Deploy Token/credential"))
7 changes: 7 additions & 0 deletions apps/site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dist/
.astro/
node_modules/
.env
dist-*/
underlay-export/
.env.local
31 changes: 31 additions & 0 deletions apps/site/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Packaging only. CI (.github/workflows/site.yml) builds the site and runs
# `pnpm --filter site deploy --prod <dir>`, which leaves the built site with
# just its production dependencies. This image copies that directory in, so
# it builds in seconds and holds no source or build tools.
#
# Production dependencies are only what the server loads at runtime: Astro
# bundles itself into dist/server, so astro and everything used only while
# building are devDependencies in package.json. A package the server imports
# has to be a dependency, or the image fails CI's smoke test.
#
# The base is Alpine, so the packages with native code (sharp, varlock) need
# their musl builds, which pnpm only installs when asked:
#
# Locally, the same: pnpm turbo run build --filter=site...
# pnpm --filter site deploy --prod --legacy --os=linux --cpu=x64 --libc=musl /tmp/site
# docker build --platform linux/amd64 -f apps/site/Dockerfile /tmp/site
# then run `pnpm install` again: the deploy leaves the workspace's own install
# marked as production-only.
#
# The image holds no secrets. It starts the server through varlock, which reads
# them from 1Password (.env.schema) with the OP_TOKEN it is given.
FROM node:24-alpine
ENV NODE_ENV=production HOST=0.0.0.0 PORT=4321
WORKDIR /app
COPY --chown=node:node . .
USER node
EXPOSE 4321
# Node asks, so the check doesn't depend on what the base image ships
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
CMD ["node", "-e", "fetch('http://127.0.0.1:4321/').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
CMD ["node_modules/.bin/varlock", "run", "--", "node", "dist/server/entry.mjs"]
Loading
Loading