+ {content.eyebrow && (
+
+ )}
+ {content.title && (
+
+ )}
+ {content.text && (
+
+ )}
+ {(content.primary_action?.url || content.secondary_action?.url) && (
+
+ )}
+
+
- {/* Eyebrow */}
- {content.eyebrow && (
-
- )}
+ const mediaEl = content.media ?
: null;
- {/* Title */}
- {content.title && (
-
- )}
-
- {/* Subtitle/Text */}
- {content.text && (
-
- )}
-
- {/* Action Buttons */}
- {(content.primary_action?.url || content.secondary_action?.url) && (
-
- )}
-
-
+ // Split layout: two-column grid, optionally reversed
+ if (layout.grid) {
+ return (
+
+
+
+
{contentStack}
+ {mediaEl &&
{mediaEl}
}
+
+ );
+ }
+
+ // Stacked layout: text top (constrained), media full-width below
+ if (layout.stacked) {
+ return (
+
+
+
{contentStack}
+ {mediaEl &&
{mediaEl}
}
+
+
+ );
+ }
+
+ // Centered (default)
+ return (
+
);
};
diff --git a/modules/blox/layouts/_partials/blox/preact-wrapper.html b/modules/blox/layouts/_partials/blox/preact-wrapper.html
index 41ce3d6e..b27df14a 100644
--- a/modules/blox/layouts/_partials/blox/preact-wrapper.html
+++ b/modules/blox/layouts/_partials/blox/preact-wrapper.html
@@ -33,6 +33,24 @@
{{ end }}
{{ end }}
+{{/* Resolve hero media (single image, optional dark variant). Videos pass through unprocessed. */}}
+{{ with $block.content.media }}
+ {{ if or (not .type) (eq .type "image") }}
+ {{ with .src }}
+ {{ $img_data := partial "functions/process_image_for_props" (dict "path" . "Page" $page) }}
+ {{ with $img_data.src }}
+ {{ $props = merge $props (dict "media_image" $img_data) }}
+ {{ end }}
+ {{ end }}
+ {{ with .dark_src }}
+ {{ $img_data := partial "functions/process_image_for_props" (dict "path" . "Page" $page) }}
+ {{ with $img_data.src }}
+ {{ $props = merge $props (dict "media_image_dark" $img_data) }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+
{{/* Resolve icon for single button (cta-card) */}}
{{ with $block.content.button.icon }}
{{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }}
diff --git a/modules/blox/layouts/_partials/functions/process_image_for_props.html b/modules/blox/layouts/_partials/functions/process_image_for_props.html
new file mode 100644
index 00000000..67b22a88
--- /dev/null
+++ b/modules/blox/layouts/_partials/functions/process_image_for_props.html
@@ -0,0 +1,45 @@
+{{/*
+ process_image_for_props — One-shot helper: resolves an image path and returns a
+ {src, srcset?, width?, height?} dict ready to merge into a Preact block's props.
+ SVGs and GIFs pass through unprocessed (preserves vectors / animation frames).
+
+ Inputs:
+ .path: image path (required)
+ .Page: current page (recommended for page-resource lookup)
+ .sizes: slice of widths for srcset generation
+ (optional, defaults to [600, 900, 1200, 1800])
+
+ Returns: image data dict {src, srcset?, width?, height?}, or empty dict if the
+ path is missing or unresolvable.
+*/}}
+{{ $result := dict }}
+{{ $path := .path | default "" }}
+
+{{ if $path }}
+ {{ $sizes := .sizes | default (slice 600 900 1200 1800) }}
+ {{ $resolved := partial "functions/resolve_image_resource" (dict "path" $path "Page" .Page) }}
+
+ {{ with $resolved.resource }}
+ {{ $canProcess := and (reflect.IsImageResourceProcessable .) (ne .MediaType.SubType "gif") }}
+ {{ if not $canProcess }}
+ {{ $result = dict "src" .RelPermalink }}
+ {{ else }}
+ {{ $responsive := partial "functions/process_responsive_image.html" (dict
+ "image" .
+ "mode" "fit"
+ "sizes" $sizes
+ ) }}
+ {{ $result = dict
+ "src" $responsive.fallback.RelPermalink
+ "srcset" $responsive.srcset
+ "width" $responsive.fallback.Width
+ "height" $responsive.fallback.Height
+ }}
+ {{ end }}
+ {{ end }}
+ {{ with $resolved.fallback_url }}
+ {{ $result = dict "src" . }}
+ {{ end }}
+{{ end }}
+
+{{ return $result }}
diff --git a/modules/blox/layouts/_partials/functions/resolve_image_resource.html b/modules/blox/layouts/_partials/functions/resolve_image_resource.html
new file mode 100644
index 00000000..796bbfc8
--- /dev/null
+++ b/modules/blox/layouts/_partials/functions/resolve_image_resource.html
@@ -0,0 +1,54 @@
+{{/*
+ resolve_image_resource — Resolves an image path string to either a Hugo image
+ resource or a fallback URL. Handles remote URLs, page resources, and assets/media
+ lookups using the same rules as the inline per-item image resolver.
+
+ Inputs:
+ .path: image path (required) — relative ("dashboard.png", "media/foo.jpg")
+ or absolute URL ("https://…").
+ .Page: current page (optional, recommended for page-resource lookup).
+
+ Returns dict with one of:
+ .resource: Hugo image resource (use with .Process, srcset, etc.)
+ .fallback_url: plain URL string (use as
![]()
)
+ Or an empty dict if path is missing.
+*/}}
+{{ $result := dict }}
+{{ $path := strings.TrimSpace (printf "%v" (.path | default "")) }}
+
+{{ if $path }}
+ {{ $is_remote := or (strings.HasPrefix $path "http://") (strings.HasPrefix $path "https://") }}
+
+ {{ if $is_remote }}
+ {{ $remote := try (resources.GetRemote $path) }}
+ {{ if and $remote (not $remote.Err) }}
+ {{ $result = dict "resource" $remote.Value }}
+ {{ else }}
+ {{ $result = dict "fallback_url" $path }}
+ {{ end }}
+ {{ else }}
+ {{ $normalized := $path }}
+ {{ $normalized = strings.TrimPrefix "/" $normalized }}
+ {{ $normalized = strings.TrimPrefix "./" $normalized }}
+ {{ $normalized = strings.TrimPrefix "assets/" $normalized }}
+ {{ $normalized = strings.TrimPrefix "media/" $normalized }}
+ {{ $normalized = path.Clean $normalized }}
+
+ {{ if and $normalized (ne $normalized ".") }}
+ {{ $resource := false }}
+ {{ with .Page }}
+ {{ $resource = (.Resources.ByType "image").GetMatch $normalized }}
+ {{ end }}
+ {{ if not $resource }}
+ {{ $resource = resources.Get (path.Join "media" $normalized) }}
+ {{ end }}
+ {{ if $resource }}
+ {{ $result = dict "resource" $resource }}
+ {{ else }}
+ {{ $result = dict "fallback_url" (printf "/media/%s" $normalized) }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+
+{{ return $result }}
diff --git a/modules/blox/schemas/blocks.json b/modules/blox/schemas/blocks.json
index 755d38d7..39f6919a 100644
--- a/modules/blox/schemas/blocks.json
+++ b/modules/blox/schemas/blocks.json
@@ -89,7 +89,10 @@
"announcement-badge",
"trust-strip",
"size-presets",
- "alignment"
+ "alignment",
+ "media-image",
+ "media-video",
+ "split-layouts"
],
"complexity": "simple",
"preact": true,
diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md
index 35558fb4..99a9f954 100644
--- a/templates/startup-landing-page/content/_index.md
+++ b/templates/startup-landing-page/content/_index.md
@@ -30,6 +30,13 @@ sections:
trust:
stars: 5
text: Loved by **10,000+** developers · Trusted by teams at Google, OpenAI, NVIDIA
+ # To switch to a split layout with a product mockup, uncomment the media block
+ # below and set `design.layout: split-left` (or `split-right` / `stacked`):
+ # media:
+ # type: image
+ # src: dashboard.png
+ # dark_src: dashboard-dark.png # optional dark-mode variant
+ # alt: Hugo Blox dashboard
design:
spacing:
padding: [0, 0, 0, 0]