How to add structured data (Schema.org)

Structured data describes a page's content to search engines in a machine-readable format. Piko has a typed first-class field for this: Metadata.StructuredData []string. Each entry renders as a separate <script type="application/ld+json"> block in the document head. The renderer silently drops invalid JSON entries with a warning log, so malformed payloads cannot break the rendered page. See the metadata reference for the surrounding metadata fields.

Use the typed metadata field

Marshal each Schema.org payload to JSON, then assign the strings to Metadata.StructuredData:

import (
    "encoding/json"
    "piko.sh/piko"
)

type Response struct {
    Title string
    Body  string
}

func Render(r *piko.RequestData, props piko.NoProps) (Response, piko.Metadata, error) {
    article, _ := json.Marshal(map[string]any{
        "@context":      "https://schema.org",
        "@type":         "Article",
        "headline":      "Deploying Piko to production",
        "author":        map[string]string{"@type": "Person", "name": "Alice Smith"},
        "datePublished": "2026-01-15",
        "dateModified":  "2026-01-20",
        "publisher":     map[string]any{"@type": "Organization", "name": "MyApp"},
        "image":         "https://example.com/articles/deploying-piko.jpg",
        "description":   "A practical guide to shipping Piko to production.",
    })

    return Response{
            Title: "Deploying Piko to production",
            Body:  "...",
        }, piko.Metadata{
            Title:          "Deploying Piko | MyApp Blog",
            Description:    "A practical guide to shipping Piko to production.",
            StructuredData: []string{string(article)},
        }, nil
}

Piko emits each entry in StructuredData as its own <script type="application/ld+json"> block, in order. Multiple entries are common - for example an Article payload alongside a BreadcrumbList.

Product schema

For e-commerce pages:

product, _ := json.Marshal(map[string]any{
    "@context":    "https://schema.org",
    "@type":       "Product",
    "name":        product.Name,
    "description": product.Description,
    "sku":         product.SKU,
    "offers": map[string]any{
        "@type":         "Offer",
        "price":         product.Price,
        "priceCurrency": "GBP",
        "availability":  "https://schema.org/InStock",
    },
    "aggregateRating": map[string]any{
        "@type":       "AggregateRating",
        "ratingValue": product.Rating,
        "reviewCount": product.ReviewCount,
    },
})

return Response{}, piko.Metadata{
    StructuredData: []string{string(product)},
}, nil

For navigation trails:

breadcrumb, _ := json.Marshal(map[string]any{
    "@context": "https://schema.org",
    "@type":    "BreadcrumbList",
    "itemListElement": []map[string]any{
        {"@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/"},
        {"@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog"},
        {"@type": "ListItem", "position": 3, "name": "Deploying Piko"},
    },
})

Combine with the article payload by appending both strings:

piko.Metadata{
    StructuredData: []string{string(article), string(breadcrumb)},
}

Organisation schema

Add once in a layout partial so every page emits it:

organisation, _ := json.Marshal(map[string]any{
    "@context": "https://schema.org",
    "@type":    "Organization",
    "name":     "MyApp Limited",
    "url":      "https://example.com",
    "logo":     "https://example.com/logo.png",
    "sameAs": []string{
        "https://twitter.com/MyApp",
        "https://github.com/MyApp",
    },
})

Fallback: rendering inside a template

Sometimes a page genuinely needs to author the JSON-LD block in the template, weaving in dynamic state alongside other markup. In that case, p-html can inject the payload as raw HTML without escaping it:

<template>
  <script type="application/ld+json" p-html="state.SchemaJSON"></script>
</template>

Metadata.StructuredData is the preferred path. The template approach skips Piko's JSON validation and ordering guarantees, so reach for it only when the metadata field cannot express the requirement.

Validate before shipping

Test JSON-LD with:

Missing or malformed properties do not surface in search results even when the page renders correctly.

See also