diff --git a/modules/blox/blox/logos/client.jsx b/modules/blox/blox/logos/client.jsx
new file mode 100644
index 00000000..80fe3b13
--- /dev/null
+++ b/modules/blox/blox/logos/client.jsx
@@ -0,0 +1,15 @@
+import {render} from "preact";
+import {LogosBlock} from "./component.jsx";
+
+function renderLogosBlocks() {
+ const blocks = document.querySelectorAll('[data-block-type="logos"]');
+ blocks.forEach((block) => {
+ const propsData = block.dataset.props;
+ if (propsData) {
+ const props = JSON.parse(propsData);
+ render(, block);
+ }
+ });
+}
+
+renderLogosBlocks();
diff --git a/modules/blox/blox/logos/component.jsx b/modules/blox/blox/logos/component.jsx
new file mode 100644
index 00000000..f04da436
--- /dev/null
+++ b/modules/blox/blox/logos/component.jsx
@@ -0,0 +1,207 @@
+import {Icon} from "../../shared/components/Icon.jsx";
+
+const MARQUEE_CSS = `
+@keyframes hbx-logos-marquee {
+ from { transform: translateX(0); }
+ to { transform: translateX(-50%); }
+}
+.hbx-logos-track {
+ animation: hbx-logos-marquee var(--hbx-marquee-dur, 30s) linear infinite;
+ will-change: transform;
+}
+.hbx-logos-track:hover { animation-play-state: paused; }
+`;
+
+// Slot widths keep every logo at a consistent visual footprint regardless of SVG aspect ratio
+const SLOT_W = {sm: "w-24", md: "w-32", lg: "w-40"};
+// Max-width caps very wide SVGs inside their slot (height × ~2.5)
+const MAX_W = {sm: "3.5rem", md: "5rem", lg: "7rem"};
+// Heights passed as inline style so Icon.jsx's auto-sizing fires correctly
+const HEIGHT = {sm: "1.75rem", md: "2.5rem", lg: "3.5rem"};
+
+// Full-string Tailwind filter classes — no dynamic concatenation
+const FILTER = {
+ grayscale: "grayscale opacity-60 hover:grayscale-0 hover:opacity-100 transition-all duration-300",
+ white: "brightness-0 invert transition-all duration-300",
+ color: "transition-all duration-300",
+};
+
+function renderText(text) {
+ if (!text) return "";
+ return String(text)
+ .replace(/\*\*(.*?)\*\*/g, "$1")
+ .replace(/\*(.*?)\*/g, "$1");
+}
+
+function LogoItem({item, idx, logoStyle, logoSize, icon_svgs, item_images, padY = "py-3"}) {
+ const iconSvg = item.icon ? (icon_svgs?.[item.icon] ?? null) : null;
+ const imgData = item_images?.[String(idx)] ?? null;
+ const height = HEIGHT[logoSize] ?? HEIGHT.md;
+ const maxW = MAX_W[logoSize] ?? MAX_W.md;
+ const slotW = SLOT_W[logoSize] ?? SLOT_W.md;
+
+ // Per-item overrides: style and scale
+ const itemStyle = item.style ?? logoStyle;
+ const filter = FILTER[itemStyle] ?? FILTER.grayscale;
+ // scale: compensates for SVG intrinsic whitespace or atypical artwork areas.
+ // e.g. scale: 1.3 enlarges a logo whose SVG has excessive padding baked into its viewBox.
+ const scale = typeof item.scale === "number" ? item.scale : 1;
+ const scaleStyle = scale !== 1 ? `;transform:scale(${scale})` : "";
+
+ let visual;
+ if (iconSvg) {
+ visual = (
+
+ );
+ } else if (imgData) {
+ visual = (
+
+ );
+ } else if (item.name) {
+ visual = {item.name};
+ } else {
+ return null;
+ }
+
+ const isExt = item.url && (item.url.startsWith("http://") || item.url.startsWith("https://"));
+ // Fixed-width slot: every logo occupies the same footprint → consistent visual weight
+ const cls = `flex items-center justify-center flex-shrink-0 ${slotW} ${padY}`;
+
+ return item.url ? (
+
+ {visual}
+
+ ) : (
+
{visual}
+ );
+}
+
+function LogoRow({items, logoStyle, logoSize, icon_svgs, item_images}) {
+ return (
+
+ {items.map((item, i) => (
+
+ ))}
+
+ );
+}
+
+function LogoGrid({items, logoStyle, logoSize, icon_svgs, item_images}) {
+ const cols =
+ items.length <= 3 ? "grid-cols-3" :
+ items.length <= 4 ? "grid-cols-2 sm:grid-cols-4" :
+ items.length <= 6 ? "grid-cols-3 sm:grid-cols-6" :
+ "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6";
+ return (
+
+ {items.map((item, i) => (
+
+ ))}
+
+ );
+}
+
+function LogoMarquee({items, logoStyle, logoSize, icon_svgs, item_images, speed}) {
+ const doubled = [...items, ...items];
+ return (
+ <>
+
+ {/*
+ mask-image fades edges without depending on the section background colour —
+ works on any bg (white, gray-50, dark, gradient, etc.)
+ */}
+
+
+ {doubled.map((item, i) => (
+
+ ))}
+
+
+ >
+ );
+}
+
+export const LogosBlock = ({content = {}, design = {}, icon_svgs = {}, item_images = {}}) => {
+ const rawItems = Array.isArray(content.items) ? content.items
+ : Array.isArray(content.logos) ? content.logos
+ : [];
+
+ const {title, subtitle, cta} = content;
+ const layout = design.layout || design.display_mode || "row";
+ const logoStyle = design.logo_style || "grayscale";
+ const logoSize = design.logo_size || "md";
+ const speed = design.marquee_speed ?? 30;
+
+ const isExtCta = cta?.url && (cta.url.startsWith("http://") || cta.url.startsWith("https://"));
+
+ return (
+
+
+ {(title || subtitle) && (
+
+ {title && (
+
+ )}
+ {subtitle && (
+
+ )}
+
+ )}
+
+ {layout === "marquee" && (
+
+ )}
+ {layout === "grid" && (
+
+ )}
+ {layout !== "marquee" && layout !== "grid" && (
+
+ )}
+
+ {cta?.text && cta?.url && (
+
+ )}
+
+
+ );
+};
diff --git a/modules/blox/blox/logos/manifest.json b/modules/blox/blox/logos/manifest.json
index 87213855..8b530d75 100644
--- a/modules/blox/blox/logos/manifest.json
+++ b/modules/blox/blox/logos/manifest.json
@@ -1,13 +1,15 @@
{
"id": "logos",
"name": "Logos",
- "version": "1.0.0",
+ "version": "2.0.0",
"license": "MIT",
- "category": "content",
- "tags": ["logos", "partners", "sponsors", "collaborators", "brands", "clients", "affiliations"],
- "description": "Display partner, sponsor, or collaborator logos with interactive effects and multiple display modes",
+ "category": "marketing",
+ "intent": ["inform"],
+ "verticals": ["startup", "saas", "agency", "academic-cv", "lab", "dev-portfolio"],
+ "tags": ["logos", "social-proof", "partners", "sponsors", "clients", "brands", "affiliations", "marquee"],
+ "description": "Social-proof logo row: display partner, client, or sponsor logos via the brands icon pack (or image files). Supports row, grid, and infinite-marquee layouts with grayscale/color/white filter styles.",
"author": "Hugo Blox",
"homepage": "https://hugoblox.com/blocks/",
"repository": "https://github.com/HugoBlox/kit",
- "keywords": ["hugo", "static-site", "logos", "partners", "sponsors", "carousel", "marquee"]
+ "keywords": ["hugo", "static-site", "logos", "social-proof", "brands", "marquee"]
}
diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md
index 0de88726..4beb5c9a 100644
--- a/templates/startup-landing-page/content/_index.md
+++ b/templates/startup-landing-page/content/_index.md
@@ -36,6 +36,35 @@ sections:
size: cover
position: center
parallax: false
+ - block: logos
+ content:
+ title: Trusted by teams at
+ items:
+ - icon: brands/github
+ name: GitHub
+ - icon: brands/google
+ name: Google
+ - icon: brands/microsoft
+ name: Microsoft
+ - icon: brands/nvidia
+ name: NVIDIA
+ - icon: brands/openai
+ name: OpenAI
+ - icon: brands/anthropic
+ name: Anthropic
+ - icon: brands/stripe
+ name: Stripe
+ - icon: brands/vercel
+ name: Vercel
+ design:
+ layout: marquee
+ logo_style: grayscale
+ logo_size: md
+ marquee_speed: 35
+ css_class: "bg-gray-50 dark:bg-gray-900"
+ spacing:
+ padding: ["2rem", 0, "2rem", 0]
+
- block: stats
content:
items: