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
98 changes: 98 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Release

# Release channel is driven by the branch you merge into:
# main -> production (dist-tag: latest)
# dev -> beta (dist-tag: beta) version must be a -beta.x prerelease
# any -> next (dist-tag: next) whenever the version contains -next.x
# A merge only publishes when the version in package.json is NOT already on npm,
# so ordinary merges that don't bump the version are no-ops.
on:
push:
branches: [main, dev]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

# No secrets. Publishing uses npm Trusted Publishing (OIDC) — there is no
# NPM_TOKEN in this repo to steal. id-token mints a short-lived identity npm
# verifies against the trusted publisher configured on npmjs.com for this exact
# repo + workflow. contents:write is only for the tag + GitHub release notes.
permissions: {}

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

# Trusted Publishing needs npm >= 11.5.1; Node 22 ships an older npm.
- run: npm install -g npm@latest

- run: pnpm install --frozen-lockfile

# Decide the dist-tag from branch + version, or skip if this channel/version
# combination is not allowed. Enforces: production only from main, beta only
# from dev, next from anywhere.
- name: Resolve release channel
id: channel
working-directory: packages/sveltekit-og
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
BRANCH="${GITHUB_REF_NAME}"
echo "name=$NAME"; echo "version=$VERSION"; echo "branch=$BRANCH"

if [[ "$VERSION" == *-next.* ]]; then
TAG=next
elif [[ "$BRANCH" == "dev" && "$VERSION" == *-beta.* ]]; then
TAG=beta
elif [[ "$BRANCH" == "main" && "$VERSION" != *-* ]]; then
TAG=latest
else
echo "No release: version '$VERSION' is not valid for branch '$BRANCH'." >> "$GITHUB_STEP_SUMMARY"
echo "publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Skip if this exact version is already on npm (merge didn't bump it).
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "$NAME@$VERSION already published — skipping." >> "$GITHUB_STEP_SUMMARY"
echo "publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "publish=true" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# Build runs via the package's prepublishOnly. --provenance signs a build attestation.
- name: Publish to npm
if: steps.channel.outputs.publish == 'true'
working-directory: packages/sveltekit-og
run: |
echo "Publishing ${{ steps.channel.outputs.version }} with dist-tag: ${{ steps.channel.outputs.tag }}"
npm publish --provenance --tag "${{ steps.channel.outputs.tag }}"

# Tag the release commit and generate GitHub release notes from conventional commits.
- name: Tag + GitHub release notes
if: steps.channel.outputs.publish == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="v${{ steps.channel.outputs.version }}"
git tag "$TAG"
git push origin "$TAG"
npx changelogithub
73 changes: 71 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ If you have an idea for a new feature or an improvement to an existing one, plea

If you would like to contribute code to the project, please follow these steps:

1. Fork the repository and create a `new branch` from `main`.
1. Fork the repository and create a `new branch` from `dev`.
2. Make your changes and commit them with a clear and descriptive commit message.
3. Push your changes to your fork.
4. Open a pull request to the `main` branch of the SvelteKit OG repository.
4. Open a pull request to the `dev` branch of the SvelteKit OG repository. (`main` is the release branch — see [Releasing](#releasing).)

Please make sure your pull request includes:

Expand All @@ -55,6 +55,75 @@ npm run lint
npm run format
```

## Releasing

> For maintainers. Releases are automated by `.github/workflows/release.yml` and published to npm via **Trusted Publishing (OIDC)** — there is no `NPM_TOKEN` stored in the repo.

**The version string in `packages/sveltekit-og/package.json` decides the npm dist-tag; the branch decides whether that release is allowed:**

| Branch | Version | Published to |
| ------ | ----------------- | ---------------- |
| `main` | clean (`4.4.0`) | `latest` (prod) |
| `dev` | `-beta.x` | `beta` |
| any | `-next.x` | `next` |

Any other branch/version combination does **not** publish. A push/merge only publishes when the version is not already on npm — merges that don't bump the version are no-ops.

Bump the version with the interactive helper (it commits and pushes to the current branch; the CI creates the git tag):

```bash
pnpm release
```

### Beta

```bash
git checkout dev
pnpm release # pick a -beta version, e.g. 4.4.0-beta.0
```

Pushing to `dev` publishes `@beta`. Install with `npm i @ethercorps/sveltekit-og@beta`.

### Next (experimental preview)

```bash
pnpm release # pick a -next version, e.g. 4.4.0-next.0
```

Publishes `@next` from any branch. Use for throwaway previews you don't want on `@beta`.

### Production

Bump to a clean version on `dev` (this is a no-op publish), then merge `dev` → `main`:

```bash
git checkout dev
pnpm release # pick the clean release, e.g. 4.4.0
git push origin dev

git checkout main
git merge --ff-only dev # or merge the dev → main PR
git push origin main
```

Pushing the clean version to `main` publishes `@latest`.

> Do **not** bump the clean version directly on `main` during a beta cycle — bump on `dev` and merge, so `main` always matches what's on `@latest`.

### Hotfix

```bash
git checkout -b hotfix main
# fix, then:
pnpm release # e.g. 4.4.1
git checkout main && git merge --ff-only hotfix && git push origin main
git checkout dev && git merge main && git push origin dev # keep dev in sync
```

### One-time setup

Trusted Publishing must be configured once on npmjs.com → the package → **Settings → Trusted Publishing**: add a GitHub Actions publisher for repo `ethercorps/sveltekit-og`, workflow `release.yml`. Without it, publishing fails (by design — npm only trusts that exact repo + workflow).

## Code of Conduct

This project and everyone participating in it is governed by the [SvelteKit OG Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [ethercorps@gmail.com](mailto:ethercorps@gmail.com).
18 changes: 18 additions & 0 deletions apps/docs/src/content/runtime/deno.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,21 @@ NotFound: No such file or directory (os error 2): open '_app/immutable/assets/Je
```

</Collapsible>

<Collapsible title="Prerendered image routes return 404">

A prerendered image endpoint — a `+server.ts` with `export const prerender = true` that returns an image — is generated at build time and written into the deploy output as a static file (e.g. `static/png/prerendered.png`). On most hosts this just works, because they serve the static directory straight from the filesystem.

Deno Deploy is different: it serves static assets from an explicit route manifest, not by scanning the filesystem. The current Deno adapter only registers prerendered **pages** in that manifest — prerendered **endpoints** (image/asset routes) are written to disk but never added — so the file is orphaned and requesting the route (e.g. `/png/prerendered.png`) returns a **404**.

Until this is fixed upstream, either patch the adapter to also register `builder.prerendered.assets` in `staticFiles` (see [the patch used by this repo](https://github.com/etherCorps/sveltekit-og/blob/main/patches/%40deno__svelte-adapter%400.2.1.patch) via [`pnpm patch`](https://pnpm.io/cli/patch)), or **don't prerender image endpoints on Deno** — serve them dynamically by dropping `export const prerender = true`:

```typescript title="routes/png/og.png/+server.ts"
// export const prerender = true; // ← remove on Deno Deploy

export const GET = async () => new ImageResponse(template, { width: 1200, height: 630 });
```

Satori and Takumi both generate images fast enough at request time. To avoid regenerating on every hit, cache with a `Cache-Control` header on the response or Deno Deploy's ISR.

</Collapsible>
69 changes: 36 additions & 33 deletions apps/docs/src/content/usage/takumi.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Reach for Takumi when you want:

<Callout type="note" title="Available from v4.3.0">

Takumi support is available from `sveltekit-og@v4.3.0`. While v4.3 is in prerelease, install it from the `next` tag: `@ethercorps/sveltekit-og@next`.
Takumi support is available from `sveltekit-og@v4.3.0`. v4.3 is in beta and ready for test deployments — install it from the `beta` tag: `@ethercorps/sveltekit-og@beta`.

</Callout>

Expand All @@ -33,6 +33,12 @@ The Takumi engine lives behind a separate entry point and `takumi-js` is an **op
pnpm add @ethercorps/sveltekit-og takumi-js
```

<Callout type="note" title="Takumi v2">

`takumi-js` has graduated from beta — install the stable `v2` line (`^2.0.1`), which is what `pnpm add takumi-js` resolves to.

</Callout>

<Callout type="warning" title="Plugin still required">

Takumi loads its WebAssembly through the same build plugin as Satori. Make sure the Vite/Rollup plugin is configured as shown in [Getting Started](/docs/getting-started).
Expand Down Expand Up @@ -101,14 +107,14 @@ new ImageResponse(template, { format: 'jpeg', quality: 90 });
new ImageResponse(template, { format: 'svg' });
```

| Format | Content-Type |
| ------ | ------------ |
| `png` (default) | `image/png` |
| `jpeg` | `image/jpeg` |
| `webp` | `image/webp` |
| `ico` | `image/x-icon` |
| `svg` | `image/svg+xml` |
| `raw` | `application/octet-stream` |
| Format | Content-Type |
| --------------- | -------------------------- |
| `png` (default) | `image/png` |
| `jpeg` | `image/jpeg` |
| `webp` | `image/webp` |
| `ico` | `image/x-icon` |
| `svg` | `image/svg+xml` |
| `raw` | `application/octet-stream` |

## Fonts

Expand All @@ -126,17 +132,14 @@ import { read } from '$app/server';
import RegularFont from '$lib/assets/MyFont-Regular.ttf?url';

export const GET = async () => {
return new ImageResponse(
`<div style="display:flex; font-family: 'Inter'">Hello World</div>`,
{
width: 1200,
height: 630,
fonts: [
new GoogleFont('Inter', { weight: 400 }),
new CustomFont('My Font', () => read(RegularFont).arrayBuffer(), { weight: 700 })
]
}
);
return new ImageResponse(`<div style="display:flex; font-family: 'Inter'">Hello World</div>`, {
width: 1200,
height: 630,
fonts: [
new GoogleFont('Inter', { weight: 400 }),
new CustomFont('My Font', () => read(RegularFont).arrayBuffer(), { weight: 700 })
]
});
};
```

Expand Down Expand Up @@ -166,19 +169,19 @@ new ImageResponse(`<div style="display:flex">Ship it 🚀</div>`, { emoji: 'noto

Everything the constructor accepts, passed as the second argument.

| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| `width` | `number` | `1200` | Image width in pixels. |
| `height` | `number` | `630` | Image height in pixels. |
| `format` | `TakumiFormat` | `'png'` | Output format — see [Output Formats](#output-formats). |
| `quality` | `number` | — | `0`–`100`, applies to lossy `jpeg` / `webp`. |
| `fonts` | `TakumiFontInput[]` | built-in font | Fonts to register — see [Fonts](#fonts). |
| `stylesheets` | `string[]` | — | Extra CSS stylesheets applied before rendering. |
| `emoji` | `EmojiType \| 'from-font'` | `'twemoji'` | Emoji provider — see [Emoji](#emoji). |
| `debug` | `boolean` | `false` | Log render timing and diagnostics. |
| `status` | `number` | `200` | HTTP status code of the `Response`. |
| `statusText` | `string` | `'Success'` | HTTP status text of the `Response`. |
| `headers` | `Record<string, string>` | — | Extra response headers (merged with the `Content-Type`). |
| Option | Type | Default | Description |
| ------------- | -------------------------- | ------------- | -------------------------------------------------------- |
| `width` | `number` | `1200` | Image width in pixels. |
| `height` | `number` | `630` | Image height in pixels. |
| `format` | `TakumiFormat` | `'png'` | Output format — see [Output Formats](#output-formats). |
| `quality` | `number` | — | `0`–`100`, applies to lossy `jpeg` / `webp`. |
| `fonts` | `TakumiFontInput[]` | built-in font | Fonts to register — see [Fonts](#fonts). |
| `stylesheets` | `string[]` | — | Extra CSS stylesheets applied before rendering. |
| `emoji` | `EmojiType \| 'from-font'` | `'twemoji'` | Emoji provider — see [Emoji](#emoji). |
| `debug` | `boolean` | `false` | Log render timing and diagnostics. |
| `status` | `number` | `200` | HTTP status code of the `Response`. |
| `statusText` | `string` | `'Success'` | HTTP status text of the `Response`. |
| `headers` | `Record<string, string>` | — | Extra response headers (merged with the `Content-Type`). |

## Next Steps

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"name": "sveltekit-og-docs",
"pages_build_output_dir": ".svelte-kit/cloudflare",
"compatibility_date": "2025-11-22",
"compatibility_flags": ["nodejs_compat"]
"compatibility_flags": ["nodejs_compat"],
}
2 changes: 1 addition & 1 deletion examples/cf-pages-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"dependencies": {
"@ethercorps/sveltekit-og": "workspace:*",
"@examples/shared": "workspace:*",
"takumi-js": "2.0.0-beta.14"
"takumi-js": "2.0.1"
}
}
2 changes: 1 addition & 1 deletion examples/cf-workers-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"dependencies": {
"@ethercorps/sveltekit-og": "workspace:*",
"@examples/shared": "workspace:*",
"takumi-js": "2.0.0-beta.14"
"takumi-js": "2.0.1"
}
}
14 changes: 12 additions & 2 deletions examples/cf-workers-build/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
}
],
"workers_dev": false,
"compatibility_date": "2025-11-20",
"compatibility_date": "2026-07-01",
"build": {
"command": "pnpm run build"
},
Expand Down Expand Up @@ -44,5 +44,15 @@
"globs": ["**/*.ttf"],
"fallthrough": true
}
]
],
"env": {
"dev": {
"routes": [
{
"pattern": "dev-workers.sveltekit-og.dev",
"custom_domain": true
}
]
}
}
}
2 changes: 1 addition & 1 deletion examples/deno-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"dependencies": {
"@ethercorps/sveltekit-og": "workspace:*",
"@examples/shared": "workspace:*",
"takumi-js": "2.0.0-beta.14"
"takumi-js": "2.0.1"
}
}
2 changes: 1 addition & 1 deletion examples/netlify-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"dependencies": {
"@ethercorps/sveltekit-og": "workspace:*",
"@examples/shared": "workspace:*",
"takumi-js": "2.0.0-beta.14"
"takumi-js": "2.0.1"
}
}
2 changes: 1 addition & 1 deletion examples/node-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"dependencies": {
"@ethercorps/sveltekit-og": "workspace:*",
"@examples/shared": "workspace:*",
"takumi-js": "2.0.0-beta.14"
"takumi-js": "2.0.1"
}
}
Loading
Loading