Direkt zum Hauptinhalt

Assets API

Die Assets API ermöglicht den Zugriff auf Bilder und andere Medien-Dateien mit dynamischen Transformationsparametern. Sie bietet optimierte Bilddelivery mit Format-Konvertierung, Größenanpassung und verschiedenen Anpassungsoptionen.

Endpunkt

Asset abrufen

GET /assets/{asset_id}

Ruft ein Asset vom CMS ab und wendet die angegebenen Transformationsparameter an.

Parameter
Parameter Typ Beschreibung Beispiel
asset_id string Asset-ID (UUID) aus dem CMS 8e1cf6a9-d1d6-4090-9a36-303f1f57a016
width integer Breite in Pixeln 1500
height integer Höhe in Pixeln 700
quality integer Bildqualität (1-100) 80
withoutEnlargement boolean Automatische Vergrößerung deaktivieren true
format string Ausgabeformat webp, jpg, png, avif, tiff, auto
fit string Anpassungsmodus cover, contain, inside, outside
Beispiele
# Einfacher Asset-Abruf
curl "https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016"

# Optimiertes Thumbnail
curl "https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=webp&width=300&height=200&fit=cover&quality=85"

# Hero-Image für moderne Browser
curl "https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=avif&width=1920&height=800&fit=cover&withoutEnlargement=true"

Parameter im Detail

Format-Optionen

Format Beschreibung Verwendung
auto Automatische Format-Auswahl (Standard) Versucht WebP/AVIF für moderne Browser, sonst JPEG
webp WebP-Format Gute Kompression, breite Browser-Unterstützung
avif AVIF-Format Beste Kompression, neuere Browser
jpg JPEG-Format Universelle Kompatibilität
png PNG-Format Für Transparenz und verlustfreie Kompression
tiff TIFF-Format Hochqualitative Archivierung

Anpassungsmodi (fit)

Modus Beschreibung Verwendung
cover Bild vollständig in Dimensionen einpassen (Standard) Hero-Images, Thumbnails
contain Bild mit Letterboxing in Dimensionen einpassen Logos, vollständige Bilder
inside So groß wie möglich innerhalb der Dimensionen Responsive Bilder
outside So klein wie möglich innerhalb/außerhalb der Dimensionen Spezielle Layouts

Qualitätseinstellungen

Qualität Beschreibung Dateigröße Verwendung
95-100 Höchste Qualität Sehr groß Druckvorbereitung
85-95 Hohe Qualität Groß Hero-Images
75-85 Gute Qualität Mittel Standard-Bilder
60-75 Moderate Qualität Klein Thumbnails
40-60 Niedrige Qualität Sehr klein Vorschaubilder

Antwort

Die API gibt eine HTTP 302 Redirect zum optimierten Asset im CMS zurück:

HTTP/1.1 302 Found
Location: https://cms.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016/?format=webp&width=1500&height=700&fit=cover
Content-Type: application/json

Beispiel-Code

Responsive Bilder in HTML

<picture>
  <source
    srcset="https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=avif&width=1920&height=800&fit=cover"
    type="image/avif"
    media="(min-width: 1200px)">
  <source
    srcset="https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=webp&width=1200&height=600&fit=cover"
    type="image/webp"
    media="(min-width: 768px)">
  <source
    srcset="https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=webp&width=800&height=400&fit=cover"
    type="image/webp">
  <img
    src="https://api.kulturpool.at/assets/8e1cf6a9-d1d6-4090-9a36-303f1f57a016?format=jpg&width=800&height=400&fit=cover"
    alt="Beschreibung"
    loading="lazy">
</picture>

JavaScript Asset-URL Generator

class AssetUrlBuilder {
  constructor(baseUrl = 'https://api.kulturpool.at/assets') {
    this.baseUrl = baseUrl;
  }

  build(assetId, options = {}) {
    const {
      width,
      height,
      quality = 85,
      format = 'auto',
      fit = 'cover',
      withoutEnlargement = false
    } = options;

    const params = new URLSearchParams();

    if (width) params.set('width', width);
    if (height) params.set('height', height);
    if (quality !== 85) params.set('quality', quality);
    if (format !== 'auto') params.set('format', format);
    if (fit !== 'cover') params.set('fit', fit);
    if (withoutEnlargement) params.set('withoutEnlargement', 'true');

    const queryString = params.toString();
    return `${this.baseUrl}/${assetId}${queryString ? `?${queryString}` : ''}`;
  }

  // Vordefinierte Größen
  thumbnail(assetId, size = 150) {
    return this.build(assetId, {
      width: size,
      height: size,
      format: 'webp',
      quality: 80
    });
  }

  hero(assetId, width = 1920, height = 800) {
    return this.build(assetId, {
      width,
      height,
      format: 'webp',
      quality: 85,
      fit: 'cover'
    });
  }

  logo(assetId, maxWidth = 200) {
    return this.build(assetId, {
      width: maxWidth,
      format: 'png',
      fit: 'contain',
      withoutEnlargement: true
    });
  }
}

// Verwendung
const assetBuilder = new AssetUrlBuilder();
const thumbnailUrl = assetBuilder.thumbnail('8e1cf6a9-d1d6-4090-9a36-303f1f57a016');
const heroUrl = assetBuilder.hero('8e1cf6a9-d1d6-4090-9a36-303f1f57a016', 1500, 600);

Python Asset-Manager

from urllib.parse import urlencode

class AssetManager:
    def __init__(self, base_url='https://api.kulturpool.at/assets'):
        self.base_url = base_url

    def build_url(self, asset_id, **kwargs):
        """Asset-URL mit Parametern erstellen"""
        params = {k: v for k, v in kwargs.items() if v is not None}

        # Boolean-Werte in Strings konvertieren
        for key, value in params.items():
            if isinstance(value, bool):
                params[key] = str(value).lower()

        url = f"{self.base_url}/{asset_id}"
        if params:
            url += f"?{urlencode(params)}"

        return url

    def get_responsive_urls(self, asset_id, format_type='webp'):
        """Responsive Bild-URLs für verschiedene Bildschirmgrößen"""
        return {
            'mobile': self.build_url(asset_id, width=480, height=320, format=format_type, fit='cover'),
            'tablet': self.build_url(asset_id, width=768, height=512, format=format_type, fit='cover'),
            'desktop': self.build_url(asset_id, width=1200, height=800, format=format_type, fit='cover'),
            'large': self.build_url(asset_id, width=1920, height=1080, format=format_type, fit='cover')
        }

    def get_optimized_thumbnail(self, asset_id, size=300):
        """Optimiertes Thumbnail"""
        return self.build_url(
            asset_id,
            width=size,
            height=size,
            format='webp',
            quality=80,
            fit='cover'
        )

# Verwendung
asset_manager = AssetManager()
asset_id = '8e1cf6a9-d1d6-4090-9a36-303f1f57a016'

# Responsive URLs
responsive_urls = asset_manager.get_responsive_urls(asset_id)
print(f"Mobile: {responsive_urls['mobile']}")

# Thumbnail
thumbnail_url = asset_manager.get_optimized_thumbnail(asset_id, 150)
print(f"Thumbnail: {thumbnail_url}")

Anwendungsfälle

Content Management System Integration

// Asset-URLs für Institution Hero-Images generieren
async function getInstitutionWithOptimizedImages(institutionId) {
  const response = await fetch(`https://api.kulturpool.at/institutions/${institutionId}`);
  const { data: institution } = await response.json();

  const assetBuilder = new AssetUrlBuilder();

  return {
    ...institution,
    optimizedImages: {
      favicon: institution.favicon?.id ?
        assetBuilder.build(institution.favicon.id, {
          width: 32,
          height: 32,
          format: 'png'
        }) : null,
      logo: institution.logo?.id ?
        assetBuilder.logo(institution.logo.id, 200) : null,
      hero: institution.hero_image?.id ?
        assetBuilder.hero(institution.hero_image.id) : null,
      heroMobile: institution.hero_image?.id ?
        assetBuilder.build(institution.hero_image.id, {
          width: 768,
          height: 400,
          format: 'webp',
          fit: 'cover'
        }) : null
    }
  };
}

Bildergalerie mit Lazy Loading

class ImageGallery {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.assetBuilder = new AssetUrlBuilder();
    this.observer = new IntersectionObserver(this.handleIntersection.bind(this));
  }

  addImage(assetId, alt = '') {
    const imageContainer = document.createElement('div');
    imageContainer.className = 'gallery-item';
    imageContainer.dataset.assetId = assetId;
    imageContainer.dataset.alt = alt;

    // Placeholder
    const placeholder = document.createElement('div');
    placeholder.className = 'image-placeholder';
    placeholder.style.backgroundColor = '#f0f0f0';
    placeholder.style.aspectRatio = '16/9';

    imageContainer.appendChild(placeholder);
    this.container.appendChild(imageContainer);

    // Für Lazy Loading beobachten
    this.observer.observe(imageContainer);
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.loadImage(entry.target);
        this.observer.unobserve(entry.target);
      }
    });
  }

  loadImage(container) {
    const assetId = container.dataset.assetId;
    const alt = container.dataset.alt;

    const picture = document.createElement('picture');

    // WebP für moderne Browser
    const sourceWebP = document.createElement('source');
    sourceWebP.srcset = this.assetBuilder.build(assetId, {
      width: 800,
      height: 450,
      format: 'webp',
      fit: 'cover'
    });
    sourceWebP.type = 'image/webp';

    // JPEG Fallback
    const img = document.createElement('img');
    img.src = this.assetBuilder.build(assetId, {
      width: 800,
      height: 450,
      format: 'jpg',
      fit: 'cover'
    });
    img.alt = alt;
    img.loading = 'lazy';

    picture.appendChild(sourceWebP);
    picture.appendChild(img);

    container.innerHTML = '';
    container.appendChild(picture);
  }
}

CDN-Integration und Caching

class CachedAssetManager {
  constructor() {
    this.assetBuilder = new AssetUrlBuilder();
    this.cache = new Map();
    this.cacheTimeout = 5 * 60 * 1000; // 5 Minuten
  }

  async preloadAsset(assetId, options = {}) {
    const url = this.assetBuilder.build(assetId, options);
    const cacheKey = `${assetId}_${JSON.stringify(options)}`;

    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      if (Date.now() - cached.timestamp < this.cacheTimeout) {
        return cached.blob;
      }
    }

    try {
      const response = await fetch(url);
      const blob = await response.blob();

      this.cache.set(cacheKey, {
        blob,
        timestamp: Date.now()
      });

      return blob;
    } catch (error) {
      console.error(`Fehler beim Laden von Asset ${assetId}:`, error);
      return null;
    }
  }

  createObjectUrl(assetId, options = {}) {
    const cacheKey = `${assetId}_${JSON.stringify(options)}`;
    const cached = this.cache.get(cacheKey);

    if (cached?.blob) {
      return URL.createObjectURL(cached.blob);
    }

    // Fallback: Direkte URL zurückgeben
    return this.assetBuilder.build(assetId, options);
  }

  // Cache aufräumen
  clearExpiredCache() {
    const now = Date.now();
    for (const [key, value] of this.cache.entries()) {
      if (now - value.timestamp >= this.cacheTimeout) {
        this.cache.delete(key);
      }
    }
  }
}

SEO-optimierte Asset-Integration

function generateSEOOptimizedAssetUrls(assetId, title = '') {
  const assetBuilder = new AssetUrlBuilder();

  return {
    // Open Graph Image
    ogImage: assetBuilder.build(assetId, {
      width: 1200,
      height: 630,
      format: 'jpg',
      fit: 'cover',
      quality: 85
    }),

    // Twitter Card Image
    twitterImage: assetBuilder.build(assetId, {
      width: 1024,
      height: 512,
      format: 'jpg',
      fit: 'cover',
      quality: 85
    }),

    // JSON-LD Structured Data Image
    structuredDataImage: assetBuilder.build(assetId, {
      width: 1200,
      height: 800,
      format: 'jpg',
      fit: 'cover',
      quality: 90
    }),

    // Apple Touch Icon
    appleTouchIcon: assetBuilder.build(assetId, {
      width: 180,
      height: 180,
      format: 'png',
      fit: 'cover'
    })
  };
}

Performance-Optimierung

Bildformate nach Browser-Unterstützung

function getBestSupportedFormat() {
  // Feature Detection für moderne Bildformate
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // AVIF Support
  if (canvas.toDataURL('image/avif').indexOf('data:image/avif') === 0) {
    return 'avif';
  }

  // WebP Support
  if (canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0) {
    return 'webp';
  }

  return 'jpg'; // Fallback
}

// Automatische Format-Auswahl
const optimalFormat = getBestSupportedFormat();
const optimizedUrl = assetBuilder.build(assetId, {
  width: 800,
  height: 600,
  format: optimalFormat
});

Batch-Preloading für bessere Performance

async function preloadAssetBatch(assetIds, options = {}) {
  const promises = assetIds.map(async (assetId) => {
    const link = document.createElement('link');
    link.rel = 'preload';
    link.as = 'image';
    link.href = assetBuilder.build(assetId, options);

    document.head.appendChild(link);

    return new Promise((resolve) => {
      link.onload = resolve;
      link.onerror = resolve; // Auch bei Fehlern fortfahren
    });
  });

  await Promise.allSettled(promises);
}

// Verwendung für kritische Bilder
const criticalAssets = ['asset-1', 'asset-2', 'asset-3'];
await preloadAssetBatch(criticalAssets, {
  width: 1200,
  height: 600,
  format: 'webp'
});

Fehlerbehandlung

404 - Asset nicht gefunden

{
  "detail": "Asset mit ID 'invalid-id' wurde nicht gefunden"
}

500 - CMS-Fehler

{
  "detail": "Fehler beim Abrufen des Assets vom CMS"
}

Fehlerbehandlung im Frontend

function createAssetWithFallback(assetId, options = {}, fallbackSrc = '/placeholder.jpg') {
  const img = new Image();
  const primaryUrl = assetBuilder.build(assetId, options);

  img.onerror = () => {
    img.src = fallbackSrc;
  };

  img.src = primaryUrl;
  return img;
}