diff --git a/modules/blox/blox/pricing/client.jsx b/modules/blox/blox/pricing/client.jsx
new file mode 100644
index 00000000..ad7b5ed3
--- /dev/null
+++ b/modules/blox/blox/pricing/client.jsx
@@ -0,0 +1,15 @@
+import {render} from "preact";
+import {PricingBlock} from "./component.jsx";
+
+function renderPricingBlocks() {
+ const blocks = document.querySelectorAll('[data-block-type="pricing"]');
+ blocks.forEach((block) => {
+ const propsData = block.dataset.props;
+ if (propsData) {
+ const props = JSON.parse(propsData);
+ render(, block);
+ }
+ });
+}
+
+renderPricingBlocks();
diff --git a/modules/blox/blox/pricing/component.jsx b/modules/blox/blox/pricing/component.jsx
new file mode 100644
index 00000000..c22336b2
--- /dev/null
+++ b/modules/blox/blox/pricing/component.jsx
@@ -0,0 +1,229 @@
+import {useState} from "preact/hooks";
+import {Icon} from "../../shared/components/Icon.jsx";
+
+const CHECK_SVG = ``;
+const X_SVG = ``;
+
+function renderText(text) {
+ if (!text) return "";
+ return String(text)
+ .replace(/\*\*(.*?)\*\*/g, "$1")
+ .replace(/\*(.*?)\*/g, "$1")
+ .replace(/`(.*?)`/g, "$1");
+}
+
+function BillingToggle({billing, setBilling, toggle}) {
+ const isYearly = billing === "yearly";
+ return (
+
+
+ {toggle.monthly_label || "Monthly"}
+
+
+
+ {toggle.yearly_label || "Yearly"}
+ {toggle.yearly_discount && (
+
+ {toggle.yearly_discount}
+
+ )}
+
+
+ );
+}
+
+function PriceDisplay({price = {}, price_suffix, price_note, price_note_monthly, price_note_yearly, billing}) {
+ const raw = billing === "yearly" && price.yearly != null ? price.yearly : price.monthly;
+ const currency = price.currency ?? "$";
+ const isContact = raw === "" || raw == null;
+ const isFree = String(raw) === "0";
+
+ // billing-aware note: per-billing field → static fallback
+ const note = billing === "yearly"
+ ? (price_note_yearly ?? price_note)
+ : (price_note_monthly ?? price_note);
+
+ if (isContact) {
+ return (
+
+
+ {price_note || "Contact us"}
+
+
+ );
+ }
+
+ if (isFree) {
+ return (
+
+
Free
+ {note &&
{note}
}
+
+ );
+ }
+
+ return (
+
+
+ {currency && {currency}}
+ {raw}
+ {price_suffix && {price_suffix}}
+
+ {note &&
{note}
}
+
+ );
+}
+
+function FeatureRow({feature}) {
+ const text = typeof feature === "string" ? feature : (feature.text ?? "");
+ const included = typeof feature === "string" ? true : (feature.included !== false);
+ const note = typeof feature === "object" ? feature.note : null;
+
+ return (
+
+
+
+ {text}
+ {note && ({note})}
+
+
+ );
+}
+
+function PricingTier({tier, billing, icon_svgs}) {
+ const {
+ name = "",
+ badge = "",
+ description = "",
+ price = {},
+ price_suffix = "",
+ price_note = "",
+ price_note_monthly,
+ price_note_yearly,
+ highlight = false,
+ cta = {},
+ features = [],
+ } = tier;
+
+ const ctaStyle = cta.style || (highlight ? "primary" : "outline");
+ const ctaIconSvg = cta.icon ? (icon_svgs?.[cta.icon] ?? null) : null;
+ const isExternalCta = cta.url && (cta.url.startsWith("http://") || cta.url.startsWith("https://"));
+
+ return (
+
+ {badge && (
+
+
+ {badge}
+
+
+ )}
+
+
+
{name}
+ {description && (
+
{description}
+ )}
+
+
+
+
+ {cta.text && cta.url && (
+
+ {cta.text}
+ {ctaIconSvg && }
+
+ )}
+
+ {features.length > 0 && (
+
+ {features.map((f, i) => )}
+
+ )}
+
+ );
+}
+
+export const PricingBlock = ({content = {}, design = {}, icon_svgs = {}}) => {
+ const {title, subtitle, billing_toggle = {}, tiers = []} = content;
+
+ const hasYearlyPrices = tiers.some(
+ (t) => t.price?.yearly != null && t.price.yearly !== t.price.monthly
+ );
+ const showToggle = billing_toggle.enabled && hasYearlyPrices;
+
+ const [billing, setBilling] = useState("monthly");
+
+ const colsClass =
+ tiers.length === 1
+ ? "max-w-sm mx-auto"
+ : tiers.length === 2
+ ? "max-w-4xl mx-auto grid-cols-1 sm:grid-cols-2"
+ : "grid-cols-1 md:grid-cols-3";
+
+ return (
+
+
+ {(title || subtitle) && (
+
+ {title && (
+
+ )}
+ {subtitle && (
+
+ )}
+
+ )}
+
+ {showToggle && (
+
+ )}
+
+ {/* pt-6 clears space for the absolutely-positioned badge on the highlighted tier */}
+
+ {tiers.map((tier, i) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/modules/blox/blox/pricing/manifest.json b/modules/blox/blox/pricing/manifest.json
new file mode 100644
index 00000000..5225a419
--- /dev/null
+++ b/modules/blox/blox/pricing/manifest.json
@@ -0,0 +1,15 @@
+{
+ "id": "pricing",
+ "name": "Pricing",
+ "version": "1.0.0",
+ "license": "MIT",
+ "category": "conversion",
+ "intent": ["convert", "inform"],
+ "verticals": ["startup", "saas", "agency", "dev-portfolio", "link-in-bio"],
+ "tags": ["pricing", "plans", "tiers", "billing", "saas", "subscription", "conversion"],
+ "description": "Display pricing tiers with optional monthly/yearly billing toggle, feature checklists, highlighted tiers, badges, and per-tier CTAs",
+ "author": "Hugo Blox",
+ "homepage": "https://hugoblox.com/blocks/",
+ "repository": "https://github.com/HugoBlox/kit",
+ "keywords": ["hugo", "static-site", "pricing", "plans", "saas", "billing"]
+}
diff --git a/modules/blox/layouts/_partials/blox/preact-wrapper.html b/modules/blox/layouts/_partials/blox/preact-wrapper.html
index 0fb41b4b..58620a60 100644
--- a/modules/blox/layouts/_partials/blox/preact-wrapper.html
+++ b/modules/blox/layouts/_partials/blox/preact-wrapper.html
@@ -148,6 +148,23 @@
{{ $props = merge $props (dict "icon_svgs" $icon_svgs) }}
{{ end }}
+{{/* Resolve CTA icons from pricing tiers (content.tiers[*].cta.icon) */}}
+{{ with $block.content.tiers }}
+ {{ range . }}
+ {{ with .cta.icon }}
+ {{ if not (index $icon_svgs .) }}
+ {{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }}
+ {{ if $icon_data }}
+ {{ $icon_svgs = merge $icon_svgs (dict . $icon_data) }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+{{ if gt (len $icon_svgs) 0 }}
+ {{ $props = merge $props (dict "icon_svgs" $icon_svgs) }}
+{{ end }}
+
{{/* Convert props to JSON for client-side rendering */}}
{{ $propsJSON := $props | jsonify }}
diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md
index 2b816536..0de88726 100644
--- a/templates/startup-landing-page/content/_index.md
+++ b/templates/startup-landing-page/content/_index.md
@@ -81,6 +81,95 @@ sections:
- name: Swappable Blocks
icon: rectangle-group
description: Build your pages with blocks - no coding required!
+ - block: pricing
+ id: pricing
+ content:
+ title: "Simple, transparent pricing"
+ subtitle: "Start for free. Upgrade when you're ready. No credit card required."
+ billing_toggle:
+ enabled: true
+ monthly_label: Monthly
+ yearly_label: Yearly
+ yearly_discount: "Save 20%"
+ tiers:
+ - name: Starter
+ description: "Perfect for personal projects and portfolios."
+ price:
+ monthly: "0"
+ yearly: "0"
+ currency: "$"
+ price_note: "Free forever"
+ highlight: false
+ cta:
+ text: Get started free
+ url: https://hugoblox.com/templates/
+ icon: hero/arrow-right
+ style: outline
+ features:
+ - text: "5 pages"
+ included: true
+ - text: "1 GB storage"
+ included: true
+ - text: "Custom domain"
+ included: false
+ - text: Analytics dashboard
+ included: false
+ - text: Priority support
+ included: false
+ - name: Pro
+ badge: Most popular
+ description: "For freelancers and teams building seriously."
+ price:
+ monthly: "19"
+ yearly: "15"
+ currency: "$"
+ price_suffix: /month
+ price_note_monthly: "Billed monthly"
+ price_note_yearly: "Billed annually"
+ highlight: true
+ cta:
+ text: Start free trial
+ url: https://hugoblox.com/templates/
+ icon: hero/arrow-right
+ style: primary
+ features:
+ - text: Unlimited pages
+ included: true
+ - text: "10 GB storage"
+ included: true
+ - text: Custom domain
+ included: true
+ - text: Analytics dashboard
+ included: true
+ - text: Priority support
+ included: false
+ - name: Business
+ description: "Scale with confidence and dedicated support."
+ price:
+ monthly: "49"
+ yearly: "39"
+ currency: "$"
+ price_suffix: /month
+ price_note_monthly: "Billed monthly"
+ price_note_yearly: "Billed annually"
+ highlight: false
+ cta:
+ text: Start free trial
+ url: https://hugoblox.com/templates/
+ icon: hero/arrow-right
+ style: outline
+ features:
+ - text: Unlimited pages
+ included: true
+ - text: "100 GB storage"
+ included: true
+ - text: Custom domain
+ included: true
+ - text: Analytics dashboard
+ included: true
+ - text: Priority support
+ included: true
+
- block: cta-image-paragraph
id: solutions
content: