From dc14b48b42a2c8e723cd6001b0ebe43027b82e9a Mon Sep 17 00:00:00 2001 From: George Cushen Date: Sat, 2 May 2026 17:03:36 +0100 Subject: [PATCH] feat(faq): migrate FAQ block from Go HTML to Preact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces block.html with a Preact component featuring smooth CSS grid 0fr→1fr height animation, icon rotation, and multi-paragraph answer support via renderRich(). Single-open accordion with question/title and answer/text field aliases for backward compatibility. --- .../blox/blox/comparison-table/component.jsx | 32 ++-- modules/blox/blox/cta-card/component.jsx | 67 +++---- modules/blox/blox/faq/block.html | 152 ---------------- modules/blox/blox/faq/client.jsx | 15 ++ modules/blox/blox/faq/component.jsx | 127 +++++++++++++ modules/blox/blox/faq/manifest.json | 18 +- modules/blox/blox/features/component.jsx | 142 +++++++++++---- modules/blox/blox/stats/component.jsx | 27 ++- .../_partials/functions/parse_block_v3.html | 29 ++- .../config/_default/menus.yaml | 20 ++- .../startup-landing-page/content/_index.md | 169 ++++++++++++++---- 11 files changed, 514 insertions(+), 284 deletions(-) delete mode 100644 modules/blox/blox/faq/block.html create mode 100644 modules/blox/blox/faq/client.jsx create mode 100644 modules/blox/blox/faq/component.jsx diff --git a/modules/blox/blox/comparison-table/component.jsx b/modules/blox/blox/comparison-table/component.jsx index a7baf9cf..3d090329 100644 --- a/modules/blox/blox/comparison-table/component.jsx +++ b/modules/blox/blox/comparison-table/component.jsx @@ -95,10 +95,10 @@ function DesktopTable({competitors, rows, cta, icon_svgs, rowStriping = false}) {/* Card — overflow-visible so the highlight ring can extend above its top edge */}
- {/* Highlight column — ring + flat tint behind cells. Lives INSIDE the card so it paints on top of card bg, but BEFORE the table so cell content sits on top. */} + {/* Highlight column FILL — paints BEFORE table, behind cells. Cell content sits on top. */} {highlightIdx >= 0 && (
)} @@ -162,21 +162,25 @@ function DesktopTable({competitors, rows, cta, icon_svgs, rowStriping = false}) dataIdx++; const values = row.values ?? []; const isOdd = rowStriping && dataIdx % 2 === 1; - const rowBg = row.highlight - ? "bg-primary-100/50 dark:bg-primary-900/25" - : isOdd - ? "bg-gray-50/70 dark:bg-gray-800/30" - : ""; + + // Row-highlight tints the whole row (intentional — the row is the focus). + // Zebra stripes apply per-cell so cells inside the highlight column can opt out + // (otherwise the stripe washes out the column's brand tint). + const cellBg = (ci) => { + if (row.highlight) return "bg-primary-100/50 dark:bg-primary-900/25"; + if (isOdd && ci !== highlightIdx) return "bg-gray-50/70 dark:bg-gray-800/30"; + return ""; + }; return ( - - + +
{row.feature}
{row.note && (
{row.note}
)} {competitors.map((_, ci) => ( - + ))} @@ -206,6 +210,14 @@ function DesktopTable({competitors, rows, cta, icon_svgs, rowStriping = false}) )} + + {/* Highlight column OUTLINE — paints AFTER the table so row stripes/tints can't interrupt the violet ring. Outline only, no fill (fill is handled by the layer behind the table). */} + {highlightIdx >= 0 && ( +
+ )}
); diff --git a/modules/blox/blox/cta-card/component.jsx b/modules/blox/blox/cta-card/component.jsx index 25ee8863..58c927c2 100644 --- a/modules/blox/blox/cta-card/component.jsx +++ b/modules/blox/blox/cta-card/component.jsx @@ -28,31 +28,45 @@ export const CtaCardBlock = ({content, design, _id, button_icon_svg}) => { if (!Number.isNaN(val)) overlayOpacity = Math.max(0, Math.min(1, val)); } - // Text Color Logic + // Text Color Logic — three modes: + // - "light": white title + white/80 body, white pill button (use on coloured/dark cards) + // - "dark": dark title + dark body, dark pill button (use on white/light cards) + // - "auto": detect from card classes — colored gradient or saturated bg → light, else fall back + // + // All output classes must be FULL LITERAL STRINGS so Tailwind's source scanner picks them up. let textColorMode = "auto"; if (["light", "dark", "auto"].includes(card.text_color?.toLowerCase())) { textColorMode = card.text_color.toLowerCase(); } - let textClasses = "text-gray-900 dark:text-white"; - let buttonClasses = "text-gray-900 dark:text-gray-100"; - - // Auto-detect based on background gradient - const bg = design?.background || {}; - const gradientStart = bg.gradient?.start || ""; - const isDarkBg = ["primary-7", "primary-8", "primary-9", "secondary-7", "secondary-8", "secondary-9"].some((p) => gradientStart.startsWith(p)); - - if (textColorMode === "light") { - textClasses = "text-white"; - buttonClasses = "text-gray-900 dark:text-white"; - } else if (textColorMode === "dark") { - textClasses = "text-gray-900 dark:text-white"; - buttonClasses = "text-gray-900 dark:text-white"; - } else if (isDarkBg) { - textClasses = "text-white"; - buttonClasses = "text-gray-900"; + if (textColorMode === "auto") { + const looksColored = + cardClass.includes("bg-gradient") || + /\bbg-(primary|secondary|violet|indigo|blue|purple|pink|fuchsia|rose|red|orange|amber|emerald|teal|cyan|sky)-[4-9]\d{0,2}\b/.test(cardClass) || + /\bfrom-(primary|secondary|violet|indigo|blue|purple|pink|fuchsia|rose|red|orange|amber|emerald|teal|cyan|sky)-[4-9]/.test(cardClass); + // Legacy detection: design.background.gradient.start using saturated Tailwind tokens + const bg = design?.background || {}; + const gradientStart = bg.gradient?.start || ""; + const legacyDarkBg = ["primary-7", "primary-8", "primary-9", "secondary-7", "secondary-8", "secondary-9"].some((p) => gradientStart.startsWith(p)); + textColorMode = (looksColored || legacyDarkBg) ? "light" : "dark"; } + // Title, body, and button classes — full literals only + const titleClasses = textColorMode === "light" + ? "text-white" + : "text-gray-900 dark:text-white"; + const bodyClasses = textColorMode === "light" + ? "text-white/80" + : "text-gray-700 dark:text-gray-300"; + // Button: in "light" mode (coloured card), keep a white pill across both themes for consistency. + // In "dark" mode (light card), use a dark pill. + const buttonBgClasses = textColorMode === "light" + ? "bg-white ring-1 ring-white/40 hover:bg-white/95 hover:ring-white/60 shadow-lg" + : "bg-gray-900 dark:bg-white ring-1 ring-gray-900/10 dark:ring-white/10 hover:bg-gray-800 dark:hover:bg-gray-100 shadow-lg"; + const buttonTextClasses = textColorMode === "light" + ? "text-gray-900" + : "text-white dark:text-gray-900"; + // Button Logic const button = content?.button || {}; const buttonText = button.text?.trim(); @@ -85,7 +99,7 @@ export const CtaCardBlock = ({content, design, _id, button_icon_svg}) => { {/* Title */} {content.title && (

)} @@ -93,7 +107,7 @@ export const CtaCardBlock = ({content, design, _id, button_icon_svg}) => { {/* Text */} {content.text && (
)} @@ -105,21 +119,14 @@ export const CtaCardBlock = ({content, design, _id, button_icon_svg}) => { href={buttonUrl} target={isNewTab ? "_blank" : undefined} rel={isNewTab ? "noopener" : undefined} - class={`group relative inline-flex items-center gap-3 px-8 py-4 text-lg font-semibold ${buttonClasses} transition-all duration-300 ease-out hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/50 dark:focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent`} + class={`group inline-flex items-center gap-3 rounded-2xl px-8 py-4 text-lg font-semibold ${buttonBgClasses} ${buttonTextClasses} transition-all duration-300 ease-out hover:scale-105 hover:shadow-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent`} > - {/* Glassmorphism Background */} -
- - {/* Button Content */} - {buttonText} + {buttonText} {button_icon_svg && ( - + )} - - {/* Glow Effect */} -
)} diff --git a/modules/blox/blox/faq/block.html b/modules/blox/blox/faq/block.html deleted file mode 100644 index 97099d91..00000000 --- a/modules/blox/blox/faq/block.html +++ /dev/null @@ -1,152 +0,0 @@ -{{- $wcPage := .wcPage -}} -{{- $wcBlock := .wcBlock -}} -{{- $wcIdentifier := .wcIdentifier -}} -{{- $content := $wcBlock.content -}} -{{- if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end -}} -{{- $design := $wcBlock.design -}} -{{- if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end -}} - -{{/* Enable Alpine.js for interactive accordion */}} -{{ $wcPage.Page.Store.Set "has_alpine" true }} - -{{- $st := index $design "spacing" -}} -{{- if not (reflect.IsMap $st) }}{{ $st = dict }}{{ end -}} -{{- $pad := slice "20px" "0" "20px" "0" -}} -{{- $pad_raw := index $st "padding" -}} -{{- if and (reflect.IsSlice $pad_raw) (ge (len $pad_raw) 4) }}{{ $pad = $pad_raw }}{{ end -}} -{{- $pad_sm := slice "3rem" "0" -}} -{{- $pad_sm_raw := index $st "padding_sm" -}} -{{- if and (reflect.IsSlice $pad_sm_raw) (ge (len $pad_sm_raw) 2) }}{{ $pad_sm = $pad_sm_raw }}{{ end -}} -{{- $mar := slice "0" "0" "0" "0" -}} -{{- $mar_raw := index $st "margin" -}} -{{- if and (reflect.IsSlice $mar_raw) (ge (len $mar_raw) 4) }}{{ $mar = $mar_raw }}{{ end -}} - -{{- $bg := index $design "background" -}} -{{- if not (reflect.IsMap $bg) }}{{ $bg = dict }}{{ end -}} -{{- $bg_color_val := "" -}} -{{- $bg_color_raw := index $bg "color" -}} -{{- if eq (printf "%T" $bg_color_raw) "string" }}{{ $bg_color_val = $bg_color_raw }}{{ end -}} - -{{- $title := "" -}} -{{- $title_raw := index $content "title" -}} -{{- if eq (printf "%T" $title_raw) "string" }}{{ $title = $title_raw }}{{ end -}} -{{- $subtitle := "" -}} -{{- $subtitle_raw := index $content "subtitle" -}} -{{- if eq (printf "%T" $subtitle_raw) "string" }}{{ $subtitle = $subtitle_raw }}{{ end -}} -{{- $text := "" -}} -{{- $text_raw := index $content "text" -}} -{{- if eq (printf "%T" $text_raw) "string" }}{{ $text = $text_raw }}{{ end -}} - -{{- $items_raw := index $content "items" -}} -{{- $items := slice -}} -{{- if reflect.IsSlice $items_raw }}{{ $items = $items_raw }}{{ end -}} - -{{- $button := index $content "button" -}} -{{- if not (reflect.IsMap $button) }}{{ $button = dict }}{{ end -}} -{{- $button_text := "" -}} -{{- with index $button "text" }}{{ $button_text = strings.TrimSpace (printf "%v" .) }}{{ end -}} -{{- $button_url := "" -}} -{{- with index $button "url" }}{{ $button_url = strings.TrimSpace (printf "%v" .) }}{{ end -}} -{{- $button_icon := "" -}} -{{- with index $button "icon" }}{{ $button_icon = printf "%v" . }}{{ end -}} - -
-
- - {{- with $title -}} -
-

- {{ . | markdownify | emojify }} -

- {{- with $subtitle -}} -

- {{ . | markdownify | emojify }} -

- {{- end -}} -
- {{- end -}} - - {{- with $text -}} -
- {{ . | markdownify | emojify }} -
- {{- end -}} - - {{- if gt (len $items) 0 -}} -
-
- {{- range $index, $item := $items -}} - {{- if reflect.IsMap $item -}} - {{- $question := "" -}} - {{- $question_raw := index $item "question" | default (index $item "title") -}} - {{- if ne $question_raw nil }}{{ $question = printf "%v" $question_raw }}{{ end -}} - {{- $answer := "" -}} - {{- $answer_raw := index $item "answer" | default (index $item "text") -}} - {{- if ne $answer_raw nil }}{{ $answer = printf "%v" $answer_raw }}{{ end -}} - {{- if or $question $answer -}} -
- -
-
- {{ $answer | markdownify | emojify }} -
-
-
- {{- end -}} - {{- end -}} - {{- end -}} -
-
- {{- end -}} - - {{- if and $button_text $button_url -}} - {{- $link := $button_url -}} - {{- $target := "" -}} - {{- $scheme := (urls.Parse $link).Scheme -}} - {{- if not $scheme -}} - {{- if not (hasPrefix $link "#") -}} - {{- $link = $link | relLangURL -}} - {{- end -}} - {{- if eq (path.Ext $link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end -}} - {{- else if in (slice "http" "https") $scheme -}} - {{- $target = "target=\"_blank\" rel=\"noopener\"" -}} - {{- end -}} - - {{- end -}} - -
-
diff --git a/modules/blox/blox/faq/client.jsx b/modules/blox/blox/faq/client.jsx new file mode 100644 index 00000000..b38416c0 --- /dev/null +++ b/modules/blox/blox/faq/client.jsx @@ -0,0 +1,15 @@ +import {render} from "preact"; +import {FaqBlock} from "./component.jsx"; + +function renderFaqBlocks() { + const blocks = document.querySelectorAll('[data-block-type="faq"]'); + blocks.forEach((block) => { + const propsData = block.dataset.props; + if (propsData) { + const props = JSON.parse(propsData); + render(, block); + } + }); +} + +renderFaqBlocks(); diff --git a/modules/blox/blox/faq/component.jsx b/modules/blox/blox/faq/component.jsx new file mode 100644 index 00000000..ce239696 --- /dev/null +++ b/modules/blox/blox/faq/component.jsx @@ -0,0 +1,127 @@ +import {useState} from "preact/hooks"; +import {Icon} from "../../shared/components/Icon.jsx"; + +const PLUS_SVG = ``; + +function renderInline(text) { + if (!text) return ""; + return String(text) + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, '$1') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); +} + +function renderRich(text) { + if (!text) return ""; + const paras = String(text) + .split(/\n\s*\n/) + .map((p) => p.trim()) + .filter(Boolean) + .map((p, i) => ` 0 ? ' class="mt-3"' : ""}>${renderInline(p)}

`); + return paras.join(""); +} + +function FaqItem({item, isOpen, onToggle}) { + const question = item.question ?? item.title ?? ""; + const answer = item.answer ?? item.text ?? ""; + + return ( +
+ + + {/* CSS grid trick: grid-rows transition from 0fr → 1fr animates from 0 height to natural height */} +
+
+
+
+
+
+ ); +} + +export const FaqBlock = ({content = {}, icon_svgs = {}, button_icon_svg}) => { + const {title, subtitle, text, items = [], button = {}} = content; + const [openIdx, setOpenIdx] = useState(null); + + const buttonIconSvg = button_icon_svg ?? (button.icon ? icon_svgs?.[button.icon] : null); + const isExtBtn = button.url && (button.url.startsWith("http://") || button.url.startsWith("https://")); + + return ( +
+
+ {(title || subtitle) && ( +
+ {subtitle && ( +

+ {subtitle} +

+ )} + {title && ( +

+ )} +

+ )} + + {text && ( +
+ )} + + {items.length > 0 && ( +
+ {items.map((item, i) => ( + setOpenIdx(openIdx === i ? null : i)} + /> + ))} +
+ )} + + {button.text && button.url && ( + + )} +
+
+ ); +}; diff --git a/modules/blox/blox/faq/manifest.json b/modules/blox/blox/faq/manifest.json index ce926498..36d4e895 100644 --- a/modules/blox/blox/faq/manifest.json +++ b/modules/blox/blox/faq/manifest.json @@ -1,8 +1,16 @@ { - "name": "faq", - "version": "1", - "title": "FAQ", - "description": "Display frequently asked questions in an accordion format with SEO-optimized structured data", - "categories": ["content", "interactive"], + "id": "faq", + "name": "FAQ", + "version": "2.0.0", + "license": "MIT", + "category": "content", + "intent": ["inform", "convert"], + "verticals": ["startup", "saas", "agency", "academic-cv", "lab", "dev-portfolio", "help"], + "tags": ["faq", "questions", "help", "support", "objections", "accordion"], + "description": "Frequently asked questions accordion. Smooth height-animated expand, single-open behavior, Markdown answers (bold, italic, code, links), optional CTA button. Use to handle objections at the decision point — common placement is between testimonials and the final CTA.", + "author": "Hugo Blox", + "homepage": "https://hugoblox.com/blocks/", + "repository": "https://github.com/HugoBlox/kit", + "keywords": ["hugo", "faq", "accordion", "questions"], "preview": "preview.svg" } diff --git a/modules/blox/blox/features/component.jsx b/modules/blox/blox/features/component.jsx index b9d80cdc..20b618e2 100644 --- a/modules/blox/blox/features/component.jsx +++ b/modules/blox/blox/features/component.jsx @@ -1,62 +1,140 @@ -/** - * Features Block Component - Single source of truth - * Responsive grid of feature items with icon, name, and description - */ - import {Icon} from "../../shared/components/Icon.jsx"; -// Simple markdown renderer function renderText(text) { if (!text) return ""; return String(text) .replace(/\*\*(.*?)\*\*/g, "$1") .replace(/\*(.*?)\*/g, "$1") - .replace(/`(.*?)`/g, "$1"); + .replace(/`(.*?)`/g, '$1'); } -function FeatureItem({item, iconSvg}) { +function FeatureCard({item, iconSvg, imgData, variant = "grid", large = false}) { + const isCard = variant === "bento"; + const wrapperCls = isCard + ? `relative h-full rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800/50 ${large ? "p-8 lg:p-10" : "p-6"} hover:ring-primary-300 dark:hover:ring-primary-600 hover:shadow-lg transition-all duration-300 overflow-hidden` + : ""; + + const iconWrapper = large + ? "w-14 h-14 lg:w-16 lg:h-16" + : "w-11 h-11 lg:w-12 lg:h-12"; + const iconSize = large + ? "height:1.75rem;width:auto" + : "height:1.4rem;width:auto"; + + const titleSize = large + ? "text-2xl lg:text-3xl" + : isCard ? "text-lg lg:text-xl" : "text-xl"; + const descSize = large ? "text-base lg:text-lg" : "text-sm lg:text-base"; + return ( -
+
{iconSvg && ( -
- +
+ +
+ )} + {item.name && ( +

+ )} + {item.description && ( +

+ )} + {large && imgData?.src && ( +

+ {item.name
)} - {item.name &&

} - {item.description &&

}

); } -export const FeaturesBlock = ({content, _design, _id, icon_svgs}) => { - const title = content?.title; - const text = content?.text; - const items = Array.isArray(content?.items) ? content.items : []; - const iconMap = icon_svgs || {}; +function GridLayout({items, iconMap, item_images}) { + return ( +
+ {items.map((item, idx) => ( + + ))} +
+ ); +} + +function BentoLayout({items, iconMap, item_images}) { + // First item is the featured "large" card spanning 2 cols (md+) and 2 rows (lg+). + // grid-flow-dense lets remaining items pack the gaps cleanly across counts (4–7). + return ( +
+ {items.map((item, idx) => { + const large = idx === 0; + const spanCls = large ? "md:col-span-2 lg:row-span-2" : ""; + return ( +
+ +
+ ); + })} +
+ ); +} + +export const FeaturesBlock = ({content = {}, design = {}, icon_svgs = {}, item_images = {}}) => { + const {title, subtitle, text, items: rawItems = []} = content; + const items = Array.isArray(rawItems) ? rawItems : []; + const layout = design.layout || "grid"; return ( -
-
- {/* Section Header */} - {(title || text) && ( -
+
+
+ {(title || text || subtitle) && ( +
+ {subtitle && ( +

+ {subtitle} +

+ )} {title && (

)} - {text &&

} + {text && ( +

+ )}

)} - {/* Feature Grid */} -
- {items.map((item, idx) => ( - - ))} -
+ {items.length > 0 && ( + layout === "bento" + ? + : + )}
-
+
); }; diff --git a/modules/blox/blox/stats/component.jsx b/modules/blox/blox/stats/component.jsx index e7fea6af..93936a40 100644 --- a/modules/blox/blox/stats/component.jsx +++ b/modules/blox/blox/stats/component.jsx @@ -117,8 +117,14 @@ function gridColsClass(count) { return "lg:grid-cols-3"; } +// Gradient text uses bg-clip-text so colors come from the gradient utilities +// (full classes for Tailwind's source scanner to pick up at build time) +const GRADIENT_NUMBER_CLS = "bg-gradient-to-br from-primary-600 via-primary-500 to-secondary-500 dark:from-primary-400 dark:via-primary-300 dark:to-secondary-400 bg-clip-text text-transparent"; +const SOLID_NUMBER_CLS = "text-gray-900 dark:text-white"; + // ─── Stat Item (Cards) ─────────────────────────────────────────────── -function StatCard({item, iconSvg}) { +function StatCard({item, iconSvg, numbersGradient}) { + const numberCls = numbersGradient ? GRADIENT_NUMBER_CLS : SOLID_NUMBER_CLS; return (
{/* Hover gradient */} @@ -135,7 +141,7 @@ function StatCard({item, iconSvg}) { {/* Statistic */}

@@ -155,7 +161,8 @@ function StatCard({item, iconSvg}) { } // ─── Stat Item (Compact) ───────────────────────────────────────────── -function StatCompact({item, iconSvg}) { +function StatCompact({item, iconSvg, numbersGradient}) { + const numberCls = numbersGradient ? GRADIENT_NUMBER_CLS : SOLID_NUMBER_CLS; return (
{iconSvg && ( @@ -165,7 +172,7 @@ function StatCompact({item, iconSvg}) { )}

@@ -179,7 +186,8 @@ function StatCompact({item, iconSvg}) { } // ─── Stat Item (Minimal) ───────────────────────────────────────────── -function StatMinimal({item, iconSvg}) { +function StatMinimal({item, iconSvg, numbersGradient}) { + const numberCls = numbersGradient ? GRADIENT_NUMBER_CLS : SOLID_NUMBER_CLS; return (
{iconSvg && ( @@ -189,7 +197,7 @@ function StatMinimal({item, iconSvg}) { )}

@@ -208,6 +216,7 @@ export const StatsBlock = ({content, design, _id, icon_svgs}) => { const text = content?.text; const items = Array.isArray(content?.items) ? content.items : []; const layout = (design?.layout || "cards").toLowerCase(); + const numbersGradient = design?.numbers_gradient === true; const count = Math.max(1, Math.min(items.length, 4)); const lgCols = gridColsClass(count); @@ -236,7 +245,7 @@ export const StatsBlock = ({content, design, _id, icon_svgs}) => { {layout === "cards" && (
{items.map((item, idx) => ( - + ))}
)} @@ -246,7 +255,7 @@ export const StatsBlock = ({content, design, _id, icon_svgs}) => {
{items.map((item, idx) => ( - + ))}
@@ -256,7 +265,7 @@ export const StatsBlock = ({content, design, _id, icon_svgs}) => { {layout === "minimal" && (
{items.map((item, idx) => ( - + ))}
)} diff --git a/modules/blox/layouts/_partials/functions/parse_block_v3.html b/modules/blox/layouts/_partials/functions/parse_block_v3.html index d0c222c8..e91a7f56 100644 --- a/modules/blox/layouts/_partials/functions/parse_block_v3.html +++ b/modules/blox/layouts/_partials/functions/parse_block_v3.html @@ -152,12 +152,20 @@ {{ $gradient_mesh_html = $mesh_html | safeHTML }} {{ end }} -{{/* Legacy simple gradient support */}} +{{/* Linear or radial gradient (radial added for 2026 hero spotlight pattern) */}} {{ if and $bg.gradient.start $bg.gradient.end }} - {{ $angle := string $bg.gradient.direction | default "135" }} {{ $start_color := $bg.gradient.start }} {{ $end_color := $bg.gradient.end }} - {{ $gradient_layer = printf "linear-gradient(%sdeg, %s, %s)" $angle $start_color $end_color }} + {{ $gradient_type := $bg.gradient.type | default "linear" }} + {{ if eq $gradient_type "radial" }} + {{ $shape := $bg.gradient.shape | default "ellipse" }} + {{ $position := $bg.gradient.position | default "center" }} + {{ $size := $bg.gradient.size | default "farthest-corner" }} + {{ $gradient_layer = printf "radial-gradient(%s %s at %s, %s, %s)" $shape $size $position $start_color $end_color }} + {{ else }} + {{ $angle := string $bg.gradient.direction | default "135" }} + {{ $gradient_layer = printf "linear-gradient(%sdeg, %s, %s)" $angle $start_color $end_color }} + {{ end }} {{ end }} {{/* Process video backgrounds */}} @@ -263,6 +271,21 @@ {{ $style = printf "%sclip-path: %s;" $style . }} {{ end }} +{{/* Section break: mask-image fade for smooth transitions between sections */}} +{{ with $block.design.section_break }} + {{ $mask := "" }} + {{ if eq . "fade-top" }} + {{ $mask = "linear-gradient(to bottom, transparent 0%, black 6rem, black 100%)" }} + {{ else if eq . "fade-bottom" }} + {{ $mask = "linear-gradient(to bottom, black 0%, black calc(100% - 6rem), transparent 100%)" }} + {{ else if eq . "fade-both" }} + {{ $mask = "linear-gradient(to bottom, transparent 0%, black 6rem, black calc(100% - 6rem), transparent 100%)" }} + {{ end }} + {{ with $mask }} + {{ $style = printf "%s-webkit-mask-image: %s; mask-image: %s;" $style . . }} + {{ end }} +{{ end }} + {{ $hash_id := $block.id | default (printf "section-%s" (replace $block_type "." "-")) }} {{ $css_classes := $block.design.css_class | default "" }} {{ $widget_class := printf "blox-%s" (strings.ReplacePairs $block_type "." "-" "_" "-") }} diff --git a/templates/startup-landing-page/config/_default/menus.yaml b/templates/startup-landing-page/config/_default/menus.yaml index e53b697c..37d7a08d 100644 --- a/templates/startup-landing-page/config/_default/menus.yaml +++ b/templates/startup-landing-page/config/_default/menus.yaml @@ -9,11 +9,14 @@ main: - name: Features url: /#features weight: 20 - - name: Solutions - url: /#solutions + - name: Compare + url: /#compare + weight: 25 + - name: Pricing + url: /#pricing weight: 30 - - name: Changelog - url: /changelog/ + - name: FAQ + url: /#faq weight: 35 - name: Resources weight: 40 @@ -21,6 +24,9 @@ main: - parent: Resources name: Blog url: blog/ + - parent: Resources + name: Changelog + url: /changelog/ # Footer links (4-column layout) # Top-level items become column headings, children become links. @@ -37,8 +43,12 @@ footer: weight: 20 - parent: Product name: Pricing - url: "#" + url: /#pricing weight: 30 + - parent: Product + name: Compare + url: /#compare + weight: 35 - parent: Product name: Changelog url: /changelog/ diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md index 5d057cf8..d11c17a8 100644 --- a/templates/startup-landing-page/content/_index.md +++ b/templates/startup-landing-page/content/_index.md @@ -24,18 +24,28 @@ sections: spacing: padding: [0, 0, 0, 0] margin: [0, 0, 0, 0] - # For full-screen, add `min-h-screen` below css_class: "dark" + section_break: fade-bottom background: - color: "navy" - image: - # Add your image background to `assets/media/`. - filename: bg-triangles.svg - filters: - brightness: 0.5 - size: cover - position: center - parallax: false + # Deep navy base; radial glow paints a soft violet spotlight from above + color: "#0a0e27" + gradient: + type: radial + start: "rgba(124,58,237,0.45)" + end: "transparent" + position: "50% -10%" + shape: ellipse + size: "80% 80%" + # Subtle mesh orbs add depth without competing with the headline + gradient_mesh: + enable: true + style: orbs + intensity: subtle + animation: pulse + colors: ["primary-500/15", "secondary-500/15"] + orb_count: 2 + positions: ["top-1/4 left-1/4", "bottom-1/4 right-1/4"] + sizes: ["w-[28rem] h-[28rem]", "w-[24rem] h-[24rem]"] - block: logos content: title: Trusted by teams at @@ -81,11 +91,11 @@ sections: Discord community for support design: - # Section background color (CSS class) - css_class: "bg-gray-100 dark:bg-gray-900" - # Reduce spacing + layout: minimal + numbers_gradient: true + css_class: "bg-white dark:bg-gray-900" spacing: - padding: ["1rem", 0, "1rem", 0] + padding: ["3rem", 0, "3rem", 0] - block: steps content: title: Get started in minutes @@ -108,27 +118,83 @@ sections: - block: features id: features content: - title: Features - text: Build your site with blocks 🧱 + subtitle: Why Hugo Blox + title: Build sites in blocks, not code + text: Production-grade page-building blocks. Edit in Markdown and YAML. Deploy anywhere. items: - - name: Optimized SEO - icon: magnifying-glass - description: Automatic sitemaps, RSS feeds, and rich metadata take the pain out of SEO and syndication. - - name: Fast - icon: bolt - description: Super fast page load with Tailwind CSS and super fast site building with Hugo. - - name: Easy - icon: sparkles - description: One-click deployment to GitHub Pages. Have your new website live within 5 minutes! - - name: No-Code - icon: code-bracket - description: Edit and design your site just using rich text (Markdown) and configurable YAML parameters. - - name: Highly Rated - icon: star - description: Rated 5-stars by the community. - - name: Swappable Blocks + - name: Swappable blocks icon: rectangle-group - description: Build your pages with blocks - no coding required! + description: Compose pages from a curated catalog of pre-designed sections — heroes, pricing, FAQs, comparison tables, testimonials, and more. Mix, match, and customise without touching the underlying HTML. + - name: Lightning fast + icon: bolt + description: Sub-3-second builds and 100/100 Lighthouse scores by default — Tailwind CSS plus Hugo's native speed. + - name: No-code editing + icon: code-bracket + description: Edit content in Markdown and configure with readable YAML. No JavaScript required. + - name: SEO out of the box + icon: magnifying-glass + description: Automatic sitemaps, RSS feeds, JSON-LD, and rich metadata — SEO and syndication handled. + - name: Deploy anywhere + icon: rocket-launch + description: Push to GitHub and deploy to Pages, Netlify, Vercel, or Cloudflare in minutes. + - name: Loved by the community + icon: star + description: 10,000+ GitHub stars and 3,000+ Discord members. Rated 5-stars by the community. + design: + layout: bento + css_class: "bg-gray-50 dark:bg-gray-900/50" + - block: comparison-table + id: compare + content: + subtitle: How we compare + title: Why teams switch to Hugo Blox + text: We picked the categories that matter to teams shipping today. + competitors: + - name: Hugo Blox + tagline: Open-source page builder + highlight: true + badge: Recommended + - name: WordPress + tagline: Self-hosted CMS + - name: Squarespace + tagline: Hosted website builder + - name: Webflow + tagline: Visual web builder + rows: + - feature: Performance + category: true + - feature: Build time + note: 100 pages, cold cache + values: ["<3s", "60s+", "—", "20s+"] + - feature: Lighthouse score + values: ["100", "70", "90", "95"] + - feature: Cost & ownership + category: true + - feature: Free tier + values: [true, true, false, false] + - feature: Self-hosted + values: [true, true, false, false] + - feature: Source code access + values: [true, true, false, false] + highlight: true + - feature: Monthly cost + values: ["Free", "$5+ hosting", "$23/mo", "$29/mo"] + - feature: Developer experience + category: true + - feature: Git-based content + values: [true, false, false, false] + - feature: AI site builder + values: [true, false, false, "partial"] + - feature: Custom code + values: [true, true, "limited", "limited"] + cta: + text: Start free + url: https://hugoblox.com/templates/ + icon: hero/arrow-right + design: + row_striping: true + css_class: "bg-gray-50 dark:bg-gray-900/50" + - block: pricing id: pricing content: @@ -263,16 +329,43 @@ sections: spacing: # Reduce bottom spacing so the testimonial appears vertically centered between sections padding: ["6rem", 0, 0, 0] + + - block: faq + id: faq + content: + title: Frequently asked questions + subtitle: Everything you need to know before getting started. + items: + - question: Do I need to know how to code? + answer: | + No. Hugo Blox is designed for everyone — edit your site in plain Markdown and YAML. If you do know code, every block is open source and customisable down to the CSS. + - question: Where is my site hosted? + answer: | + Anywhere you like. Hugo Blox builds a static site you can deploy to GitHub Pages, Netlify, Vercel, Cloudflare Pages, or any static host — many on a free tier. + - question: Can I migrate from WordPress, Squarespace, or Webflow? + answer: | + Yes. Your content lives in Markdown files, so you own it and can move it freely. We provide migration guides for the most common platforms. + - question: Is it really free? + answer: | + The framework, blocks, and starter templates are MIT-licensed and free forever. Premium templates and the AI site builder are optional paid upgrades that fund continued development. + - question: Can I use my own domain? + answer: | + Yes — every host we recommend (GitHub Pages, Netlify, Vercel, Cloudflare) supports custom domains for free. + - question: How does the AI site builder work? + answer: | + Describe what you want and the AI assembles a full site from our block catalog — copy, layout, theme, and content scaffolding included. Everything generated is yours to edit, version, and deploy. + - block: cta-card content: - title: Build your future-proof website - text: As easy as 1, 2, 3! + title: Ready to ship your site? + text: Free, open source, and production-grade. Deploy your first site in minutes. button: - text: Get Started + text: Get Started Free url: https://hugoblox.com/templates/ design: + section_break: fade-top card: - # Card background color (CSS class) - css_class: "bg-primary-300" + # Brand-coloured card with subtle gradient depth + css_class: "bg-gradient-to-br from-primary-500 via-primary-600 to-secondary-600 text-white shadow-2xl" css_style: "" ---