diff --git a/apps/sites/app/sites/[teamId]/(site)/[[...slug]]/page.tsx b/apps/sites/app/sites/[teamId]/(site)/[[...slug]]/page.tsx index bcc9b884..52ba5619 100644 --- a/apps/sites/app/sites/[teamId]/(site)/[[...slug]]/page.tsx +++ b/apps/sites/app/sites/[teamId]/(site)/[[...slug]]/page.tsx @@ -54,6 +54,7 @@ export async function generateMetadata( alternates: { canonical: url }, openGraph: { url, + type: "website", title: seo.title, description: seo.description ?? undefined, images: seo.imageUrl ? [{ url: seo.imageUrl }] : undefined, @@ -89,8 +90,9 @@ export default async function SitePage({ params }: Props) { dangerouslySetInnerHTML={{ __html: serializeJsonLd( organizationAndWebsiteJsonLd({ - siteName: page.name, - logoUrl: null, + siteName: settings?.title || page.title || "Website", + description: settings?.subtitle || page.description || null, + logoUrl: (settings?.logo?.url as string | undefined) ?? null, canonicalHost, }), ), diff --git a/apps/sites/app/sites/[teamId]/(site)/docs/[...slug]/page.tsx b/apps/sites/app/sites/[teamId]/(site)/docs/[...slug]/page.tsx index 344c894f..a5f1be5c 100644 --- a/apps/sites/app/sites/[teamId]/(site)/docs/[...slug]/page.tsx +++ b/apps/sites/app/sites/[teamId]/(site)/docs/[...slug]/page.tsx @@ -51,7 +51,7 @@ async function apiMetadata( title, description, alternates: { canonical: url }, - openGraph: { url, title, description }, + openGraph: { url, type: "article", title, description }, robots: { index, follow: index }, }; } @@ -113,6 +113,7 @@ export async function generateMetadata( alternates: { canonical: url }, openGraph: { url, + type: "article", title: seo.title, description: seo.description ?? undefined, images: seo.imageUrl ? [{ url: seo.imageUrl }] : undefined, diff --git a/apps/sites/app/sites/[teamId]/layout.tsx b/apps/sites/app/sites/[teamId]/layout.tsx index b30a92bc..8cea26b5 100644 --- a/apps/sites/app/sites/[teamId]/layout.tsx +++ b/apps/sites/app/sites/[teamId]/layout.tsx @@ -34,6 +34,7 @@ export async function generateMetadata({ // wins). A non-canonical host gets `X-Robots-Tag` from the proxy. robots: settings?.hideFromSearch ? { index: false, follow: false } : undefined, openGraph: { + type: "website", siteName: siteTitle, images: logoUrl ? [{ url: logoUrl }] : undefined, }, diff --git a/apps/sites/lib/json-ld.test.ts b/apps/sites/lib/json-ld.test.ts index 074e35f0..cc2c313d 100644 --- a/apps/sites/lib/json-ld.test.ts +++ b/apps/sites/lib/json-ld.test.ts @@ -11,6 +11,7 @@ describe("json-ld builders", () => { it("builds Organization + WebSite for the homepage", () => { const [org, site] = organizationAndWebsiteJsonLd({ siteName: "Acme", + description: "Rockets for everyone", logoUrl: "https://cdn/logo.png", canonicalHost: "acme.frontlit.app", }); @@ -18,6 +19,7 @@ describe("json-ld builders", () => { "@type": "Organization", name: "Acme", url: "https://acme.frontlit.app", + description: "Rockets for everyone", logo: "https://cdn/logo.png", }); expect(site).toMatchObject({ @@ -27,6 +29,17 @@ describe("json-ld builders", () => { }); }); + it("omits description and logo when the site has none", () => { + const [org] = organizationAndWebsiteJsonLd({ + siteName: "Acme", + description: null, + logoUrl: null, + canonicalHost: "acme.frontlit.app", + }); + expect(org).not.toHaveProperty("description"); + expect(org).not.toHaveProperty("logo"); + }); + it("builds BlogPosting with dateModified falling back to datePublished", () => { const ld = blogPostingJsonLd({ title: "Hello", diff --git a/apps/sites/lib/json-ld.ts b/apps/sites/lib/json-ld.ts index 05789beb..28f486be 100644 --- a/apps/sites/lib/json-ld.ts +++ b/apps/sites/lib/json-ld.ts @@ -6,6 +6,7 @@ export function organizationAndWebsiteJsonLd(input: { siteName: string; + description: string | null; logoUrl: string | null; canonicalHost: string; }) { @@ -16,6 +17,7 @@ export function organizationAndWebsiteJsonLd(input: { "@type": "Organization", name: input.siteName, url, + ...(input.description ? { description: input.description } : {}), ...(input.logoUrl ? { logo: input.logoUrl } : {}), }, { @@ -23,6 +25,7 @@ export function organizationAndWebsiteJsonLd(input: { "@type": "WebSite", name: input.siteName, url, + ...(input.description ? { description: input.description } : {}), }, ]; } diff --git a/packages/page-builder/src/blocks/_shared/title.ts b/packages/page-builder/src/blocks/_shared/title.ts index 75752627..d7a7f11c 100644 --- a/packages/page-builder/src/blocks/_shared/title.ts +++ b/packages/page-builder/src/blocks/_shared/title.ts @@ -59,3 +59,9 @@ export function renderTitle(title?: string, theme?: ThemeStyle): React.ReactNode : part, ); } + +/** Heading element for item titles under a block title (cards, steps), + * one level below it so the outline never skips a level. */ +export function itemHeadingLevel(titleLevel: HeadingLevel = "h2"): "h2" | "h3" | "h4" { + return titleLevel === "h1" ? "h2" : titleLevel === "h2" ? "h3" : "h4"; +} diff --git a/packages/page-builder/src/blocks/featured/widget.tsx b/packages/page-builder/src/blocks/featured/widget.tsx index 65593078..2e8df27d 100644 --- a/packages/page-builder/src/blocks/featured/widget.tsx +++ b/packages/page-builder/src/blocks/featured/widget.tsx @@ -4,7 +4,7 @@ import * as Icons from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { ThemeStyle, WidgetProps } from "../../models"; import type { FeaturedSettings } from "./settings"; -import { renderTitle, titleProps } from "../_shared/title"; +import { itemHeadingLevel, renderTitle, titleProps } from "../_shared/title"; import { TextRenderer } from "../../components/text-renderer"; import { Header1, @@ -48,6 +48,7 @@ export function FeaturedWidget({ overriddenTheme.structure.page.width = maxWidth || theme.theme.structure.page.width; overriddenTheme.structure.section.padding.y = verticalPadding || theme.theme.structure.section.padding.y; + const itemLevel = itemHeadingLevel(titleLevel); return (
@@ -74,7 +75,9 @@ export function FeaturedWidget({ {String(index + 1).padStart(2, "0")} - {item.title} + + {item.title} + {item.description && ( {item.description} )} @@ -119,7 +122,9 @@ export function FeaturedWidget({ layout === "compact" && "items-center gap-1", )} > - {item.title} + + {item.title} + {item.description && layout !== "compact" && ( {item.description} )} diff --git a/packages/page-builder/src/blocks/grid/widget.tsx b/packages/page-builder/src/blocks/grid/widget.tsx index 052cf831..e5c4e7f7 100644 --- a/packages/page-builder/src/blocks/grid/widget.tsx +++ b/packages/page-builder/src/blocks/grid/widget.tsx @@ -3,9 +3,10 @@ import clsx from "clsx"; import * as Icons from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { ThemeStyle, WidgetProps } from "../../models"; +import type { HeadingTag } from "../../primitives/heading"; import { getMediaUrl, getMediaAlt } from "../../models/media"; import type { GridItem, GridSettings } from "./settings"; -import { renderTitle, titleProps } from "../_shared/title"; +import { itemHeadingLevel, renderTitle, titleProps } from "../_shared/title"; import { Image } from "../../components/image"; import { NavLink } from "../../components/nav-link"; import { TextRenderer } from "../../components/text-renderer"; @@ -25,7 +26,15 @@ function resolveIcon(name?: string): LucideIcon | null { return (Icons as unknown as Record)[name] ?? null; } -function GridItemCard({ item, theme }: { item: GridItem; theme: ThemeStyle }) { +function GridItemCard({ + item, + theme, + headingLevel, +}: { + item: GridItem; + theme: ThemeStyle; + headingLevel: HeadingTag; +}) { const Icon = resolveIcon(item.icon); const mediaUrl = getMediaUrl(item.media); const mediaAlt = getMediaAlt(item.media); @@ -50,7 +59,7 @@ function GridItemCard({ item, theme }: { item: GridItem; theme: ThemeStyle }) { )}
- + {item.title}
@@ -79,7 +88,9 @@ function GridItemCard({ item, theme }: { item: GridItem; theme: ThemeStyle }) { const text = (
- {item.title} + + {item.title} + {item.description && {item.description}} {item.buttonAction && item.buttonCaption && ( @@ -164,7 +175,12 @@ export function GridWidget({ )} > {items.map((item, index) => ( - + ))}
)} diff --git a/packages/page-builder/src/blocks/pricing/widget.tsx b/packages/page-builder/src/blocks/pricing/widget.tsx index a9e601e0..c0cdca69 100644 --- a/packages/page-builder/src/blocks/pricing/widget.tsx +++ b/packages/page-builder/src/blocks/pricing/widget.tsx @@ -90,7 +90,7 @@ export function PricingWidget({ {tier.name} {tier.highlighted && Most popular}
- + {pricingSwitcher ? ( <> {tier.price} diff --git a/packages/page-builder/src/primitives/card.tsx b/packages/page-builder/src/primitives/card.tsx index 1a4d043d..60fbcefa 100644 --- a/packages/page-builder/src/primitives/card.tsx +++ b/packages/page-builder/src/primitives/card.tsx @@ -1,6 +1,7 @@ import type React from "react"; import type { ThemeStyle } from "../models"; import { Header4 } from "./header4"; +import type { HeadingTag } from "./heading"; import { cn } from "./lib/utils"; export interface PageCardProps extends React.HTMLAttributes { @@ -27,6 +28,8 @@ export interface PageCardHeaderProps extends React.HTMLAttributes = ({ @@ -102,6 +105,7 @@ export const PageCardHeader: React.FC = ({ children, className = "", theme, + as, ...props }) => { const classes = cn( @@ -112,7 +116,9 @@ export const PageCardHeader: React.FC = ({ return (
- {children} + + {children} +
); };