Site search

Search IndexLane

Type at least two characters to search.

    Technical note

    Use stable @id values in Schema.org markup

    Give an author, publisher, or article one consistent identifier so your JSON-LD blocks describe the same entities across pages.

    By NikoPublished October 8, 2026Read 6 min

    Use the same @id every time your structured data refers to the same person, organization, article, or product. That lets separate JSON-LD blocks identify the same thing rather than repeating disconnected descriptions.

    The JSON-LD data model defines @id as a node identifier. An object containing only that identifier refers to the same node; it does not supply all of the node's properties.

    Connect an article to its author

    These abbreviated examples show the difference. They illustrate author references, rather than complete Recipe markup.

    The first recipe includes an author name without a reusable ID:

    // Fragmented: each page defines an isolated Recipe entity
    {
      "@context": "https://schema.org",
      "@type": "Recipe",
      "name": "Sourdough Starter",
      "author": { "@type": "Person", "name": "Jane Baker" }
    }
    

    The second uses the identifier also used for that author's description:

    // Connected: Recipe references a Person that exists on the author page
    {
      "@context": "https://schema.org",
      "@type": "Recipe",
      "name": "Sourdough Starter",
      "author": { "@id": "https://example.com/authors/jane-baker#person" }
    }
    

    Keep the accurate author name and any required properties in the published data too. A reference does not mean a search engine will visit another page to fill in missing rich-result properties.

    Generate consistent IDs in Laravel

    Use the canonical URL plus a stable fragment, such as #article or #person, to distinguish the entity from the page that describes it:

    public function articleJsonLd(Article $article): array
    {
        return [
            '@context' => 'https://schema.org',
            '@type' => 'Article',
            '@id' => route('articles.show', $article) . '#article',
            'headline' => $article->title,
            'author' => [
                '@id' => route('authors.show', $article->author) . '#person',
            ],
            'publisher' => [
                '@id' => route('home') . '#organization',
            ],
            'mainEntityOfPage' => [
                '@id' => route('articles.show', $article),
            ],
        ];
    }
    

    Check that your route helpers produce the public hostname and preferred URL form. A staging host, extra slash, or different author URL can create a second identifier for the same entity.

    Reuse the same convention in Go

    This example generates organization, person, and article descriptions from consistent URLs:

    type EntityGraph struct {
        BaseURL string // canonical site URL, with no trailing slash
        OrganizationName string
        OfficialProfiles []string
    }
    
    func (g *EntityGraph) Organization() map[string]interface{} {
        return map[string]interface{}{
            "@context": "https://schema.org",
            "@type":    "Organization",
            "@id":      g.BaseURL + "#organization",
            "name":     g.OrganizationName,
            "url":      g.BaseURL,
            "sameAs":   g.OfficialProfiles,
        }
    }
    
    func (g *EntityGraph) Person(name, profileURL string) map[string]interface{} {
        return map[string]interface{}{
            "@context": "https://schema.org",
            "@type":    "Person",
            "@id":      profileURL + "#person",
            "name":     name,
            "affiliation": map[string]interface{}{
                "@id": g.BaseURL + "#organization",
            },
        }
    }
    
    func (g *EntityGraph) TechArticle(slug, title, authorURL string) map[string]interface{} {
        return map[string]interface{}{
            "@context": "https://schema.org",
            "@type":    "TechArticle",
            "@id":      g.BaseURL + "/" + slug + "#article",
            "headline": title,
            "author": map[string]interface{}{
                "@id": authorURL + "#person",
            },
            "publisher": map[string]interface{}{
                "@id": g.BaseURL + "#organization",
            },
        }
    }
    

    Set OrganizationName and OfficialProfiles from the real publisher. Include only profiles that belong to that publisher. The example assumes BaseURL has no trailing slash and that article slugs produce their canonical URLs; use your application's URL helper if routing is more complicated.

    The descriptions can appear together in an @graph, or use the same identifiers in page-specific blocks. Include the properties needed for each rich-result type on the page where you want that result.

    Find IDs that do not line up

    When auditing the output, distinguish a full description from an object that only references an ID. Look for staging hosts, conflicting descriptions, different entity types sharing an ID, and references without a description in the pages you checked.

    This small Python example collects described and referenced IDs from one response:

    import json, re, requests
    from urllib.parse import urljoin
    
    def audit_entity_graph(start_url):
        response = requests.get(start_url, timeout=10)
        response.raise_for_status()
        jsonld_blocks = re.findall(
            r'<script type="application/ld\+json">(.*?)</script>',
            response.text, re.DOTALL
        )
    
        described_ids = set()
        referenced_ids = set()
    
        for block in jsonld_blocks:
            try:
                data = json.loads(block)
                described, referenced = collect_ids(data)
                described_ids.update(described)
                referenced_ids.update(referenced)
            except json.JSONDecodeError:
                continue
    
        unresolved = referenced_ids - described_ids
    
        return {
            'described_entities': len(described_ids),
            'referenced_entities': len(referenced_ids),
            'unresolved_references': list(unresolved),
        }
    
    def collect_ids(obj):
        described, referenced = set(), set()
        if isinstance(obj, dict):
            node_id = obj.get('@id')
            if node_id:
                target = referenced if set(obj) == {'@id'} else described
                target.add(node_id)
            for key, value in obj.items():
                if key != '@id':
                    child_described, child_referenced = collect_ids(value)
                    described.update(child_described)
                    referenced.update(child_referenced)
        elif isinstance(obj, list):
            for item in obj:
                child_described, child_referenced = collect_ids(item)
                described.update(child_described)
                referenced.update(child_referenced)
        return described, referenced
    

    The sample's script-tag extraction assumes the exact markup shown in its regular expression. Use an HTML parser when auditing varying attributes, quotation marks, or script formatting. It also skips invalid JSON, so report those parse failures separately in a production audit.

    A reference may be described on another page. An unresolved ID in this one-page result is a reason to inspect further, not proof that Google cannot understand the page. Run the relevant rich-result validation separately.

    Check overlapping WordPress output

    A theme and multiple SEO plugins may each emit Organization, WebSite, Person, and Article markup. Inspect the rendered scripts and decide which component should produce each description. Use the plugin's filters or settings to align identifiers before adding another script in wp_head.

    Stable IDs keep descriptions consistent. They do not establish expertise, topical authority, or a ranking advantage by themselves.

    Related notes

    Keep dates in structured data accurate

    Use real publication, update, offer, and event dates in your structured data. Find expired values and stop templates from inventing a new date on every request.

    Read article →

    Image SEO: check URLs, lazy loading, and CDN access

    If important images are missing from search, check their public URLs, image markup, CDN access, and format handling before rewriting alt text.

    Read article →