Deliver fast, responsive images with zero layout shift. This guide covers the SDK's image utilities, pre-computed variants, responsive <img> and <picture> patterns, custom Next.js loaders, blur placeholders, OG images, and focal-point cropping.

Prerequisites

You should have the Lynkow SDK installed and a client initialized. If not, see Guide 1: Quick Start.

TypeScript
// lib/lynkow.ts
import { createClient } from 'lynkow'

export const lynkow = createClient({
  siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!,
  fetchOptions: {
    next: { revalidate: 60 },
  },
})

1. How Lynkow serves images

All media uploaded to Lynkow is stored on Cloudflare R2 and served through Cloudflare's CDN. When you request a content item, the API returns:

  • featuredImage -- the original, untransformed URL

  • featuredImageVariants -- an object of pre-computed variant URLs, each optimized for a specific use case

The CDN automatically negotiates the best format (WebP or AVIF) based on the browser's Accept header, so you never need to manage format conversion yourself.

The SDK provides two additional methods on lynkow.media for on-the-fly transformations:

Method

Purpose

lynkow.media.srcset(url, options?)

Build a complete srcset string for responsive images

lynkow.media.transform(url, options?)

Build a single transformed URL with specific dimensions

Both methods are pure utilities -- they construct Cloudflare /cdn-cgi/image/ URLs without making any API calls. They work on server and browser, and handle null/undefined safely (returning an empty string).


2. Pre-computed ImageVariants

Every content item with a featuredImage returns a featuredImageVariants object. These are ready-to-use URLs optimized for common UI patterns:

TypeScript
interface ImageVariants {
  thumbnail?: string  // 400x300, fit: cover  -- grid thumbnails, admin previews
  card?: string       // 600x400, fit: cover  -- article cards, listing pages
  content?: string    // 1200w, fit: scale-down  -- inline content images
  medium?: string     // 960w, fit: scale-down  -- medium-width containers
  hero?: string       // 1920w, fit: scale-down  -- full-width hero banners
  og?: string         // 1200x630, fit: cover  -- Open Graph / social sharing
  avatar?: string     // 128x128, fit: cover  -- user avatars, author photos
  full?: string       // 2560w, fit: scale-down  -- lightbox, full-resolution
}

When to use variants vs. srcset/transform:

Use case

Approach

Article card at a fixed size

featuredImageVariants.card

Hero banner, single size

featuredImageVariants.hero

OG meta tag

featuredImageVariants.og or ogImageVariants.og

Responsive <img> with multiple breakpoints

lynkow.media.srcset()

Custom size not in the presets

lynkow.media.transform()

Next.js <Image> component

Custom loader with lynkow.media.transform()

Pre-computed variants are ideal when you need a single, known size. Use srcset when you need the browser to pick the best width at runtime.


3. Hero banners with featuredImageVariants.hero

The hero variant is 1920 pixels wide with scale-down fit, suitable for full-width banners:

tsx
// app/blog/[slug]/page.tsx
import { lynkow } from '@/lib/lynkow'

interface Props {
  params: Promise<{ slug: string }>
}

export default async function ArticlePage({ params }: Props) {
  const { slug } = await params
  const article = await lynkow.contents.getBySlug(slug)

  return (
    <article>
      {article.featuredImage && (
        <div className="relative w-full h-[60vh] overflow-hidden">
          <img
            src={article.featuredImageVariants?.hero || article.featuredImage}
            alt={article.title}
            className="w-full h-full object-cover"
            loading="eager"
            decoding="async"
            fetchPriority="high"
          />
        </div>
      )}
      <div className="max-w-3xl mx-auto px-4 py-12">
        <h1 className="text-4xl font-bold">{article.title}</h1>
        {/* ... article body */}
      </div>
    </article>
  )
}

Use loading="eager" and fetchPriority="high" for above-the-fold hero images. The browser should not lazy-load them.


4. Article cards with featuredImageVariants.card

The card variant is 600x400 with cover fit, cropped to a consistent aspect ratio:

tsx
// components/article-card.tsx
import Link from 'next/link'

interface ArticleCardProps {
  title: string
  slug: string
  excerpt: string | null
  featuredImage: string | null
  featuredImageVariants: Record<string, string> | null
  publishedAt: string
}

export function ArticleCard({
  title,
  slug,
  excerpt,
  featuredImage,
  featuredImageVariants,
  publishedAt,
}: ArticleCardProps) {
  const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  })

  return (
    <article className="group border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
      {featuredImage && (
        <Link href={`/blog/${slug}`}>
          <img
            src={featuredImageVariants?.card || featuredImage}
            alt={title}
            width={600}
            height={400}
            className="w-full aspect-[3/2] object-cover group-hover:scale-105 transition-transform duration-300"
            loading="lazy"
            decoding="async"
          />
        </Link>
      )}
      <div className="p-6">
        <time dateTime={publishedAt} className="text-sm text-gray-500">
          {formattedDate}
        </time>
        <h2 className="mt-2 text-xl font-semibold">
          <Link href={`/blog/${slug}`} className="hover:text-blue-600 transition-colors">
            {title}
          </Link>
        </h2>
        {excerpt && <p className="mt-2 text-gray-600 line-clamp-3">{excerpt}</p>}
      </div>
    </article>
  )
}

Setting explicit width and height attributes prevents layout shift while the image loads.


5. Building responsive <img> with srcset and sizes

For images that need to adapt to the viewport, use lynkow.media.srcset() to generate a standards-compliant srcset attribute:

tsx
// components/responsive-image.tsx
import { lynkow } from '@/lib/lynkow'

interface ResponsiveImageProps {
  src: string
  alt: string
  variants?: Record<string, string> | null
  sizes?: string
  priority?: boolean
  className?: string
}

export function ResponsiveImage({
  src,
  alt,
  variants,
  sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
  priority = false,
  className,
}: ResponsiveImageProps) {
  const srcSet = lynkow.media.srcset(src, {
    widths: [400, 800, 1200, 1920],
    quality: 80,
  })

  // Use the card variant as a sensible default src for browsers
  // that do not support srcset
  const defaultSrc = variants?.card || src

  return (
    <img
      src={defaultSrc}
      srcSet={srcSet || undefined}
      sizes={sizes}
      alt={alt}
      className={className}
      loading={priority ? 'eager' : 'lazy'}
      decoding="async"
    />
  )
}

How srcset + sizes work together

The srcset attribute tells the browser which image widths are available:

https://cdn.../cdn-cgi/image/w=400,.../image.jpg 400w,
https://cdn.../cdn-cgi/image/w=800,.../image.jpg 800w,
https://cdn.../cdn-cgi/image/w=1200,.../image.jpg 1200w,
https://cdn.../cdn-cgi/image/w=1920,.../image.jpg 1920w

The sizes attribute tells the browser how wide the image will be rendered at each viewport width. The browser then picks the smallest image that covers the rendered width (accounting for device pixel ratio).

Common sizes patterns:

Layout

sizes value

Full-width hero

100vw

Two-column grid

(max-width: 768px) 100vw, 50vw

Three-column grid

(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw

Sidebar image

(max-width: 768px) 100vw, 300px

Fixed-width card

300px

Customizing widths and quality

Pass custom options to srcset():

TypeScript
// Fewer breakpoints for a sidebar image
const srcSet = lynkow.media.srcset(imageUrl, {
  widths: [300, 600],
  quality: 75,
})

// More breakpoints for a hero image with crop
const heroSrcSet = lynkow.media.srcset(imageUrl, {
  widths: [640, 960, 1280, 1920, 2560],
  fit: 'cover',
  quality: 85,
  gravity: '0.5x0.3', // focal point: center-horizontal, upper-third
})

6. Building <picture> for art direction

When you need different crops at different breakpoints (e.g., a landscape crop on desktop and a square crop on mobile), use the <picture> element with lynkow.media.transform():

tsx
// components/hero-picture.tsx
import { lynkow } from '@/lib/lynkow'

interface HeroPictureProps {
  src: string
  alt: string
  className?: string
}

export function HeroPicture({ src, alt, className }: HeroPictureProps) {
  // Desktop: wide landscape crop
  const desktopSrc = lynkow.media.transform(src, {
    w: 1920,
    h: 640,
    fit: 'cover',
    quality: 85,
  })

  // Tablet: 4:3 crop
  const tabletSrc = lynkow.media.transform(src, {
    w: 1024,
    h: 768,
    fit: 'cover',
    quality: 85,
  })

  // Mobile: square crop
  const mobileSrc = lynkow.media.transform(src, {
    w: 640,
    h: 640,
    fit: 'cover',
    quality: 80,
  })

  return (
    <picture>
      <source media="(min-width: 1024px)" srcSet={desktopSrc} />
      <source media="(min-width: 640px)" srcSet={tabletSrc} />
      <img
        src={mobileSrc}
        alt={alt}
        className={className}
        loading="eager"
        decoding="async"
        fetchPriority="high"
      />
    </picture>
  )
}

You can combine <picture> with srcset for art direction plus resolution switching:

tsx
export function HeroPictureResponsive({ src, alt }: HeroPictureProps) {
  // Desktop: wide landscape crop at multiple resolutions
  const desktopSrcSet = lynkow.media.srcset(src, {
    widths: [1280, 1920, 2560],
    fit: 'cover',
    quality: 85,
  })

  // Mobile: square crop at multiple resolutions
  const mobileSrcSet = lynkow.media.srcset(src, {
    widths: [400, 640, 800],
    fit: 'cover',
    quality: 80,
  })

  const fallbackSrc = lynkow.media.transform(src, { w: 800, fit: 'cover' })

  return (
    <picture>
      <source
        media="(min-width: 1024px)"
        srcSet={desktopSrcSet}
        sizes="100vw"
      />
      <source
        media="(max-width: 1023px)"
        srcSet={mobileSrcSet}
        sizes="100vw"
      />
      <img
        src={fallbackSrc}
        alt={alt}
        loading="eager"
        decoding="async"
        fetchPriority="high"
      />
    </picture>
  )
}

7. Custom Next.js Image loader with transform()

If you prefer the Next.js <Image> component for its built-in lazy loading, priority hints, and placeholder support, create a custom loader that delegates to Lynkow's CDN:

TypeScript
// lib/lynkow-image-loader.ts
import { lynkow } from './lynkow'

interface ImageLoaderParams {
  src: string
  width: number
  quality?: number
}

export function lynkowImageLoader({ src, width, quality }: ImageLoaderParams): string {
  return lynkow.media.transform(src, {
    w: width,
    quality: quality || 80,
    format: 'auto',
    fit: 'scale-down',
  })
}

Use it with the <Image> component:

tsx
// components/optimized-image.tsx
import Image from 'next/image'
import { lynkowImageLoader } from '@/lib/lynkow-image-loader'

interface OptimizedImageProps {
  src: string
  alt: string
  width: number
  height: number
  priority?: boolean
  className?: string
  sizes?: string
}

export function OptimizedImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className,
  sizes,
}: OptimizedImageProps) {
  return (
    <Image
      loader={lynkowImageLoader}
      src={src}
      alt={alt}
      width={width}
      height={height}
      priority={priority}
      className={className}
      sizes={sizes}
    />
  )
}

Usage in a page:

tsx
import { lynkow } from '@/lib/lynkow'
import { OptimizedImage } from '@/components/optimized-image'

export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const article = await lynkow.contents.getBySlug(slug)

  return (
    <article>
      {article.featuredImage && (
        <OptimizedImage
          src={article.featuredImage}
          alt={article.title}
          width={1200}
          height={630}
          priority
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
        />
      )}
      <h1>{article.title}</h1>
    </article>
  )
}

Important: When using the Next.js <Image> component with a custom loader, you must also configure remotePatterns in next.config.ts to allow the Lynkow CDN domain:

TypeScript
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '*.lynkow.com',
      },
    ],
  },
}

export default nextConfig

8. Blur placeholder with tiny transform

Generate a tiny, low-quality image as a blur placeholder to show while the full image loads. This eliminates layout shift and gives users an immediate visual preview.

tsx
// components/blur-image.tsx
import { lynkow } from '@/lib/lynkow'

interface BlurImageProps {
  src: string
  alt: string
  variants?: Record<string, string> | null
  sizes?: string
  priority?: boolean
  className?: string
}

export function BlurImage({
  src,
  alt,
  variants,
  sizes = '100vw',
  priority = false,
  className,
}: BlurImageProps) {
  // Generate a 20px-wide placeholder -- tiny enough to inline
  const blurSrc = lynkow.media.transform(src, {
    w: 20,
    quality: 50,
    format: 'jpeg',
  })

  const srcSet = lynkow.media.srcset(src, {
    widths: [400, 800, 1200, 1920],
  })

  const defaultSrc = variants?.hero || variants?.content || src

  return (
    <div className={`relative overflow-hidden ${className || ''}`}>
      {/* Blur placeholder (shows immediately, tiny payload) */}
      <img
        src={blurSrc}
        alt=""
        aria-hidden="true"
        className="absolute inset-0 w-full h-full object-cover blur-xl scale-110"
      />
      {/* Full-resolution image (loads on top) */}
      <img
        src={defaultSrc}
        srcSet={srcSet || undefined}
        sizes={sizes}
        alt={alt}
        loading={priority ? 'eager' : 'lazy'}
        decoding="async"
        className="relative w-full h-full object-cover"
        onLoad={(e) => {
          // Hide the blur placeholder once the real image loads
          const prev = e.currentTarget.previousElementSibling as HTMLElement
          if (prev) prev.style.opacity = '0'
        }}
      />
    </div>
  )
}

The 20px-wide JPEG is typically under 500 bytes and loads almost instantly. The blur-xl scale-110 CSS creates a smooth blurred background while the full image downloads.

For server-side blur with the Next.js <Image> component, you can fetch the tiny image and convert it to a base64 data URL:

TypeScript
// lib/blur-data-url.ts
import { lynkow } from './lynkow'

export async function getBlurDataUrl(imageUrl: string): Promise<string> {
  const tinyUrl = lynkow.media.transform(imageUrl, {
    w: 20,
    quality: 50,
    format: 'jpeg',
  })

  const response = await fetch(tinyUrl)
  const buffer = await response.arrayBuffer()
  const base64 = Buffer.from(buffer).toString('base64')

  return `data:image/jpeg;base64,${base64}`
}

Then use it with <Image>:

tsx
import Image from 'next/image'
import { lynkowImageLoader } from '@/lib/lynkow-image-loader'
import { getBlurDataUrl } from '@/lib/blur-data-url'

interface Props {
  src: string
  alt: string
  width: number
  height: number
}

export async function BlurNextImage({ src, alt, width, height }: Props) {
  const blurDataURL = await getBlurDataUrl(src)

  return (
    <Image
      loader={lynkowImageLoader}
      src={src}
      alt={alt}
      width={width}
      height={height}
      placeholder="blur"
      blurDataURL={blurDataURL}
    />
  )
}

9. OG images for social sharing

Lynkow provides two sets of OG image variants:

  • featuredImageVariants.og -- derived from the content's featured image (1200x630, cover fit)

  • ogImageVariants.og -- derived from a dedicated OG image field, if one was uploaded separately

Use them in generateMetadata():

TypeScript
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { lynkow } from '@/lib/lynkow'

const BASE_URL = 'https://example.com'

interface Props {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const content = await lynkow.contents.getBySlug(slug)

  // Prefer the dedicated OG image, fall back to the featured image variant
  const ogImageUrl =
    content.ogImageVariants?.og ||
    content.featuredImageVariants?.og ||
    null

  return {
    title: content.metaTitle || content.title,
    description: content.metaDescription || undefined,
    openGraph: {
      title: content.metaTitle || content.title,
      description: content.metaDescription || undefined,
      url: `${BASE_URL}/blog/${slug}`,
      type: 'article',
      images: ogImageUrl
        ? [{ url: ogImageUrl, width: 1200, height: 630, alt: content.title }]
        : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: content.metaTitle || content.title,
      description: content.metaDescription || undefined,
      images: ogImageUrl ? [ogImageUrl] : [],
    },
  }
}

The OG variant is always 1200x630 pixels with cover fit, which is the standard aspect ratio for Facebook, LinkedIn, and Twitter (X) cards.


10. Gravity and focal points

When cropping images with fit: 'cover' or fit: 'crop', the gravity option controls which part of the image is preserved. This is essential for portrait photos, product shots, or any image where the subject is not centered.

Automatic face detection

TypeScript
// Use face detection for author avatars
const avatarUrl = lynkow.media.transform(authorPhoto, {
  w: 128,
  h: 128,
  fit: 'cover',
  gravity: 'face',
})

Coordinate-based focal points

Focal points use the format {x}x{y} where both values are between 0 and 1. The origin (0,0) is the top-left corner.

TypeScript
// Focus on the upper-right area of the image
const croppedUrl = lynkow.media.transform(imageUrl, {
  w: 800,
  h: 400,
  fit: 'cover',
  gravity: '0.8x0.2',
})

Common focal point values:

Position

Gravity value

Center (default)

'0.5x0.5'

Top center

'0.5x0.0'

Bottom center

'0.5x1.0'

Left center

'0.0x0.5'

Right center

'1.0x0.5'

Face detection

'face'

Portrait vs. landscape crops

tsx
// components/adaptive-thumbnail.tsx
import { lynkow } from '@/lib/lynkow'

interface AdaptiveThumbnailProps {
  src: string
  alt: string
  focalPoint?: { x: number; y: number }
}

export function AdaptiveThumbnail({ src, alt, focalPoint }: AdaptiveThumbnailProps) {
  const gravity = focalPoint ? `${focalPoint.x}x${focalPoint.y}` : undefined

  // Landscape crop for desktop
  const landscapeSrc = lynkow.media.transform(src, {
    w: 800,
    h: 450,
    fit: 'cover',
    gravity,
  })

  // Square crop for mobile
  const squareSrc = lynkow.media.transform(src, {
    w: 400,
    h: 400,
    fit: 'cover',
    gravity,
  })

  return (
    <picture>
      <source media="(min-width: 640px)" srcSet={landscapeSrc} />
      <img
        src={squareSrc}
        alt={alt}
        className="w-full object-cover"
        loading="lazy"
        decoding="async"
      />
    </picture>
  )
}

With srcset for responsive focal-point crops:

TypeScript
const srcSet = lynkow.media.srcset(portraitPhoto, {
  widths: [400, 800, 1200],
  fit: 'cover',
  gravity: '0.5x0.3', // keep the upper portion (face area)
  quality: 85,
})

11. Complete ResponsiveImage component

This production-ready component combines srcset, variants, blur placeholder, focal points, and priority loading:

tsx
// components/responsive-image.tsx
import { lynkow } from '@/lib/lynkow'

interface ResponsiveImageProps {
  /** Original image URL from the Lynkow API */
  src: string | null | undefined
  /** Alt text for accessibility */
  alt: string
  /** Pre-computed image variants from the API */
  variants?: Record<string, string> | null
  /** CSS sizes attribute for responsive layout */
  sizes?: string
  /** Load eagerly for above-the-fold images */
  priority?: boolean
  /** Additional CSS classes */
  className?: string
  /** Custom srcset widths (default: [400, 800, 1200, 1920]) */
  widths?: number[]
  /** Focal point for crop-based transforms */
  gravity?: string
  /** Image quality 1-100 (default: 80) */
  quality?: number
  /** Resize fit mode */
  fit?: 'cover' | 'contain' | 'scale-down' | 'crop'
  /** Explicit width for aspect ratio */
  width?: number
  /** Explicit height for aspect ratio */
  height?: number
}

export function ResponsiveImage({
  src,
  alt,
  variants,
  sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
  priority = false,
  className,
  widths = [400, 800, 1200, 1920],
  gravity,
  quality = 80,
  fit = 'scale-down',
  width,
  height,
}: ResponsiveImageProps) {
  if (!src) return null

  const srcSet = lynkow.media.srcset(src, { widths, gravity, quality, fit })
  const defaultSrc = variants?.card || variants?.content || src

  // Generate a tiny blur placeholder URL
  const blurSrc = lynkow.media.transform(src, {
    w: 20,
    quality: 50,
    format: 'jpeg',
  })

  return (
    <div className={`relative overflow-hidden ${className || ''}`}>
      {/* Blur placeholder */}
      {blurSrc && (
        <img
          src={blurSrc}
          alt=""
          aria-hidden="true"
          className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 transition-opacity duration-500"
        />
      )}
      {/* Full image */}
      <img
        src={defaultSrc}
        srcSet={srcSet || undefined}
        sizes={sizes}
        alt={alt}
        width={width}
        height={height}
        loading={priority ? 'eager' : 'lazy'}
        decoding="async"
        fetchPriority={priority ? 'high' : undefined}
        className="relative w-full h-full object-cover"
        onLoad={(e) => {
          const blur = e.currentTarget.previousElementSibling as HTMLElement | null
          if (blur) blur.style.opacity = '0'
        }}
      />
    </div>
  )
}

12. Complete ArticleCard component with optimized image

Putting it all together -- a card component that uses the card variant as a direct src, with srcset for retina displays, and a blur placeholder:

tsx
// components/article-card.tsx
import Link from 'next/link'
import { lynkow } from '@/lib/lynkow'

interface ArticleCardProps {
  title: string
  slug: string
  excerpt: string | null
  featuredImage: string | null
  featuredImageVariants: Record<string, string> | null
  publishedAt: string
  category?: { name: string; slug: string } | null
}

export function ArticleCard({
  title,
  slug,
  excerpt,
  featuredImage,
  featuredImageVariants,
  publishedAt,
  category,
}: ArticleCardProps) {
  const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  })

  // Generate srcset for retina-quality cards (1x at 600, 2x at 1200)
  const srcSet = featuredImage
    ? lynkow.media.srcset(featuredImage, {
        widths: [400, 600, 800, 1200],
        fit: 'cover',
        quality: 80,
      })
    : ''

  // Tiny blur placeholder
  const blurSrc = featuredImage
    ? lynkow.media.transform(featuredImage, {
        w: 20,
        quality: 40,
        format: 'jpeg',
      })
    : ''

  return (
    <article className="group border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
      {featuredImage && (
        <Link href={`/blog/${slug}`} className="block relative overflow-hidden">
          {/* Blur placeholder */}
          {blurSrc && (
            <img
              src={blurSrc}
              alt=""
              aria-hidden="true"
              className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 transition-opacity duration-500"
            />
          )}
          {/* Full image */}
          <img
            src={featuredImageVariants?.card || featuredImage}
            srcSet={srcSet || undefined}
            sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
            alt={title}
            width={600}
            height={400}
            className="relative w-full aspect-[3/2] object-cover group-hover:scale-105 transition-transform duration-300"
            loading="lazy"
            decoding="async"
            onLoad={(e) => {
              const blur = e.currentTarget.previousElementSibling as HTMLElement | null
              if (blur) blur.style.opacity = '0'
            }}
          />
        </Link>
      )}

      <div className="p-6">
        <div className="flex items-center gap-3 text-sm text-gray-500">
          <time dateTime={publishedAt}>{formattedDate}</time>
          {category && (
            <>
              <span aria-hidden="true">-</span>
              <Link
                href={`/blog/category/${category.slug}`}
                className="text-blue-600 hover:underline"
              >
                {category.name}
              </Link>
            </>
          )}
        </div>

        <h2 className="mt-2 text-xl font-semibold">
          <Link
            href={`/blog/${slug}`}
            className="hover:text-blue-600 transition-colors"
          >
            {title}
          </Link>
        </h2>

        {excerpt && (
          <p className="mt-2 text-gray-600 line-clamp-3">{excerpt}</p>
        )}
      </div>
    </article>
  )
}

Usage on a listing page:

tsx
// app/blog/page.tsx
import { lynkow } from '@/lib/lynkow'
import { ArticleCard } from '@/components/article-card'

export default async function BlogPage() {
  const { data: articles } = await lynkow.contents.list({
    limit: 12,
    sort: 'published_at',
    order: 'desc',
  })

  return (
    <main className="max-w-7xl mx-auto px-4 py-12">
      <h1 className="text-3xl font-bold mb-8">Blog</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {articles.map((article) => (
          <ArticleCard
            key={article.id}
            title={article.title}
            slug={article.slug}
            excerpt={article.excerpt}
            featuredImage={article.featuredImage}
            featuredImageVariants={article.featuredImageVariants}
            publishedAt={article.publishedAt}
            category={article.category}
          />
        ))}
      </div>
    </main>
  )
}

API reference summary

lynkow.media.srcset(imageUrl, options?)

Generates a complete srcset attribute string from a Lynkow image URL.

TypeScript
lynkow.media.srcset(
  imageUrl: string | null | undefined,
  options?: {
    widths?: number[]        // default: [400, 800, 1200, 1920]
    fit?: 'cover' | 'contain' | 'scale-down' | 'crop'  // default: 'scale-down'
    quality?: number         // 1-100, default: 80
    gravity?: string         // e.g. '0.5x0.3', 'face'
  }
): string  // Returns srcset string or empty string

lynkow.media.transform(imageUrl, options?)

Generates a single transformed image URL.

TypeScript
lynkow.media.transform(
  imageUrl: string | null | undefined,
  options?: {
    w?: number               // target width
    h?: number               // target height
    fit?: 'cover' | 'contain' | 'scale-down' | 'crop'  // default: 'scale-down'
    quality?: number         // 1-100, default: 80
    format?: 'auto' | 'webp' | 'avif' | 'jpeg'         // default: 'auto'
    gravity?: string         // e.g. '0.5x0.3', 'face'
    dpr?: number             // device pixel ratio, 1-4
  }
): string  // Returns transformed URL or empty string

Pre-computed variant presets

Variant

Dimensions

Fit

Use case

thumbnail

400x300

cover

Grid thumbnails, admin previews

card

600x400

cover

Article cards, listing pages

content

1200w

scale-down

Inline content images

medium

960w

scale-down

Medium-width containers

hero

1920w

scale-down

Full-width hero banners

og

1200x630

cover

Open Graph / social sharing

avatar

128x128

cover

User avatars (gravity: face)

full

2560w

scale-down

Lightbox, full resolution