Image & SEO

# image(opts) — responsive images

Emits responsive <img> or <picture> tags with srcset, sizes, lazy loading, and CLS-preventing width/height:

import { image } from "@elurjs/kit";
import { html } from "@elurjs/core";

export default function HeroPage() {
  return html`
    ${image({
      src: "/images/hero.jpg",
      alt: "Hero image",
      width: 1920,
      height: 1080,
      widths: [640, 1280, 1920],
      sizes: "100vw",
      priority: true,
    })}
  `;
}

# ImageOptions

Field Type Required Default Description
src string Yes — Source path relative to public dir
alt string Yes — Alt text (required for accessibility)
width number Yes — Intrinsic width in pixels
height number Yes — Intrinsic height in pixels
widths number[] No [width] Responsive widths for srcset
sizes string No — Sizes attribute, e.g. "100vw" or "(min-width: 768px) 50vw, 100vw"
priority boolean No false Eager load with fetchpriority="high" (above-the-fold)
class string No — CSS class
attributes Record<string, string> No {} Additional HTML attributes

# Build-time processing

When sharp is installed (optional peer dep), build() automatically:

  • Generates WebP and AVIF variants at requested widths
  • Content-based hashing for indefinite caching
  • Emits <picture> with <source> per format
  • Uses real dimensions from the manifest

Without sharp, emits a plain <img> with the original src.

# Image pipeline hardening

  • SHA-256 transform keys — variant hash incorporates source content digest + transform options + encoder versions
  • Path containment — sources and outputs validated against traversal, NUL, symlink escape
  • Atomic writes + single-flight — temp+rename with bounded concurrency pool
  • images.strict — fails build on missing source or failed transform

# Pipeline limitations

  • Local sources only — no remote image fetching (capabilities.remote = false).
  • No runtime image endpoint — variants are generated at build time, not on request (capabilities.runtimeEndpoint = false).
  • Resize only — no enlargement, crop, or rotation. (ImageRequest.fit exists in the type but is not implemented yet.)
  • Manual width/height required — intrinsic dimensions are not detected at render time.
  • processImages is the legacy pipeline (pipeline.ts: MD5 keys, non-atomic). processImageBatch / getImage (service.ts) is the hardened path the build uses.

# getImage(request, options) — programmatic

import { getImage } from "@elurjs/kit/image";

const meta = await getImage(
  {
    src: "/images/hero.jpg",
    alt: "Hero",
    widths: [640, 1280],
    formats: ["avif", "webp"],
    width: 1920,
    height: 1080,
    priority: true,
  },
  { publicDir: "public", outDir: "dist" },
);
// meta.src, meta.width, meta.height, meta.sources, meta.attributes, meta.generated

# ImageRequest

Field Type Description
src string Source path relative to public dir
alt string Alt text
widths number[]? Responsive widths
formats ImageFormat[]? Output formats
sizes string? Sizes attribute
width number? Intrinsic width
height number? Intrinsic height
priority boolean? Eager load with high priority
loading "lazy" | "eager"? Loading strategy
decoding "async" | "sync" | "auto"? Decoding hint
quality number? Quality (1-100)
fit string? Declared but not implemented — ignored today
class string? CSS class
attributes Record<string, unknown>? Additional HTML attributes

# ImageMetadata

Field Type Description
src string Original source path
width number? Real width from manifest
height number? Real height from manifest
sources Array<{ type, srcset }> <source> entries per format
attributes Record<string, string | number | boolean | undefined> HTML attributes for <img>
generated GeneratedImage[] Generated variant details

# ProcessOptions

Options for getImage and createImageService:

Field Type Default Description
publicDir string — Absolute path to public directory (required)
outDir string — Absolute path to output directory (required)
formats ImageFormat[]? ["webp", "avif"] Formats to generate
quality number? 80 Quality (1-100)
manifestPath string? — Path to write manifest JSON
strict boolean? false Fail build on missing sources or transform errors
concurrency number? 4 Max concurrent sharp transforms
base string? — URL base prefix applied to variant URLs

# createImageService(options)

import { createImageService } from "@elurjs/kit/image";

const service = createImageService({ publicDir: "public", outDir: "dist" });
// service.resolve(request, ctx) — resolve an image request
// service.capabilities — ImageServiceCapabilities

# processImages(images, options) — build-time

Processes a batch of images: for each image, generates variants at the specified widths and formats using sharp:

import { processImages } from "@elurjs/kit";

const results = await processImages(
  [
    { src: "/images/hero.jpg", widths: [640, 1280, 1920], formats: ["avif", "webp"] },
    { src: "/images/logo.png", widths: [320, 640], formats: ["webp"] },
  ],
  {
    publicDir: "public",
    outDir: "dist",
    quality: 80,
  }
);
// Returns ProcessedImage[] with src and generated variants

If sharp is not installed, returns an empty array and logs a warning.

# PipelineOptions

Field Type Default Description
publicDir string — Absolute path to public directory
outDir string — Absolute path to output directory
formats ImageFormat[]? ["webp", "avif"] Formats to generate
quality number? 80 Quality (1-100)

# isSharpAvailable()

import { isSharpAvailable } from "@elurjs/kit";

if (isSharpAvailable()) {
  // sharp is installed — build will generate variants
} else {
  // no sharp — images emit plain <img> with original src
}

# consumeImageRegistry()

Returns all images registered via image() during rendering, for build-time processing:

import { consumeImageRegistry } from "@elurjs/kit";

const images = consumeImageRegistry();
// [{ src: "/images/hero.jpg", widths: [640, 1280, 1920] }, ...]

# setImageManifest(manifest)

Loads the image manifest so image() emits <picture> with real variant URLs:

import { setImageManifest } from "@elurjs/kit";

const manifest = JSON.parse(await readFile("dist/_elur/image-manifest.json", "utf-8"));
setImageManifest(manifest);

# ImageManifest

interface ImageManifest {
  entries: Record<string, ImageEntry>;
}

# ImageEntry

Field Type Description
src string Original source path
width number Real width
height number Real height
variants ImageVariant[] Generated variants

# ImageVariant

Field Type Description
url string Variant URL (hashed)
width number Variant width
height number Variant height
format ImageFormat Variant format (webp, avif, etc.)
size number File size in bytes

# SEO

# generateSitemap(config)

Writes sitemap.xml to outDir and returns the file path:

import { generateSitemap } from "@elurjs/kit/seo";

const filePath = await generateSitemap({
  siteUrl: "https://example.com",
  outDir: "./dist",
  urls: [
    "/",
    "/blog",
    { url: "/blog/hello-world", lastmod: "2024-01-15", priority: 0.8 },
  ],
});

# SitemapConfig

Field Type Description
siteUrl string Base URL of the site
urls (SitemapEntry | string)[] URL entries (strings or objects)
outDir string Output directory where sitemap.xml is written

# SitemapEntry

Field Type Description
url string URL path
lastmod string? Last modification date (ISO 8601 or YYYY-MM-DD)
changefreq "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"? Change frequency
priority number? Priority (0.0–1.0)

# generateRobots(config)

Writes robots.txt to outDir and returns the file path:

import { generateRobots } from "@elurjs/kit/seo";

const filePath = await generateRobots({
  siteUrl: "https://example.com",
  outDir: "./dist",
  disallow: ["/api/", "/_elur/"],
});

# RobotsConfig

Field Type Description
siteUrl string Base URL of the site
outDir string Output directory where robots.txt is written
rules RobotsRule[]? Access rules per user agent (alternative to disallow)
disallow string[]? Paths to disallow for all crawlers (shorthand)
sitemapUrl string? Sitemap URL override (defaults to ${siteUrl}/sitemap.xml)

# RobotsRule

Field Type Description
userAgent string User agent (* for all)
allow string[]? Allowed paths
disallow string[]? Disallowed paths
crawlDelay number? Crawl delay in seconds

# jsonLd(schema) — structured data

import { jsonLd } from "@elurjs/kit/seo";

const script = jsonLd({
  "@context": "https://schema.org",
  "@type": "Article",
  headline: "Hello World",
  author: { "@type": "Person", name: "Ada" },
  datePublished: "2024-01-15",
});
// Returns <script type="application/ld+json">...</script>

Safe escaping: <, >, &, U+2028, U+2029 are escaped to prevent injection.

# generateSitemapFromRoutes(options) — automatic sitemap

When site is set in defineConfig, elur-kit build writes sitemap.xml from the scanned route manifest automatically — dynamic routes, error pages, and internal namespaces are excluded; sites over 50,000 URLs are split into sitemap-N.xml files with a sitemap.xml index. An existing sitemap.xml (from public/ or an integration hook) always takes precedence and is never overwritten.

To generate it manually from the scanned routes:

import { generateSitemapFromRoutes } from "@elurjs/kit/seo";

await generateSitemapFromRoutes({
  siteUrl: "https://example.com",
  outDir: "./dist",
  routes,                    // ScannedRoutes from scanRoutes()
  extraUrls: ["/blog/hello-world"], // concrete URLs for dynamic routes
  defaultChangefreq: "weekly",
  defaultPriority: 0.7,
});

Dynamic routes are excluded automatically — pass their concrete URLs via extraUrls (e.g. generated from your content collections).

# Using in build hook

Generate sitemap and robots during build — integration hooks are declared flat on the integration object:

// elur.config.ts
import { defineConfig } from "@elurjs/kit";
import { generateSitemap, generateRobots } from "@elurjs/kit/seo";

export default defineConfig({
  site: "https://example.com",
  integrations: [
    {
      name: "seo",
      build: async (result, ctx) => {
        await generateSitemap({
          siteUrl: "https://example.com",
          outDir: result.outDir,
          urls: ["/", "/blog"],
        });

        await generateRobots({
          siteUrl: "https://example.com",
          outDir: result.outDir,
          disallow: ["/api/"],
        });
      },
    },
  ],
});
💡 Tip

When site is configured you usually don't need this — the build already generates sitemap.xml from the route manifest. Use the hook for custom URLs or lastmod/priority metadata.