diff --git a/modules/blox/blox/hero/component.jsx b/modules/blox/blox/hero/component.jsx
index b8d0bc4a..bf8eecad 100644
--- a/modules/blox/blox/hero/component.jsx
+++ b/modules/blox/blox/hero/component.jsx
@@ -8,72 +8,158 @@ import {Icon} from "../../shared/components/Icon.jsx";
// Simple markdown renderer
export function renderText(text) {
if (!text) return "";
- return text
+ return String(text)
.replace(/\*\*(.*?)\*\*/g, "$1")
.replace(/\*(.*?)\*/g, "$1")
.replace(/`(.*?)`/g, "$1");
}
+// Title renderer: supports [highlight] syntax for gradient-text spans (in addition to markdown).
+// `[word]` becomes a primary→secondary gradient span. Negative lookahead `(?!\()` avoids
+// clobbering markdown link syntax `[text](url)`.
+const HIGHLIGHT_CLS =
+ "bg-gradient-to-r from-primary-500 via-secondary-500 to-primary-500 dark:from-primary-400 dark:via-secondary-400 dark:to-primary-400 bg-clip-text text-transparent";
+
+export function renderTitle(text) {
+ if (!text) return "";
+ return String(text)
+ .replace(/\[([^\]]+)\](?!\()/g, `$1`)
+ .replace(/\*\*(.*?)\*\*/g, "$1")
+ .replace(/\*(.*?)\*/g, "$1");
+}
+
// Process URLs
export function processUrl(url) {
if (!url) return {href: "#"};
-
if (url.startsWith("http://") || url.startsWith("https://")) {
- return {
- href: url,
- target: "_blank",
- rel: "noopener",
- };
+ return {href: url, target: "_blank", rel: "noopener"};
}
-
- if (url.startsWith("#")) {
- return {href: url};
- }
-
return {href: url};
}
+// Full literal class strings (Tailwind scanner safety)
+const ACTION_STYLES = {
+ gradient:
+ "rounded-full bg-gradient-to-r from-primary-600 to-secondary-600 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/40 hover:from-primary-500 hover:to-secondary-500 transition-all duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500",
+ solid:
+ "rounded-full bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600",
+ outline:
+ "rounded-full px-6 py-3 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-gray-600 text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800/60 transition-colors",
+ ghost:
+ "rounded-full px-6 py-3 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800/60 transition-colors",
+ text: "text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors",
+};
+
+function ActionButton({action, defaultStyle, iconSvg}) {
+ if (!action?.url || !action?.text) return null;
+ const styleKey = action.style && ACTION_STYLES[action.style] ? action.style : defaultStyle;
+ const cls = ACTION_STYLES[styleKey];
+ const url = processUrl(action.url);
+ // text-style without an explicit icon shows the classic trailing arrow
+ const showTextArrow = styleKey === "text" && !iconSvg;
+
+ return (
+
+
+ {iconSvg && (
+
+
+
+ )}
+ {showTextArrow && →}
+
+ );
+}
+
+// Star strip for the trust block. Supports fractional values (e.g. 4.5 → 4 full + half).
+const STAR_FULL = ``;
+const STAR_EMPTY = ``;
+
+function StarStrip({stars}) {
+ const n = Math.max(0, Math.min(5, Number(stars) || 0));
+ const full = Math.floor(n);
+ return (
+
+ {Array.from({length: 5}).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+function TrustStrip({trust}) {
+ if (!trust) return null;
+ const {stars, text} = trust;
+ if (!stars && !text) return null;
+ return (
+
+ {stars != null && }
+ {text && }
+
+ );
+}
+
+// Announcement: pill above the title with optional leading badge.
+// announcement.badge: {text, color?} — color is one of: primary | green | amber | rose
+const BADGE_COLORS = {
+ primary: "bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300",
+ green: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
+ amber: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
+ rose: "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300",
+};
+
+function AnnouncementPill({announcement}) {
+ if (!announcement?.text) return null;
+ const badge = announcement.badge;
+ const badgeCls = badge && BADGE_COLORS[badge.color] ? BADGE_COLORS[badge.color] : BADGE_COLORS.primary;
+
+ return (
+
+ );
+}
+
// Hero Block Component - Single implementation
-export const HeroBlock = ({content, design, _id, icon_svg}) => {
+export const HeroBlock = ({content, design, _id, icon_svg, secondary_icon_svg}) => {
const paddingClasses = design?.no_padding ? "" : "py-32 sm:py-48 lg:py-56";
return (
- {/* Announcement Banner */}
- {content.announcement?.text && (
-
- )}
+
- {/* Main Content */}
+ {/* Eyebrow */}
+ {content.eyebrow && (
+
+ )}
+
{/* Title */}
{content.title && (
)}
@@ -87,50 +173,13 @@ export const HeroBlock = ({content, design, _id, icon_svg}) => {
{/* Action Buttons */}
{(content.primary_action?.url || content.secondary_action?.url) && (
-
diff --git a/modules/blox/layouts/_partials/blox/preact-wrapper.html b/modules/blox/layouts/_partials/blox/preact-wrapper.html
index b3bfac54..41ce3d6e 100644
--- a/modules/blox/layouts/_partials/blox/preact-wrapper.html
+++ b/modules/blox/layouts/_partials/blox/preact-wrapper.html
@@ -25,6 +25,14 @@
{{ end }}
{{ end }}
+{{/* Resolve icon for secondary_action (hero) */}}
+{{ with $block.content.secondary_action.icon }}
+ {{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }}
+ {{ if $icon_data }}
+ {{ $props = merge $props (dict "secondary_icon_svg" $icon_data) }}
+ {{ 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/schemas/blocks.json b/modules/blox/schemas/blocks.json
index e090bba3..8caa0bd2 100644
--- a/modules/blox/schemas/blocks.json
+++ b/modules/blox/schemas/blocks.json
@@ -78,7 +78,17 @@
"categories": ["marketing", "landing"],
"meta": {
"hugoblox": {
- "capabilities": ["headline", "subtitle", "dual-cta", "announcement-banner"],
+ "capabilities": [
+ "headline",
+ "title-highlight",
+ "eyebrow",
+ "subtitle",
+ "dual-cta",
+ "cta-styles",
+ "announcement-banner",
+ "announcement-badge",
+ "trust-strip"
+ ],
"complexity": "simple",
"preact": true,
"schemaUrl": "https://api.hugoblox.com/v1/blocks/hero/schema"
diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md
index bde96c85..35558fb4 100644
--- a/templates/startup-landing-page/content/_index.md
+++ b/templates/startup-landing-page/content/_index.md
@@ -6,20 +6,30 @@ type: landing
sections:
- block: hero
content:
- title: Build Your Landing Pages with Hugo Blox
- text: 🧱 EASY. FREE (OPEN SOURCE). NO-CODE 🧱
+ eyebrow: The open-source page builder
+ title: Build your landing page with [Hugo Blox]
+ text: Production-grade blocks. Edit in Markdown and YAML. Deploy anywhere — for free.
primary_action:
text: Get Started
url: https://hugoblox.com/templates/
icon: rocket-launch
+ style: gradient
secondary_action:
text: Read the docs
url: https://docs.hugoblox.com
+ icon: book-open
+ style: ghost
announcement:
- text: "Announcing the release of version 1."
+ badge:
+ text: NEW
+ color: primary
+ text: Hugo Blox v1 is here.
link:
- text: "Read more"
+ text: Read more
url: "/blog/"
+ trust:
+ stars: 5
+ text: Loved by **10,000+** developers · Trusted by teams at Google, OpenAI, NVIDIA
design:
spacing:
padding: [0, 0, 0, 0]