Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/weak-suns-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/koenig-lexical": minor
---

Added an embedPreviewUrl card config option that previews embed cards in a renderer served from a separate origin
9 changes: 5 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ compose.yml
.codex
.cursor

# NOTE: core/built/admin is intentionally NOT ignored — the production image's
# `full` stage COPYs it from the build context, where CI injects the admin build
# artifact. The `core` stage copies from the deploy stage (which never builds
# admin), so a stray local admin build cannot leak into the core image.
# NOTE: core/built/admin and core/built/embed-renderer are intentionally NOT
# ignored — the production image's `full` stage COPYs them from the build
# context, where CI injects the admin build artifact. The `core` stage copies
# from the deploy stage (which never builds either), so stray local builds cannot
# leak into the core image.

# Ignore local config files (.json and .jsonc)
ghost/core/config.local.json*
11 changes: 11 additions & 0 deletions .github/embed-renderer/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<title>Not found</title>
</head>
<body>
<p>Not found. This domain serves Ghost's embed preview renderer and nothing else.</p>
</body>
</html>
36 changes: 36 additions & 0 deletions .github/embed-renderer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Embed renderer hosting

The editor previews embed card html in
[`koenig/koenig-lexical/public/embed-renderer/`](../../koenig/koenig-lexical/public/embed-renderer/),
loaded from a domain that serves nothing else. Embed scripts run with that
domain's origin, so it must never share one with Ghost Admin, a Ghost site, or
anything holding cookies. Sites point at it with `security.embedPreviewUrl`.

Self-hosted Ghost previews embeds from `public.ghostembeds.com`, deployed from
this repository.

## Deploying

CI deploys on pushes to `main` that touch the renderer. By hand:

```bash
.github/embed-renderer/build.sh /tmp/embed-renderer
netlify deploy --prod --dir=/tmp/embed-renderer --no-build
```

The Netlify site has no linked repository and asset post-processing off, so the
renderer's inline script isn't rewritten. DNS: the host as a CNAME, an empty
apex, and no mail (no MX, SPF `-all`, DMARC `p=reject`).

## Adding a version

The renderer is versioned by its message protocol, not by Ghost release. Add
`v<N>.html` alongside the existing files and keep every older version: editors
request the version they were built against, so each deploy ships all of them.

## Rules for this domain

- serve nothing but the renderer files; every other path 404s
- never set cookies, serve Ghost content, or add branding
- never list it in `security.txt`, OAuth redirects, CORS allowlists or a CSP
`script-src`: it runs arbitrary third-party code by design
6 changes: 6 additions & 0 deletions .github/embed-renderer/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Content-Security-Policy: frame-ancestors https:
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cache-Control: public, max-age=3600
34 changes: 34 additions & 0 deletions .github/embed-renderer/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
#
# Assembles the upload directory for public.ghostembeds.com.
#
# ./build.sh [output-directory] (default: ./deploy)
#
# It holds every renderer version, the _headers next to this script, and a 404
# page. Upload the directory to Netlify, or deploy it with:
#
# netlify deploy --prod --dir=<output-directory> --no-build
#
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$here/../.." && pwd)"
renderer_dir="$repo_root/koenig/koenig-lexical/public/embed-renderer"
out_dir="${1:-$PWD/deploy}"

if [ ! -d "$renderer_dir" ]; then
echo "renderer source missing: $renderer_dir" >&2
exit 1
fi

rm -rf "$out_dir"
mkdir -p "$out_dir"

cp "$renderer_dir"/v*.html "$out_dir/"
cp "$here/404.html" "$out_dir/"
cp "$here/_headers" "$out_dir/"

echo "$out_dir"
for file in "$out_dir"/*; do
printf ' %s %s\n' "$(shasum -a 256 "$file" | cut -c1-16)" "$(basename "$file")"
done
11 changes: 10 additions & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@
allowedVersions: '<9',
},

// Admin's Cmd-K search must match Ember's results, and Ember ships
// FlexSearch 0.7; 0.8 changes how titles are tokenized and ranked.
{
description: 'Cap the catalog flexsearch at 0.7 (matches Ember admin search)',
matchDepTypes: ['pnpm.catalog'],
matchPackageNames: ['flexsearch'],
allowedVersions: '<0.8',
},

// Keep `@types/*` aligned with the runtime major they describe. Type defs
// for a different major than what actually runs are silently wrong at best
// (e.g. @types/express 5 vs Express 4) and build-breaking at worst
Expand All @@ -283,7 +292,7 @@
// Node.js runtime declared in `engines`, not a dependency — so a hard
// version cap is the only lever. Raise it when we bump the Node engine.
{
description: 'Cap @types/node at the default Node major (devEngines: 22.23.1)',
description: 'Cap @types/node at the default Node major (devEngines: 22.23.3)',
matchPackageNames: ['@types/node'],
allowedVersions: '<23',
},
Expand Down
28 changes: 15 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
env:
FORCE_COLOR: 1
HEAD_COMMIT: ${{ github.sha }}
NODE_VERSION: 22.23.1
NODE_VERSION: 22.23.3
# Disable v8-compile-cache to prevent intermittent V8 deserializer crashes
# when multiple parallel Nx workers race to read/write shared bytecode cache
# files. The cache lives in /tmp and is discarded after each run anyway,
Expand Down Expand Up @@ -228,7 +228,7 @@ jobs:
# test:unit's Nx cache is keyed on `node -v` (nx.json) so each leg runs.
id: node_matrix
run: |
echo 'matrix=["22.23.1", "24.20.0"]' >> $GITHUB_OUTPUT
echo "matrix=[\"${NODE_VERSION}\", \"24.20.0\"]" >> $GITHUB_OUTPUT

- name: Start Nx Cloud CI run
run: pnpm nx start-ci-run
Expand Down Expand Up @@ -1255,19 +1255,20 @@ jobs:
echo "::error::IS_SHIPPING is set but VITE_SENTRY_AUTH_TOKEN is empty — Koenig sourcemaps would not reach Sentry"
exit 1
fi
# Builds apps/admin/dist AND ghost/core/core/built/admin (asset-delivery).
# Builds apps/admin/dist, ghost/core/core/built/admin (asset-delivery),
# and ghost/core/core/built/embed-renderer (separate-origin previews).
pnpm nx run @tryghost/admin:build

# The built admin (ghost/core/core/built/admin) is consumed by both job_pack
# (packed into the Ghost-CLI archive) and job_docker (COPYed into the full
# image). Ship it as a tarball to preserve file modes and speed transfer.
# The built admin and embed renderer are consumed by both job_pack (packed
# into the Ghost-CLI archive) and job_docker (COPYed into the full image).
# Ship them as a tarball to preserve file modes and speed transfer.
#
# --exclude '*.map': admin sourcemaps (~60MB) are uploaded to Sentry during
# the build (IS_SHIPPING), not shipped in the image. ghost/core's `files`
# field strips them from the Ghost-CLI archive (!core/built/**/*.map), so
# excluding them here matches that for the Docker image too.
- name: Pack admin build
run: tar --exclude='*.map' -czf admin-build.tar.gz -C ghost/core/core/built admin
run: tar --exclude='*.map' -czf admin-build.tar.gz -C ghost/core/core/built admin embed-renderer

- name: Upload admin build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down Expand Up @@ -1346,7 +1347,8 @@ jobs:
name: admin-build

- name: Extract admin build
# The archive includes core/built/admin via ghost/core's files allowlist.
# The archive includes core/built/admin and core/built/embed-renderer via
# ghost/core's files allowlist.
run: |
mkdir -p ghost/core/core/built
tar -xzf admin-build.tar.gz -C ghost/core/core/built
Expand Down Expand Up @@ -1575,9 +1577,9 @@ jobs:
path: ${{ runner.temp }}/admin-artifact

- name: Extract admin build into context
# The full stage COPYs ghost/core/core/built/admin from the context; the
# deploy stage excludes core/built entirely, so admin never leaks into core
# and adding it is the only context change between the core and full builds.
# The full stage COPYs the admin and embed renderer from the context; the
# deploy stage excludes core/built entirely, so neither leaks into core and
# adding them is the only context change between the core and full builds.
run: |
mkdir -p ghost/core/core/built
tar -xzf "${RUNNER_TEMP}/admin-artifact/admin-build.tar.gz" -C ghost/core/core/built
Expand All @@ -1587,8 +1589,8 @@ jobs:
env:
BUILDKIT_PROGRESS: plain
with:
# Same repo-root context as core (admin now present at
# ghost/core/core/built/admin, excluded by the deploy stage) so the
# Same repo-root context as core (admin and embed renderer now present
# under ghost/core/core/built, excluded by the deploy stage) so the
# deploy/install/build layers cache-hit from the core build above.
context: .
file: Dockerfile.production
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-runner-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:
- 'package.json'

env:
NODE_VERSION: 22.23.1
NODE_VERSION: 22.23.3

permissions:
contents: read
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/embed-renderer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Embed renderer

# Deploys the embed renderer to public.ghostembeds.com, which serves
# self-hosted Ghost. See .github/embed-renderer/README.md.

on:
push:
branches: [main]
paths:
- 'koenig/koenig-lexical/public/embed-renderer/**'
- '.github/embed-renderer/**'
- '.github/workflows/embed-renderer.yml'
workflow_dispatch:

permissions:
contents: read

env:
NODE_VERSION: 22.23.3

concurrency:
group: embed-renderer
cancel-in-progress: false

jobs:
deploy:
name: Deploy public renderer
runs-on: ubuntu-latest
if: github.repository == 'TryGhost/Ghost'
steps:
- name: Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
install: 'false'
store-cache: 'false'

# every version ships on every deploy: a Netlify deploy replaces the site,
# and editors request the renderer version they were built against
- name: Build upload directory
run: .github/embed-renderer/build.sh "$RUNNER_TEMP/embed-renderer"

# --ignore-scripts: a static --no-build deploy needs none of netlify-cli's
# dependency build scripts, which pnpm otherwise refuses to skip.
# Runs outside the checkout: from the repo root netlify-cli sees the pnpm
# workspace and refuses to deploy until one of its packages is picked
- name: Deploy to Netlify
working-directory: ${{ runner.temp }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_EMBEDS_PUBLIC_SITE_ID }}
run: |
pnpm --ignore-scripts --package=netlify-cli@27.8.0 dlx netlify deploy \
--prod \
--no-build \
--dir="$RUNNER_TEMP/embed-renderer" \
--message "${GITHUB_SHA:0:7} via ${GITHUB_WORKFLOW}"
2 changes: 1 addition & 1 deletion .github/workflows/koenig-demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
cancel-in-progress: true

env:
NODE_VERSION: 22.23.1
NODE_VERSION: 22.23.3

jobs:
deploy:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ concurrency:
cancel-in-progress: false

env:
NODE_VERSION: 22.23.1
NODE_VERSION: 22.23.3

jobs:
publish:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ on:

env:
FORCE_COLOR: 1
NODE_VERSION: 22.23.1
NODE_VERSION: 22.23.3
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.23.1
22.23.3
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.23.1
22.23.3
5 changes: 5 additions & 0 deletions .pnpmfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ function readPackage(pkg) {
*/
async function updateConfig(config) {
const { packages, versioning = {} } = config;
// `pnpm dlx` runs this hook without the workspace package list
if (!packages) {
return config;
}

const ignoredPackages = new Set(versioning.ignore ?? []);

// step 1: enumerate all workspace packages with glob
Expand Down
Loading
Loading