All blocks

SEO JSON-LD Preview

Fetches and injects JSON-LD plus a copy-friendly rich-results debug panel.

Live preview

JSON-LD preview
Schema.org BreadcrumbList and Place for local SEO rich results.

No JSON-LD for this point.

Install

$bunx shadcn@latest add https://mapbase.dev/r/seo-json-ld-preview.json

The shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).

npm dependencies

mapbase

Registry dependencies

cardinputlabelbuttonhttps://mapbase.dev/r/seo-json-ld.json

Source

components/mapbase/seo-json-ld-preview.tsx
"use client"

import * as React from "react"
import { Mapbase, MapbaseError, type CountryCode } from "mapbase"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { SeoJsonLd } from "@/components/mapbase/seo-json-ld"

export type SeoJsonLdPreviewProps = {
  apiKey: string
  baseUrl?: string
  initialLng?: number
  initialLat?: number
  country?: CountryCode
  siteBaseUrl?: string
  className?: string
}

const DEFAULT_LNG = -9.1393
const DEFAULT_LAT = 38.7223

/**
 * Card that fetches JSON-LD for coordinates, injects script tags, and
 * shows a copy-friendly preview for rich-result debugging.
 */
export function SeoJsonLdPreview({
  apiKey,
  baseUrl,
  initialLng = DEFAULT_LNG,
  initialLat = DEFAULT_LAT,
  country,
  siteBaseUrl = "https://example.com",
  className,
}: SeoJsonLdPreviewProps) {
  const [lng, setLng] = React.useState(initialLng)
  const [lat, setLat] = React.useState(initialLat)
  const [lngInput, setLngInput] = React.useState(String(initialLng))
  const [latInput, setLatInput] = React.useState(String(initialLat))
  const [raw, setRaw] = React.useState<string | null>(null)
  const [error, setError] = React.useState<MapbaseError | null>(null)
  const [loading, setLoading] = React.useState(false)
  const [copied, setCopied] = React.useState(false)

  const client = React.useMemo(
    () => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
    [apiKey, baseUrl],
  )

  const fetchJsonLd = React.useCallback(async () => {
    setLoading(true)
    setError(null)
    const response = await client.seo.jsonLd({
      lng,
      lat,
      ...(country ? { country } : {}),
      ...(siteBaseUrl ? { base_url: siteBaseUrl } : {}),
    })
    setLoading(false)
    if (response.error) {
      setError(response.error)
      setRaw(null)
      return
    }
    setRaw(
      JSON.stringify(
        {
          breadcrumb_list: response.data.breadcrumb_list,
          place: response.data.place,
        },
        null,
        2,
      ),
    )
  }, [client, lng, lat, country, siteBaseUrl])

  React.useEffect(() => {
    void fetchJsonLd()
  }, [fetchJsonLd])

  const applyCoords = () => {
    const nextLng = Number.parseFloat(lngInput)
    const nextLat = Number.parseFloat(latInput)
    if (!Number.isFinite(nextLng) || !Number.isFinite(nextLat)) return
    setLng(nextLng)
    setLat(nextLat)
  }

  const copy = async () => {
    if (!raw) return
    await navigator.clipboard.writeText(raw)
    setCopied(true)
    window.setTimeout(() => setCopied(false), 2000)
  }

  return (
    <Card className={className}>
      <CardHeader>
        <CardTitle>JSON-LD preview</CardTitle>
        <CardDescription>
          Schema.org BreadcrumbList and Place for local SEO rich results.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <div className="flex flex-wrap items-end gap-3">
          <div className="flex min-w-[8rem] flex-col gap-1.5">
            <Label htmlFor="seo-ld-lng" className="text-xs">
              Longitude
            </Label>
            <Input
              id="seo-ld-lng"
              inputMode="decimal"
              value={lngInput}
              onChange={(e) => setLngInput(e.target.value)}
              className="h-8 font-mono text-sm"
            />
          </div>
          <div className="flex min-w-[8rem] flex-col gap-1.5">
            <Label htmlFor="seo-ld-lat" className="text-xs">
              Latitude
            </Label>
            <Input
              id="seo-ld-lat"
              inputMode="decimal"
              value={latInput}
              onChange={(e) => setLatInput(e.target.value)}
              className="h-8 font-mono text-sm"
            />
          </div>
          <Button type="button" size="sm" className="h-8" onClick={applyCoords}>
            Apply
          </Button>
        </div>

        <SeoJsonLd
          apiKey={apiKey}
          baseUrl={baseUrl}
          lng={lng}
          lat={lat}
          country={country}
          siteBaseUrl={siteBaseUrl}
        />

        {loading ? (
          <p className="text-xs text-muted-foreground">Refreshing preview…</p>
        ) : null}
        {error ? (
          <p className="text-xs text-destructive">{error.message}</p>
        ) : null}

        {raw ? (
          <div className="flex flex-col gap-2">
            <div className="flex items-center justify-between gap-2">
              <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
                Copy payload
              </p>
              <Button type="button" variant="outline" size="sm" onClick={copy}>
                {copied ? "Copied" : "Copy JSON"}
              </Button>
            </div>
            <pre className="max-h-64 overflow-auto rounded-md border bg-muted/30 p-3 font-mono text-xs text-muted-foreground">
              {raw}
            </pre>
          </div>
        ) : null}
      </CardContent>
    </Card>
  )
}