05 — Media and Blob Storage
Dev Guide — Savoy Signature Hotels
PRD refs:13_Media_and_Image_Pipeline.md,02_Infrastructure_and_Environments.md
1. Overview
Section titled “1. Overview”Media assets are uploaded to Umbraco and stored in Azure Blob Storage. The frontend never processes images server-side — all image transformation is handled by Cloudflare Images at the edge. This guide covers Blob Storage setup, media library organization, upload restrictions, image optimization, and the delivery pipeline.
2. Asset Delivery Pipeline
Section titled “2. Asset Delivery Pipeline”Key principle: No server-side image processing. Umbraco.ImageSharp is NOT used for frontend delivery. All processing happens at the Cloudflare edge.
3. Azure Blob Storage Configuration
Section titled “3. Azure Blob Storage Configuration”3.1 NuGet Package
Section titled “3.1 NuGet Package”<PackageReference Include="Umbraco.StorageProviders.AzureBlob" Version="..." />3.2 Configuration
Section titled “3.2 Configuration”{ "Umbraco": { "Storage": { "AzureBlob": { "Media": { "ConnectionString": "${AZURE_BLOB_CONNECTION_STRING}", "ContainerName": "media" } } } }}3.3 Access
Section titled “3.3 Access”| Operation | Method |
|---|---|
| Read (public) | Container is public for reads (Cloudflare fronts) |
| Write | SAS tokens or Azure Managed Identity |
Benefit: Stateless web app — Umbraco can scale horizontally or teardown without losing media files.
4. Media Library Organization
Section titled “4. Media Library Organization”Pre-create this folder structure BEFORE editors start uploading:
Media Library├── Savoy Signature/│ ├── Homepage/│ │ ├── Desktop/│ │ └── Mobile/│ ├── Rooms/│ │ ├── Desktop/│ │ └── Mobile/│ ├── Restaurants/│ ├── Spa/│ ├── Events/│ └── General/├── Savoy Palace/│ ├── Homepage/│ │ ├── Desktop/│ │ └── Mobile/│ ├── Rooms/│ ├── Restaurants/│ └── ...├── Royal Savoy/│ └── ...├── Saccharum/│ └── ...├── The Reserve/│ └── ...├── Calheta Beach/│ └── ...├── Gardens/│ └── ...├── Hotel Next/│ └── ...└── _Shared/ # Underscore prefix sorts first ├── Logos/ ├── Icons/ ├── Legal/ └── Group/Rules:
- One folder per hotel — editors should NOT cross-reference between hotel folders
- Desktop/Mobile sub-folders for image-heavy sections (Hero, Rooms, Galleries)
_Sharedfolder (underscore prefix) for common assets across all sites- File naming convention:
{site}-{section}-{subject}-{variant}.{ext}
File Naming Examples:
savoy-palace-room-deluxe-ocean-desktop.jpgsavoy-palace-room-deluxe-ocean-mobile.jpgroyal-savoy-hero-main-desktop.jpgroyal-savoy-hero-main-mobile.jpg_shared-logo-group-color.svg5. Upload Restrictions
Section titled “5. Upload Restrictions”Configure in Umbraco backoffice (Settings > Media Types):
| File Type | Max Size | Allowed Extensions |
|---|---|---|
| Images | Desktop: 1MB, Mobile: 500KB | .jpg, .jpeg, .png, .webp, .svg |
| Videos | 10MB | .mp4, .webm |
| Documents | 5MB | .pdf, .docx |
5.1 Video Policy
Section titled “5.1 Video Policy”- Short looping mute background videos (< 10MB): Upload to Blob Storage
- Full videos with audio (> 10MB): MUST be hosted on YouTube/Vimeo and embedded via
videoBlock(M12) module - Never upload large video files to Blob Storage
5.2 SVG Security
Section titled “5.2 SVG Security”SVGs uploaded to Media Library are sanitized on upload:
- Strip ALL
<script>tags - Strip ALL
on*event attributes (onclick, onload, etc.) - This prevents XSS via malicious SVG files
SVG usage split:
- Theme UI icons (arrows, close, menu, social): Inline in Next.js
packages/uias React components - Hotel logos and content illustrations: Uploaded to Media Library as
<img src=".svg">
6. Responsive Image Pattern
Section titled “6. Responsive Image Pattern”Every image in the system requires both Desktop and Mobile variants.
6.1 Image Size Guidelines
Section titled “6.1 Image Size Guidelines”| Context | Desktop Dimensions | Mobile Dimensions |
|---|---|---|
| Hero / Full-width | 1920x1080 | 750x1000 |
| Card thumbnail | 600x400 | 375x250 |
| Gallery | 1440x960 | 750x500 |
| Logo / Icons | SVG | SVG |
6.2 Compression Targets
Section titled “6.2 Compression Targets”| Variant | Max File Size (compressed) |
|---|---|
| Desktop images | 500KB |
| Mobile images | 200KB |
6.3 Content Model Pattern
Section titled “6.3 Content Model Pattern”Every module that displays images uses the responsiveImageComposition or responsiveImageItem:
imageDesktop (Media Picker, required, 1920x1080+)imageMobile (Media Picker, required, 750x1000+)For galleries, use the responsiveImageItem Element Type:
| Property | Alias | Editor | Required |
|---|---|---|---|
| Image Desktop | imageDesktop | Media Picker | Yes |
| Image Mobile | imageMobile | Media Picker | Yes |
| Alt Text | altText | Textstring | Yes |
| Caption | caption | Textstring | No |
7. Focal Points
Section titled “7. Focal Points”Umbraco’s Image Cropper editor is used ONLY for focal point definition (not predefined crops).
7.1 How it works
Section titled “7.1 How it works”- Editor uploads image and sets focal point in Umbraco Image Cropper
- API returns focal point coordinates:
focalPoint: { top: 0.3, left: 0.5 } - Frontend translates to CSS:
object-position: 50% 30% - No hardcoded crops — uses
<picture>+object-fit: cover
7.2 Frontend Translation
Section titled “7.2 Frontend Translation”function focalPointToObjectPosition(focalPoint: { top: number; left: number }): string { return `${Math.round(focalPoint.left * 100)}% ${Math.round(focalPoint.top * 100)}%`;}
// Usage in component:// style={{ objectPosition: focalPointToObjectPosition(image.focalPoint) }}8. Cloudflare Images Custom Loader
Section titled “8. Cloudflare Images Custom Loader”The Next.js custom image loader routes all images through Cloudflare for on-the-fly transformation:
export default function cloudflareLoader({ src, width, quality,}: { src: string; width: number; quality?: number;}) { const params = [ `width=${width}`, `quality=${quality || 75}`, 'format=auto', // Cloudflare picks WebP or AVIF based on browser support ].join(',');
return `https://savoysignature.com/cdn-cgi/image/${params}/${src}`;}next.config.ts:
const nextConfig = { images: { loader: 'custom', loaderFile: './src/lib/cloudflare-image-loader.ts', },};8.1 Cloudflare Polish Settings
Section titled “8.1 Cloudflare Polish Settings”| Setting | Value |
|---|---|
| Polish | Lossy |
| WebP | Enabled |
| Mirage | Enabled (lazy-loads images on slow connections) |
9. Multi-Language Media
Section titled “9. Multi-Language Media”- PT is the default — all images are uploaded for PT
- EN override is uploaded ONLY when the image contains visible text (menus, infographics, certificates)
- Images without text content serve the PT version for both languages
export function resolveMediaForLocale( mediaProperty: any, locale: string): UmbracoMedia | null { // Invariant media (no culture variants) if ('url' in mediaProperty) return mediaProperty;
// Check for locale-specific variant if (locale === 'en' && mediaProperty.en) return mediaProperty.en;
// Fallback to PT return mediaProperty.pt || null;}10. Form Upload Storage
Section titled “10. Form Upload Storage”Form file uploads (CVs on Careers page) use a separate Blob container:
| Setting | Value |
|---|---|
| Container | form-uploads |
| Max file size | 5MB |
| Allowed types | .pdf, .docx |
| Access | Private (no public read) |
| Download | Secure link via CMS auth or expiring SAS token |
| Retention | Auto-delete after 90 days (GDPR) |
Rule: File attachments are NEVER sent directly in emails. Emails contain secure download links only.
11. Performance Budget
Section titled “11. Performance Budget”| Asset | Budget |
|---|---|
| Total page weight | < 500KB gzipped |
| LCP image (desktop) | < 500KB |
| LCP image (mobile) | < 200KB |
| Fonts | < 100KB per theme |
| Third-party scripts | < 100KB |
12. Step-by-Step: Setting Up Blob Storage for a New Environment
Section titled “12. Step-by-Step: Setting Up Blob Storage for a New Environment”- Create Azure Storage Account — Standard LRS, Hot tier
- Create
mediacontainer — Set public access level to “Blob” (read-only) - Create
form-uploadscontainer — Set access level to “Private” - Install NuGet package —
Umbraco.StorageProviders.AzureBlob - Configure connection string — Store in Azure Key Vault, inject via Managed Identity
- Configure Cloudflare — Point image CDN URLs to Blob Storage origin
- Enable Polish — Lossy compression + WebP in Cloudflare dashboard
- Pre-create Media Library folders — Follow the structure in Section 4
- Test upload — Verify images are accessible via Cloudflare CDN URL
13. Common Pitfalls
Section titled “13. Common Pitfalls”| Pitfall | Solution |
|---|---|
| Missing mobile image variant | Every image requires imageDesktop + imageMobile |
| Large video uploaded to Blob | Videos > 10MB must be on YouTube/Vimeo |
| Images exceeding size limits | Desktop max 1MB, Mobile max 500KB (before Cloudflare compression) |
| Cross-site media references | Keep media isolated per hotel folder |
| Unsanitized SVG upload | Ensure SVG sanitization strips <script> and on* attributes |
| Using Umbraco ImageSharp for frontend | All image processing is via Cloudflare Images, not server-side |
| Forgot focal point | Always use Image Cropper editor for focal point, even if default center |