Skip to content

Links

A Link is one hyperlink found on a WebPage: an <a href> element, with its anchor text, its rel tokens, its place in the layout and its destination. With destinations recorded, the links of a site form a graph, and the usual internal-linking work becomes a query over that graph:

  • Which pages link to a given page, with what anchor text, and from which part of the layout?
  • Which of those anchors are editorial, and which are site-wide navigation or footer boilerplate?
  • Which internal links point at pages that return an error?
  • Which internal links carry nofollow?

Short version

Emit one Link per <a href>, with linkTargetUrl (the href) and linkText (the anchor). Put navigation, footer and other blocks of links in a LinkGroup with a linkGroupType. Add linkTarget (the destination node) only once you have resolved the href to a page or entity.

On this page, the href is the URL written in the markup, recorded with linkTargetUrl, and the destination is the node that href resolves to, recorded with linkTarget.

The model at a glance

flowchart TB
  P[WebPage] -- hasLinkGroup --> G[LinkGroup]
  G -- hasLink --> L[Link]
  P -- link --> L
  L -- linkTarget --> T[WebPage, URL or entity]

A page reaches its links in one of two ways:

  • Grouped: hasLinkGroup from the WebPage to a LinkGroup, then hasLink from the group to each Link. Use this for blocks of related links: the main navigation, the footer, a sidebar, a "Related insights" block.
  • Ungrouped: link straight from the WebPage to the Link. Use this for raw extracted links before any grouping pass, and for single inline links in body text.
Term Value Notes
linkTargetUrl string The href, absolute and resolved against the page's base URL. Always set it.
linkTarget node The destination: a WebPage, a URL node or an entity. Set it only once resolved. Inverse: isLinkTargetOf.
linkText literal The anchor text. May carry a language tag.
linkRel string One rel token per value: nofollow, sponsored, ugc, noopener, and so on.
linkLocation placement Placement of an ungrouped link.
position number Order of the link within its group.

LinkGroup

Term Value Notes
hasLink Link Each link in the group. Inverse: isLinkOf.
linkGroupType placement Placement of the whole group. Takes precedence over linkLocation.
linkGroupName string Display name, for example "Footer links".
linkGroupID string Identifier of the group.

WebPage

Term Value Notes
hasLinkGroup LinkGroup Grouped links. Inverse: isLinkGroupOf.
link Link Ungrouped links.

placement is one of header, footer, body, sidebar or navigation. The ontology reference lists the formal domain and range of each term.

Why it is modeled this way

Two properties for where a link points. A parser knows the href, a string. A resolver knows, later, the node the href stands for: a page in the graph, a URL node, or an entity. OWL 2 DL does not allow one property to take both a string and a node, so there are two: linkTargetUrl for the href, always present, and linkTarget for the destination, present once resolved. Because linkTarget ranges over schema:Thing, one property covers every kind of destination, and a query narrows it with a type filter.

The href belongs to the link, not to a page identity. Many links share one href, and a page can link to the same URL several times. linkTargetUrl is a plain value on the Link, so two links to the same href simply share a value. It is deliberately not a URL node via hasURL: that property is functional and inverse-functional, which makes the URL node a key for page identity.

Placement is a fixed set of values. linkGroupType and linkLocation share one enumeration, so a consumer can filter out boilerplate without guessing at free text. When a link sits in a LinkGroup, the group's linkGroupType applies and linkLocation is redundant. linkLocation exists for ungrouped links, which have no group to carry their placement.

Internal or external is derived, not stored. A link is internal when its source page and its destination share a Domain. Comparing domains handles subdomains and multi-domain brands, which a stored internal / external flag cannot, and it never goes stale when a site's domain setup changes.

rel is a list of tokens. HTML allows several rel tokens on one link, for example nofollow noopener. linkRel holds one token per value, so sponsored and ugc are as easy to express as nofollow, and no separate boolean can contradict them.

Examples

Each example is shown in three syntaxes that produce the same RDF graph. Pick one and every example on the page switches to it.

  • JSON-LD is what most producers emit.
  • YAML-LD is JSON-LD written as YAML (W3C Working Draft, media type application/ld+yaml). It allows comments, so comments explaining each property appear in the YAML-LD tab.
  • Turtle is what you load into a triple store.

The JSON-LD and YAML-LD examples use the SEOntology JSON-LD context, https://w3id.org/seovoc/context.jsonld, which maps short names such as linkTargetUrl to their seovoc: IRIs. The Turtle examples set @base <https://example.com/>, so <outlook/> stands for https://example.com/outlook/.

The least a crawler can say about a link, and enough to start building a link graph: the page, the anchor text and the href. The link sits inline in body text, so it is ungrouped and states its own placement with linkLocation.

{
  "@context": "https://w3id.org/seovoc/context.jsonld",
  "@id": "https://example.com/outlook/",
  "@type": "WebPage",
  "link": {
    "@type": "Link",
    "linkText": "where wealth managers see 2026 risk",
    "linkTargetUrl": "https://example.com/wealth/",
    "linkLocation": "body"
  }
}
"@context": https://w3id.org/seovoc/context.jsonld
"@id": https://example.com/outlook/
"@type": WebPage
# An inline link in body text, so it is ungrouped
link:
  "@type": Link
  # The anchor text, as it appears on the page
  linkText: where wealth managers see 2026 risk
  # The href, made absolute against the page's URL
  linkTargetUrl: https://example.com/wealth/
  # Placement, because there is no LinkGroup to carry it
  linkLocation: body
@base   <https://example.com/> .
@prefix seovoc: <https://w3id.org/seovoc/> .

<outlook/> a seovoc:WebPage ;
    seovoc:link [
        a seovoc:Link ;
        seovoc:linkText "where wealth managers see 2026 risk" ;
        seovoc:linkTargetUrl "https://example.com/wealth/" ;
        seovoc:linkLocation "body"
    ] .

2. Navigation group

The main navigation of the same page, as a LinkGroup. The first link has been resolved to a page in the graph, so it carries linkTarget as well as linkTargetUrl, and the destination's status is a single lookup away. The second link points at a page that has not been crawled, so it has an href and no destination.

{
  "@context": "https://w3id.org/seovoc/context.jsonld",
  "@graph": [
    {
      "@id": "https://example.com/outlook/",
      "@type": "WebPage",
      "hasLinkGroup": {
        "@id": "https://example.com/outlook/#nav",
        "@type": "LinkGroup",
        "linkGroupID": "main-nav",
        "linkGroupName": "Main navigation",
        "linkGroupType": "navigation",
        "hasLink": [
          {
            "@type": "Link",
            "position": 1,
            "linkText": "Wealth Management",
            "linkTargetUrl": "https://example.com/wealth/",
            "linkTarget": "https://example.com/wealth/"
          },
          {
            "@type": "Link",
            "position": 2,
            "linkText": "Investment Banking",
            "linkTargetUrl": "https://example.com/banking/"
          }
        ]
      }
    },
    {
      "@id": "https://example.com/wealth/",
      "@type": "WebPage",
      "status": 200
    }
  ]
}
"@context": https://w3id.org/seovoc/context.jsonld
"@graph":
  - "@id": https://example.com/outlook/
    "@type": WebPage
    # A block of links on this page
    hasLinkGroup:
      "@id": https://example.com/outlook/#nav
      "@type": LinkGroup
      # Identifier of the group, a string
      linkGroupID: main-nav
      # Display name, usually a heading or landmark label
      linkGroupName: Main navigation
      # Placement of every link in the group
      linkGroupType: navigation
      hasLink:
        - "@type": Link
          # Order within the group
          position: 1
          linkText: Wealth Management
          linkTargetUrl: https://example.com/wealth/
          # Resolved: the destination is a page in this graph
          linkTarget: https://example.com/wealth/
        - "@type": Link
          position: 2
          linkText: Investment Banking
          # Not crawled yet: an href, but no destination
          linkTargetUrl: https://example.com/banking/

  # The destination page, in the same graph
  - "@id": https://example.com/wealth/
    "@type": WebPage
    # HTTP status of the destination
    status: 200
@base   <https://example.com/> .
@prefix seovoc: <https://w3id.org/seovoc/> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .

<outlook/> a seovoc:WebPage ;
    seovoc:hasLinkGroup <outlook/#nav> .

<outlook/#nav> a seovoc:LinkGroup ;
    seovoc:linkGroupID "main-nav" ;
    seovoc:linkGroupName "Main navigation" ;
    seovoc:linkGroupType "navigation" ;
    seovoc:hasLink [
        a seovoc:Link ;
        seovoc:position 1 ;
        seovoc:linkText "Wealth Management" ;
        seovoc:linkTargetUrl "https://example.com/wealth/" ;
        seovoc:linkTarget <wealth/>
    ] , [
        a seovoc:Link ;
        seovoc:position 2 ;
        seovoc:linkText "Investment Banking" ;
        seovoc:linkTargetUrl "https://example.com/banking/"
    ] .

<wealth/> a seovoc:WebPage ;
    seovoc:status "200"^^xsd:int .

3. Two anchors, one target

The same page linked twice from one page: from the navigation with a generic anchor, and from an editorial "Related insights" block with a descriptive one. The two links are separate Link nodes with the same linkTargetUrl value, which is how you compare the anchors that point at one page. The editorial anchor carries a language tag.

{
  "@context": "https://w3id.org/seovoc/context.jsonld",
  "@id": "https://example.com/outlook/",
  "@type": "WebPage",
  "hasLinkGroup": [
    {
      "@type": "LinkGroup",
      "linkGroupName": "Main navigation",
      "linkGroupType": "navigation",
      "hasLink": {
        "@type": "Link",
        "linkText": "Wealth Management",
        "linkTargetUrl": "https://example.com/wealth/"
      }
    },
    {
      "@type": "LinkGroup",
      "linkGroupName": "Related insights",
      "linkGroupType": "body",
      "hasLink": {
        "@type": "Link",
        "linkText": {
          "@value": "where wealth managers see 2026 risk",
          "@language": "en"
        },
        "linkTargetUrl": "https://example.com/wealth/"
      }
    }
  ]
}
"@context": https://w3id.org/seovoc/context.jsonld
"@id": https://example.com/outlook/
"@type": WebPage
hasLinkGroup:
  # Boilerplate: repeated on every page of the site
  - "@type": LinkGroup
    linkGroupName: Main navigation
    linkGroupType: navigation
    hasLink:
      "@type": Link
      # Generic anchor, chosen for navigation
      linkText: Wealth Management
      linkTargetUrl: https://example.com/wealth/
  # Editorial: specific to this page
  - "@type": LinkGroup
    linkGroupName: Related insights
    linkGroupType: body
    hasLink:
      "@type": Link
      # Descriptive anchor, with a language tag
      linkText:
        "@value": where wealth managers see 2026 risk
        "@language": en
      # The same href as the navigation link
      linkTargetUrl: https://example.com/wealth/
@base   <https://example.com/> .
@prefix seovoc: <https://w3id.org/seovoc/> .

<outlook/> a seovoc:WebPage ;
    seovoc:hasLinkGroup [
        a seovoc:LinkGroup ;
        seovoc:linkGroupName "Main navigation" ;
        seovoc:linkGroupType "navigation" ;
        seovoc:hasLink [
            a seovoc:Link ;
            seovoc:linkText "Wealth Management" ;
            seovoc:linkTargetUrl "https://example.com/wealth/"
        ]
    ] , [
        a seovoc:LinkGroup ;
        seovoc:linkGroupName "Related insights" ;
        seovoc:linkGroupType "body" ;
        seovoc:hasLink [
            a seovoc:Link ;
            seovoc:linkText "where wealth managers see 2026 risk"@en ;
            seovoc:linkTargetUrl "https://example.com/wealth/"
        ]
    ] .

A link to another site, with two rel tokens. linkRel takes one value per token, so the full list is kept: nofollow, sponsored and ugc are hints search engines read, and noopener matters for security audits. There is no linkTarget: the destination is outside the graph, so the href is all there is to record.

{
  "@context": "https://w3id.org/seovoc/context.jsonld",
  "@id": "https://example.com/outlook/",
  "@type": "WebPage",
  "link": {
    "@type": "Link",
    "linkText": "the Federal Reserve's policy statement",
    "linkTargetUrl": "https://www.federalreserve.gov/monetarypolicy.htm",
    "linkLocation": "body",
    "linkRel": ["nofollow", "noopener"]
  }
}
"@context": https://w3id.org/seovoc/context.jsonld
"@id": https://example.com/outlook/
"@type": WebPage
link:
  "@type": Link
  linkText: the Federal Reserve's policy statement
  # Another site: an href, and no destination node
  linkTargetUrl: https://www.federalreserve.gov/monetarypolicy.htm
  linkLocation: body
  # Every rel token in the markup, one value each
  linkRel:
    - nofollow
    - noopener
@base   <https://example.com/> .
@prefix seovoc: <https://w3id.org/seovoc/> .

<outlook/> a seovoc:WebPage ;
    seovoc:link [
        a seovoc:Link ;
        seovoc:linkText "the Federal Reserve's policy statement" ;
        seovoc:linkTargetUrl
            "https://www.federalreserve.gov/monetarypolicy.htm" ;
        seovoc:linkLocation "body" ;
        seovoc:linkRel "nofollow" , "noopener"
    ] .

5. Internal vs external

Whether a link is internal is not stored on the link. Give pages a Domain with hasDomain: a link is internal when its destination page has the same Domain as the source page. For a destination that was never crawled, compare the host of its linkTargetUrl with the source page's domainName.

{
  "@context": "https://w3id.org/seovoc/context.jsonld",
  "@graph": [
    {
      "@id": "https://example.com/outlook/",
      "@type": "WebPage",
      "hasDomain": "https://example.com/#domain",
      "link": [
        {
          "@type": "Link",
          "linkText": "where wealth managers see 2026 risk",
          "linkTargetUrl": "https://example.com/wealth/",
          "linkTarget": "https://example.com/wealth/"
        },
        {
          "@type": "Link",
          "linkText": "the Federal Reserve's policy statement",
          "linkTargetUrl": "https://www.federalreserve.gov/monetarypolicy.htm"
        }
      ]
    },
    {
      "@id": "https://example.com/wealth/",
      "@type": "WebPage",
      "hasDomain": "https://example.com/#domain"
    },
    {
      "@id": "https://example.com/#domain",
      "@type": "Domain",
      "domainName": "example.com"
    }
  ]
}
"@context": https://w3id.org/seovoc/context.jsonld
"@graph":
  - "@id": https://example.com/outlook/
    "@type": WebPage
    # The source page's domain
    hasDomain: https://example.com/#domain
    link:
      # Internal: the destination has the same Domain
      - "@type": Link
        linkText: where wealth managers see 2026 risk
        linkTargetUrl: https://example.com/wealth/
        linkTarget: https://example.com/wealth/
      # External: no destination, and the href host
      # differs from the source page's domainName
      - "@type": Link
        linkText: the Federal Reserve's policy statement
        linkTargetUrl: https://www.federalreserve.gov/monetarypolicy.htm

  - "@id": https://example.com/wealth/
    "@type": WebPage
    # The same Domain node: this is what "internal" means
    hasDomain: https://example.com/#domain

  - "@id": https://example.com/#domain
    "@type": Domain
    # The host, for comparing with hrefs that were not crawled
    domainName: example.com
@base   <https://example.com/> .
@prefix seovoc: <https://w3id.org/seovoc/> .

<outlook/> a seovoc:WebPage ;
    seovoc:hasDomain <#domain> ;
    seovoc:link [
        a seovoc:Link ;
        seovoc:linkText "where wealth managers see 2026 risk" ;
        seovoc:linkTargetUrl "https://example.com/wealth/" ;
        seovoc:linkTarget <wealth/>
    ] , [
        a seovoc:Link ;
        seovoc:linkText "the Federal Reserve's policy statement" ;
        seovoc:linkTargetUrl
            "https://www.federalreserve.gov/monetarypolicy.htm"
    ] .

<wealth/> a seovoc:WebPage ;
    seovoc:hasDomain <#domain> .

<#domain> a seovoc:Domain ;
    seovoc:domainName "example.com" .

Pages that link to a page, with anchor text and placement, excluding navigation and footer boilerplate. The query matches on the linkTargetUrl value, so it also finds links whose destination was never resolved, and it covers both grouped and ungrouped links.

PREFIX seovoc: <https://w3id.org/seovoc/>

SELECT ?page ?text ?placement
WHERE {
  ?link seovoc:linkTargetUrl "https://example.com/wealth/" .
  OPTIONAL { ?link seovoc:linkText ?text }
  {
    ?page seovoc:hasLinkGroup ?group .
    ?group seovoc:hasLink ?link .
    OPTIONAL { ?group seovoc:linkGroupType ?placement }
  } UNION {
    ?page seovoc:link ?link .
    OPTIONAL { ?link seovoc:linkLocation ?placement }
  }
  FILTER (!BOUND(?placement)
          || ?placement NOT IN ("header", "footer", "navigation"))
}

Links whose destination returns an HTTP error.

PREFIX seovoc: <https://w3id.org/seovoc/>

SELECT ?page ?href ?status
WHERE {
  { ?page seovoc:link ?link }
  UNION
  { ?page seovoc:hasLinkGroup/seovoc:hasLink ?link }
  ?link seovoc:linkTargetUrl ?href ;
        seovoc:linkTarget ?target .
  ?target seovoc:status ?status .
  FILTER (?status >= 400)
}

Links that carry nofollow and point at a page on the source page's own domain.

PREFIX seovoc: <https://w3id.org/seovoc/>

SELECT ?page ?href
WHERE {
  { ?page seovoc:link ?link }
  UNION
  { ?page seovoc:hasLinkGroup/seovoc:hasLink ?link }
  ?link seovoc:linkRel "nofollow" ;
        seovoc:linkTargetUrl ?href ;
        seovoc:linkTarget ?target .
  ?page seovoc:hasDomain ?domain .
  ?target seovoc:hasDomain ?domain .
}

Guidance

These recommendations follow Google's documentation on crawlable links and qualifying outbound links, and common practice in crawlers that audit links.

Capturing links

  • Record <a href> elements. They are the links search engines reliably follow. Elements that act as links only through JavaScript, and javascript: hrefs, are not links in this sense.
  • Always set linkTargetUrl, absolute and resolved against the page's base URL. Otherwise keep it as the page wrote it: do not replace it with the canonical URL or the URL after redirects. That belongs in linkTarget, which points at the destination's WebPage node.
  • Set linkText to the anchor as it appears on the page. Keep empty or generic anchors ("click here", "read more") as they are: finding them is part of an audit.
  • Split rel into tokens, one linkRel value each, lowercased. HTML separates tokens with spaces; split on commas as well, because some sites use them.
  • Use position for document order within a group. It supports analysis; it does not imply that the first link to a URL counts more, which Google has not documented.

Placement

  • Group links that belong together (navigation, footer, a related-content block) in a LinkGroup and set its linkGroupType. Do not create a LinkGroup for one inline link in body text: use link and linkLocation.
  • Placement is a heuristic. Crawlers classify it differently, from HTML landmarks such as <nav> and <footer>, from CSS selectors or from rendering. Document which method your pipeline uses, so consumers know how far to trust a navigation or body value.

Avoid

  • Putting anchor text, or several hrefs, in linkTargetUrl.
  • Packing several rel tokens into one value, such as "nofollow noopener".
  • Setting linkTarget before you have resolved the href. Leave it out until you know what it points at.
  • Storing internal or external as a flag. Derive it from Domain, as in example 5.
  • Using the deprecated terms below in new data.

rel and placement are signals, not rules

rel values describe the publisher's intent. Google treats nofollow, sponsored and ugc as hints, not directives, so a query over linkRel tells you what the site declared, not how a search engine handled the link. Likewise, Google has said informally that it tells primary content apart from boilerplate but does not document how: use placement to focus your own analysis, not as a ranking rule.

Migrating from deprecated terms

Earlier versions modeled links with terms that are now deprecated. Their IRIs still resolve and existing data stays valid, but new data should use the replacements.

Deprecated Use instead
anchorResource linkTarget
linkgroupID linkGroupID, a string rather than an integer
noFollow linkRel with the value nofollow
linkType Nothing: derive internal or external from Domain, as in example 5
AnchorText (class) linkText on the Link

If you used nav as a linkLocation value, use navigation. See the compatibility notes for the full list of changes.

Not covered yet

These come up in link audits but have no agreed modeling yet. They are open for ontology proposals:

  • Whether repeated links to the same href on one page are one Link or several. Repeated links are common and Google has said they are fine; until there is a rule, document which convention your pipeline follows.
  • An href normalization policy: trailing slashes, query parameters, fragments.
  • Redirect hops between the href and the final destination.
  • The element a link comes from (<a> versus <link rel>), and hreflang.
  • Image links. Google uses the image's alt text as the anchor, but there is no agreed way yet to record that a linkText came from alt.
  • Tying an entity mentioned in body text to the absence of a link, for finding internal-link opportunities.