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
1 change: 1 addition & 0 deletions frontend/purestack.studio/guides/content/_nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"index.mdx",
"links.mdx",
"code-blocks.mdx",
"image-optimization.mdx",
"shared-content.mdx",
"localization.mdx"
]
Expand Down
286 changes: 286 additions & 0 deletions frontend/purestack.studio/guides/content/image-optimization.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
---
title: Image optimization
description: Enable responsive images with ts-images, choose widths and formats, prioritize hero images, and reuse optimized assets across builds.
template: doc
layout:
showNav: true
showToc: true
showFooter: true
nav:
icon: tabler:photo
---

# Image optimization

`@purestack/ts-images` turns local images into responsive AVIF and WebP sources with JPEG or PNG fallbacks during the build. Keep writing ordinary Markdown or HTML images. The plugin adds responsive markup, missing dimensions, and loading defaults without a special component or browser script.

Image optimization is optional and is not included in PureStack's default plugin set. Original files remain available for downloads and links, and source files are never edited.

## Enable the plugin

Install the package alongside PureStack. It requires Node.js 22.20 or newer and uses Sharp's platform-specific image codecs:

```sh
npm install @purestack/ts-images
```

Add the named plugin export to `content/siteConfig.json`, keeping any plugins already listed:

```json
{
"plugins": ["@purestack/ts-images#imagesPlugin"],
"extensions": {
"images": {
"widths": [480, 960, 1440],
"formats": ["avif", "webp"]
}
}
}
```

No TypeScript configuration is required. Listing the plugin enables its defaults; the `extensions.images` section is optional. Settings in that section belong to the image plugin and are applied only when it is installed in the plugin list.

Place images inside the content folder and reference them from your pages:

```text
content/
siteConfig.json
index.mdx
images/
dashboard.png
```

```md
![Dashboard showing project activity](./images/dashboard.png)
```

Run your usual [build or publish command](/guides/getting-started/purestack-cli/). The same optimization applies to images in rendered components, page templates, and shared headers and footers. Images inserted later by browser JavaScript are outside the build's rendered document.

## What visitors receive

For an eligible image, the plugin generates a `<picture>` with AVIF and WebP sources and an `<img>` fallback. Each format has a `srcset` of available widths. The browser chooses a supported format and a suitable candidate for the image's layout and the screen's pixel density.

The plugin preserves `alt`, classes, IDs, styles, links around images, and author-supplied loading attributes. It fills missing `width` and `height` attributes from the image's oriented aspect ratio so the browser can reserve space. If you supply one dimension, the other is derived; if you supply both, both are kept.

Generated images are written under `/_assets/images/`, with your site's `basePath` applied automatically. Filenames include a hash of the encoded bytes. Shared variants are reused across pages, and generated files are removed when no page references them. Original assets remain available at their usual URLs.

Widths are sorted, deduplicated, and capped at the source image's dimensions. A 700-pixel-wide source with widths `[480, 960, 1440]` produces 480- and 700-pixel candidates for each format. The plugin never enlarges a source image.

## Match responsive images to your layout

`widths` chooses the pixel widths to generate. `sizes` tells the browser how wide the image will appear in the layout. It does not set the image's CSS width.

The default `sizes` is `100vw`, suitable for an image that spans the viewport. For an image that spans the viewport only up to a 960-pixel limit, configure:

```json
{
"extensions": {
"images": {
"widths": [480, 960, 1440, 1920],
"sizes": "(max-width: 960px) 100vw, 960px"
}
}
}
```

Adjust `sizes` for your container widths, sidebars, and gutters. A browser may choose a larger candidate for a high-density screen, so a 960-pixel layout can benefit from a 1920-pixel source.

An individual image can supply its own `sizes`, overriding the configured default:

```html
<img
src="./images/dashboard.png"
alt="Dashboard showing project activity"
sizes="(max-width: 640px) 100vw, 480px"
style="width: 100%; max-width: 480px; height: auto"
>
```

Use HTML when you need per-image attributes. Markdown images use the global settings. More widths and formats generate more files and take more encoding time; choose candidates that serve your actual layouts.

## Use presets for common images

Presets group per-image choices. Select one with `data-image-preset`:

```html
<img src="./images/workshop.jpg" alt="The workshop" data-image-preset="hero">
<img src="./images/settings.png" alt="Editor settings" data-image-preset="screenshot">
<img src="./images/portrait.jpg" alt="A contributor" data-image-preset="thumbnail">
```

| Preset | Behavior |
| --- | --- |
| `content` | Uses the global image settings. |
| `hero` | Generates 960, 1440, and 1920 widths and supplies eager loading with high fetch priority. |
| `thumbnail` | Generates 160, 320, and 640 widths with `sizes="(max-width: 640px) 50vw, 320px"`. |
| `screenshot` | Uses lossless WebP with a PNG fallback and inherits the global widths. |

Preset widths are still capped at the source's dimensions. Presets inherit the resolved global settings before applying their own overrides. Override a built-in preset or add your own under `presets`:

```json
{
"extensions": {
"images": {
"presets": {
"card": {
"widths": [320, 640, 960],
"sizes": "(max-width: 640px) 100vw, 480px",
"quality": { "webp": 85 }
}
}
}
}
}
```

```html
<img src="./images/project.jpg" alt="Project overview" data-image-preset="card">
```

Presets can set `widths`, `formats`, `quality`, `sizes`, `loading`, `decoding`, `priority`, `lossless`, and `metadata`.

### Prioritize the main image

Images default to `loading="lazy"` and `decoding="async"`. For a prominent image visible when the page opens, use the `hero` preset or mark it directly:

```html
<img src="./images/cover.jpg" alt="The project workspace" data-image-priority>
```

Priority fills missing attributes with `loading="eager"` and `fetchpriority="high"`. Explicit `loading`, `decoding`, and `fetchpriority` attributes take precedence, so remove `loading="lazy"` if you want the priority default to apply. Mark the important image explicitly; the plugin does not infer which images are above the fold.

## Choose formats, quality, and metadata

The default formats are AVIF followed by WebP, with an automatic JPEG or PNG fallback. The order in `formats` determines source preference. PNG sources, transparent images, and lossless presets use a PNG fallback.

```json
{
"extensions": {
"images": {
"formats": ["avif", "webp"],
"quality": { "avif": 50, "webp": 80, "jpeg": 82 },
"metadata": "strip"
}
}
}
```

Quality values range from 1 to 100. Use `lossless: true` when preserving detail matters more than file size, as with screenshots and diagrams. The `screenshot` preset provides that choice per image.

Set `formats: []` to generate only responsive JPEG/PNG fallbacks without adding a `<picture>` wrapper. This can help when existing CSS depends on the image being a direct child of its parent.

Images are automatically oriented and converted to sRGB. `metadata: "strip"` omits embedded metadata by default. `"preserve"` retains metadata with orientation normalized. Optimization can produce files larger than the original, especially for small or lossless images; inspect the results for representative sources before choosing settings for the whole site.

## Control which images are processed

Use `include` and `exclude` to select source files. Patterns match content-root-relative paths with forward slashes, regardless of the page referencing the image:

```json
{
"extensions": {
"images": {
"include": ["images/**"],
"exclude": ["images/downloads/**"]
}
}
}
```

To keep one image's markup and source unchanged, opt out on that image:

```html
<img src="./images/diagram.png" alt="Source diagram" data-image-optimize="false">
```

To turn off all optimization while keeping the plugin declaration, set `extensions.images.enabled` to `false`. You can also remove the plugin from `plugins`.

| Input | Treatment |
| --- | --- |
| Local JPEG, PNG, WebP, AVIF, single-frame GIF, and TIFF references | Eligible for optimization. |
| External, data, and blob URLs | Left unchanged; the plugin does not download images. |
| SVG and animated images, including APNG | Left unchanged. |
| An image inside `<picture>`, or one with `srcset` already set | Existing responsive markup is preserved. |
| CSS backgrounds, video posters, social preview metadata, and URLs in browser scripts | Original assets remain available; these references are not rewritten. |

Because optimized images normally gain a `<picture>` parent, review direct-child selectors such as `.gallery > img`. Adjust the selector, use fallback-only output, or opt out where the original structure is required.

## Builds, development, and caching

Full builds and publishes process images referenced by rendered pages. During `serve`, images are processed when their pages render, and generated files exist before those pages are served. Editing a source image invalidates dependent pages. Local paths follow the same [content link and localization rules](./links) as other assets.

The persistent encoding cache survives cleaned output directories. Its keys include source bytes, pixel settings, and codec versions. Changes to `alt`, loading attributes, or `sizes` can reuse encoded files. The default cache lives in the OS temporary directory under `purestack-images`, separated by content root.

`cacheMaxAgeDays` defaults to 30. During use, cleanup removes entries unused for that many days, checking at most hourly per processor. Set it to `0` to disable cleanup. A custom `cacheDir` must stay outside the content and output directories; relative paths resolve from the build's `rootDir`, not the content folder. The default temporary cache needs no path configuration.

To bypass persistent cache reads and writes for a run:

```sh
yarn purestack publish --content ./content --no-cache
```

The same flag works with `build` and `serve`. Programmatic builds use `options.cache: false`. Existing cache files remain in place, and development still reuses rendered pages until they are invalidated. See the [CLI guide](/guides/getting-started/purestack-cli/) for cache and output-cleaning behavior.

Encoding concurrency defaults to two jobs per site, including output emission. Increase it only after measuring duration and memory on your build machine. Keeping fewer widths or formats also reduces work on a cache miss.

## Configure from TypeScript

For custom factory defaults or diagnostic callbacks, add the plugin to your existing `purestack.config.ts`:

```ts
import { imagesPlugin } from '@purestack/ts-images'
import { defineConfig } from 'purestack'

export default defineConfig({
plugins: [
imagesPlugin({
widths: [480, 960, 1440],
onDiagnostic(diagnostic) {
console.log(diagnostic)
},
}),
],
})
```

Factory settings supply defaults that `extensions.images` can override. An explicit TypeScript plugin instance replaces a JSON-selected instance with the same plugin name. Callbacks belong in TypeScript; JSON supplies settings only. The [Plugins guide](/guides/extending/plugins/) explains how plugin declarations compose.

Diagnostics report the referring page, source path, cache result, original bytes, total generated bytes, variant count, and duration. Without a callback, completed optimizations are logged as `image optimized`. Total generated bytes include every variant; they are not the bytes a visitor downloads for a single image.

## Settings reference

All settings below belong under `extensions.images`, or can be supplied as factory defaults to `imagesPlugin()`.

| Setting | Default | Purpose |
| --- | --- | --- |
| `enabled` | `true` | Enable optimization when the plugin is listed. |
| `widths` | `[480, 960, 1440]` | 1 to 16 widths, each an integer from 1 to 16384; no upscaling. |
| `formats` | `["avif", "webp"]` | Modern sources in preference order; `[]` generates only fallbacks. |
| `quality` | `{ "avif": 50, "webp": 80, "jpeg": 82 }` | Encoder quality from 1 to 100. |
| `sizes` | `"100vw"` | Default layout width hint; an image's own `sizes` takes precedence. |
| `loading` | `"lazy"` | `"lazy"`, `"eager"`, or `false` to omit the default attribute. |
| `decoding` | `"async"` | `"async"`, `"sync"`, `"auto"`, or `false`. |
| `priority` | `false` | Supply missing eager-loading and high-priority attributes. |
| `lossless` | `false` | Use lossless modern encoders and a PNG fallback. |
| `metadata` | `"strip"` | Strip metadata or use `"preserve"`. |
| `include` / `exclude` | `["**/*"]` / `[]` | Source selection using content-root-relative glob patterns. |
| `presets` | `content`, `hero`, `thumbnail`, `screenshot` | Override built-ins or define named per-image settings. |
| `cacheDir` | OS temporary cache per content root | Location outside content and output directories. |
| `cacheMaxAgeDays` | `30` | Retain entries used within this many days; `0` disables expiry. |
| `concurrency` | `2` | Per-site processing limit from 1 to 16. |
| `maxInputPixels` | `40000000` | Maximum source pixel count. |
| `maxSourceBytes` | `52428800` (50 MiB) | Maximum source file size. |
| `onError` | `"error"` | Fail on optimization errors, or use `"warn"` to retain the original image. |

## When something goes wrong

| Symptom | Check |
| --- | --- |
| Images still reference their originals | Confirm the plugin is listed, `enabled` is true, the file matches `include`/`exclude`, and the input is eligible. Existing `<picture>` and `srcset` are intentionally preserved. |
| A hero still loads lazily | Remove an explicit `loading="lazy"`; author-supplied attributes override priority defaults. |
| The browser selects an unexpectedly large candidate | Match `sizes` to the actual layout and account for screen pixel density. |
| A gallery's CSS stops matching | Optimization added a `<picture>` parent. Adjust direct-child selectors or opt out. |
| The build reports an unknown preset | Define the name under `presets` or choose one of the built-in presets. |
| A source exceeds the byte or pixel limit | Resize the source, exclude it, or deliberately adjust the corresponding limit. |
| Sharp cannot load on the build machine | Install dependencies for that machine's platform and confirm Node.js 22.20 or newer. |

`onError: "warn"` lets the plugin keep an original image after an optimization failure, but missing or invalid local URLs still follow PureStack's normal URL validation. Remote downloads, SVG optimization, animated transcoding, placeholders, and automatic cropping are outside this plugin's current scope.
5 changes: 3 additions & 2 deletions frontend/purestack.studio/guides/content/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Content
description: Write links and code examples, share content, and organize translations.
description: Write links and code examples, optimize images, share content, and organize translations.
template: doc
nav:
icon: tabler:files
Expand All @@ -10,9 +10,10 @@ layout:

# Content

Write links and code examples, share content, and organize translations.
Write links and code examples, optimize images, share content, and organize translations.

- [Links](./links)
- [Code blocks](./code-blocks)
- [Image optimization](./image-optimization)
- [Shared content](./shared-content)
- [Localization](./localization)
2 changes: 2 additions & 0 deletions frontend/purestack.studio/guides/content/links.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Images, videos, downloads, and other files in your content folder follow the sam

Write a file's full name, with its extension and the same capitals, so it works on any host.

For responsive image formats, sizing, and loading defaults, see [Image optimization](./image-optimization). The optional `ts-images` plugin handles ordinary Markdown and rendered HTML images while keeping originals available for downloads.

Files that a build step writes straight into the output aren't in your content folder, so PureStack can't check them. Link them from the site root, such as `/previews/demo.html`.

## Link outside your content
Expand Down
15 changes: 3 additions & 12 deletions frontend/purestack.studio/guides/extending/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ its defaults even without `extensions.images`. Set `extensions.images.enabled`
to `false` to disable optimization. An explicit plugin instance in
`purestack.config.ts` replaces a JSON-selected plugin with the same name.

The [Image optimization guide](/guides/content/image-optimization/) covers responsive sizing, presets, formats, caching, and per-image controls.

Both config formats use PureStack's own copy of a package when available so
shared registries stay together; other packages resolve from the site.
Plugin declarations execute installed JavaScript with the build process's
Expand Down Expand Up @@ -242,18 +244,7 @@ Builder style state is isolated across asynchronous operations, including concur

## Optimize images

Image optimization is optional. Install `@purestack/ts-images` alongside PureStack and add it to your existing plugin list:

```ts
import { defineConfig } from 'purestack'
import { imagesPlugin } from '@purestack/ts-images'

export default defineConfig({
plugins: [imagesPlugin({ widths: [480, 960, 1440] })],
})
```

Ordinary Markdown images and rendered HTML images receive responsive AVIF/WebP sources, JPEG/PNG fallbacks, dimensions, and loading defaults. Originals remain available for links and downloads. Remote images, SVGs, animations, existing `picture` markup, and images with `srcset` pass through. Use `data-image-preset="hero"` for eager priority images, `data-image-preset="screenshot"` for lossless output, or `data-image-optimize="false"` to keep an individual image unchanged. Set `sizes` for your actual image layout.
For installation and site configuration, follow the dedicated [Image optimization guide](/guides/content/image-optimization/). It covers JSON and TypeScript setup, responsive sizing, presets, caching, and per-image controls. The rest of this section explains the asset contribution for plugin authors.

The plugin owns its `extensions.images` settings, persistent cache, and encoder limits. It uses the generic asset contribution: `assets(config)` returns a per-site processor with a unique namespace and `processPage(input)`. This runs after all `onPageDocument` hooks, before URL rewriting. `resolveSource(element, url)` resolves local inputs and registers dependencies; `dependOn(relPath)` registers additional inputs; `emit(fileName, bytes)` atomically writes an owned file and returns its URL. Use content-addressed names and await all writes. Core handles base paths, incremental invalidation, shared references, manifest persistence, and cleanup when pages or processors disappear. The builder contains no image format or encoder selection.

Expand Down
2 changes: 1 addition & 1 deletion frontend/purestack.studio/guides/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Start with the CLI and site configuration, then choose a topic below. Each categ
</Panel>
<Panel tone="neutral" bodyClass="p-3">
<h2 class="mt-0">Content</h2>
<p>Write links and code examples, share content, and organize translations.</p>
<p>Write links and code examples, optimize images, share content, and organize translations.</p>
<BtnLink href="./content/" tone="accent" variant="link">Explore content</BtnLink>
</Panel>
<Panel tone="neutral" bodyClass="p-3">
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "purestack-repo",
"version": "1.1.16",
"version": "1.1.17",
"packageManager": "yarn@4.18.1",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purestack",
"version": "1.1.16",
"version": "1.1.17",
"packageManager": "yarn@4.9.2",
"module": "./dist/purestack.mjs",
"types": "./dist/purestack.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const version: string = '1.1.16'
export const version: string = '1.1.17'

export * from '@purestack/ts-ssg'
Loading
Loading