<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Headlark</title>
    <description>The latest articles on DEV Community by Headlark (@headlark).</description>
    <link>https://dev.to/headlark</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4198871%2F29c6e8ae-65a0-4003-b928-6a9ae8adee1c.png</url>
      <title>DEV Community: Headlark</title>
      <link>https://dev.to/headlark</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/headlark"/>
    <language>en</language>
    <item>
      <title>Duplicate titles and meta descriptions on a docs site</title>
      <dc:creator>Headlark</dc:creator>
      <pubDate>Wed, 21 Oct 2026 22:57:48 +0000</pubDate>
      <link>https://dev.to/headlark/duplicate-titles-and-meta-descriptions-on-a-docs-site-ape</link>
      <guid>https://dev.to/headlark/duplicate-titles-and-meta-descriptions-on-a-docs-site-ape</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://headlark.com/guides/duplicate-meta-descriptions-docs" rel="noopener noreferrer"&gt;headlark.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Docs sites grow by copying pages. A new section starts as a copy of an old one, every product area gets an "Overview" and a "Configuration" page, and the framework fills in whatever the front matter leaves out. The result is dozens of pages that tell search engines the same thing in their &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Google asks for a distinct description per page and descriptive, concise titles (&lt;a href="https://developers.google.com/search/docs/appearance/snippet" rel="noopener noreferrer"&gt;Google Search Central: snippets&lt;/a&gt;, &lt;a href="https://developers.google.com/search/docs/appearance/title-link" rel="noopener noreferrer"&gt;title links&lt;/a&gt;). When fifty pages share one description, Google writes its own snippet from the page text, and search results for your docs look alike, so people have a harder time picking the right page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the duplicates come from
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docusaurus
&lt;/h3&gt;

&lt;p&gt;Each doc's description comes from its &lt;code&gt;description&lt;/code&gt; front matter. Without it, Docusaurus uses the first line of Markdown content as the description (&lt;a href="https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter" rel="noopener noreferrer"&gt;docs front matter&lt;/a&gt;). It skips imports, code blocks and the &lt;code&gt;#&lt;/code&gt; title, but not everything else, so these patterns produce repeats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pages that open with the same &lt;code&gt;## Overview&lt;/code&gt; or &lt;code&gt;## Prerequisites&lt;/code&gt; heading get "Overview" or "Prerequisites" as their description.&lt;/li&gt;
&lt;li&gt;Pages that open with the same admonition (&lt;code&gt;:::note&lt;/code&gt; followed by "This feature is in beta.") all get that sentence.&lt;/li&gt;
&lt;li&gt;Versioned docs repeat every page once per version, with identical titles and descriptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Titles come from &lt;code&gt;title&lt;/code&gt; front matter, then the Markdown &lt;code&gt;#&lt;/code&gt; heading, then the doc id, followed by the site title. Two pages called "Configuration" in different sections become two identical "Configuration | My Project" titles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starlight
&lt;/h3&gt;

&lt;p&gt;Every page has a required &lt;code&gt;title&lt;/code&gt;; &lt;code&gt;description&lt;/code&gt; is optional. The site-wide &lt;code&gt;description&lt;/code&gt; in your Starlight config is used for every page that doesn't set its own (&lt;a href="https://starlight.astro.build/reference/configuration/#description" rel="noopener noreferrer"&gt;Starlight configuration&lt;/a&gt;). So the more pages you have without front matter descriptions, the more pages share the site description.&lt;/p&gt;

&lt;h3&gt;
  
  
  VitePress
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; in &lt;code&gt;.vitepress/config&lt;/code&gt; renders on every page, and a page's front matter &lt;code&gt;description&lt;/code&gt; overrides it (&lt;a href="https://vitepress.dev/reference/site-config#description" rel="noopener noreferrer"&gt;VitePress site config&lt;/a&gt;). A page's title is its front matter &lt;code&gt;title&lt;/code&gt; or the text of its first &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, plus the &lt;code&gt;titleTemplate&lt;/code&gt; suffix. The same failure modes as Starlight: one description everywhere, and repeated headings become repeated titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find them in your build
&lt;/h2&gt;

&lt;p&gt;Check the HTML your framework produced, not the source files: that's what crawlers read, and it includes the fallbacks. Build first, then point these at the output folder (&lt;code&gt;build&lt;/code&gt; for Docusaurus, &lt;code&gt;dist&lt;/code&gt; for Starlight, &lt;code&gt;.vitepress/dist&lt;/code&gt; for VitePress).&lt;/p&gt;

&lt;p&gt;Descriptions used on more than one page, with a count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="description" content="[^"]*"'&lt;/span&gt; build | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-cd&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Titles used on more than one page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title[^&amp;gt;]*&amp;gt;[^&amp;lt;]*'&lt;/span&gt; build | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;title[^&amp;gt;]*&amp;gt;//'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-cd&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see which files share a given description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlF&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'content="Overview"'&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attribute order and quoting vary between frameworks; if a command prints nothing, open one built page and adjust the pattern to match its tag. For the live site, the &lt;a href="https://headlark.com/tools/site-checker" rel="noopener noreferrer"&gt;free site checker&lt;/a&gt; reads your sitemap and lists every duplicated title and description with the pages that share it, across up to 50 pages. For a single live page, the &lt;a href="https://headlark.com/tools/seo-checker" rel="noopener noreferrer"&gt;free SEO checker&lt;/a&gt; shows the title and description exactly as crawlers get them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix them per framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docusaurus
&lt;/h3&gt;

&lt;p&gt;Set both fields in the front matter of each doc. &lt;code&gt;description&lt;/code&gt; also becomes &lt;code&gt;og:description&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure the CLI&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Every option in headlark.config.json, with defaults and an example for monorepos.&lt;/span&gt;
&lt;span class="na"&gt;sidebar_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configuration&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;title&lt;/code&gt; is for the browser tab and search results; &lt;code&gt;sidebar_label&lt;/code&gt; keeps the short name in the sidebar. For old versions you don't want in search results, set &lt;code&gt;noIndex&lt;/code&gt; on the version in the docs plugin options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// docusaurus.config.js, inside the docs plugin or preset options&lt;/span&gt;
&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;noIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Starlight
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure the CLI&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Every option in headlark.config.json, with defaults and an example for monorepos.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the site-wide &lt;code&gt;description&lt;/code&gt; in &lt;code&gt;astro.config.mjs&lt;/code&gt; for the home page, and treat any other page that still shows it as unfinished. Starlight joins the page and site titles with &lt;code&gt;titleDelimiter&lt;/code&gt; (default &lt;code&gt;|&lt;/code&gt;); the page title is the part that has to be unique.&lt;/p&gt;

&lt;h3&gt;
  
  
  VitePress
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure the CLI&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Every option in headlark.config.json, with defaults and an example for monorepos.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To catch pages that still inherit the site description, a &lt;code&gt;transformPageData&lt;/code&gt; hook in &lt;code&gt;.vitepress/config.mts&lt;/code&gt; can list them during the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Docs for the example CLI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transformPageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`no description: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pageData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Writing ones that stay unique
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead with what the page is about, not the section.&lt;/strong&gt; "Configure the CLI" beats "Configuration"; "Deploy to Netlify" beats "Netlify".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the distinguishing detail in the description:&lt;/strong&gt; the option names, the platform, the error message the reader searched for. Aim for one or two sentences, roughly 70 to 155 characters, so it isn't cut off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fill descriptions with keywords.&lt;/strong&gt; Google is explicit that keyword lists don't help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reused partials and generated pages&lt;/strong&gt; (API references, changelogs per version) need a description built from the data that differs, such as the class name or version number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run the duplicate check in CI&lt;/strong&gt; so a copied page doesn't reintroduce the problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automating it with Headlark
&lt;/h2&gt;

&lt;p&gt;Writing and maintaining a description for every page is the part that slips on a large docs site. Headlark's &lt;a href="https://headlark.com/docs/build" rel="noopener noreferrer"&gt;build step&lt;/a&gt; runs after your framework's build, on the HTML in &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt; or &lt;code&gt;.vitepress/dist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docusaurus build &amp;amp;&amp;amp; headlark build --site-url https://docs.example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Each page gets a title and description written from that page's own content, so two pages only end up alike if they say the same thing.&lt;/li&gt;
&lt;li&gt;Your canonical, robots directives and JSON-LD types are kept; an empty result never replaces one of your tags.&lt;/li&gt;
&lt;li&gt;Unchanged pages come from cache on every rebuild.&lt;/li&gt;
&lt;li&gt;Docusaurus and VitePress switch pages in the browser after the first load and can apply their own title from their route data, so the browser tab may show your original title after a click. Crawlers and link previewers load each page's HTML and see the rewritten tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Review the results on your most visited pages; it takes minutes when the rest is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The free plan covers one site and 25 pages. &lt;a href="https://headlark.com/app" rel="noopener noreferrer"&gt;Add your site&lt;/a&gt;, copy its secret key, and your next build ships with SEO.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>documentation</category>
      <category>webdev</category>
      <category>astro</category>
    </item>
    <item>
      <title>SEO for Docusaurus: titles, descriptions, link previews and structured data</title>
      <dc:creator>Headlark</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:47:28 +0000</pubDate>
      <link>https://dev.to/headlark/seo-for-docusaurus-titles-descriptions-link-previews-and-structured-data-e69</link>
      <guid>https://dev.to/headlark/seo-for-docusaurus-titles-descriptions-link-previews-and-structured-data-e69</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://headlark.com/guides/docusaurus-seo" rel="noopener noreferrer"&gt;headlark.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Docusaurus renders a static HTML file for every route, so crawlers and link previewers get real content without running JavaScript. Out of the box each page also gets a &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, a canonical link, &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;twitter:card&lt;/code&gt; set to &lt;code&gt;summary_large_image&lt;/code&gt;, and &lt;code&gt;hreflang&lt;/code&gt; links when you have more than one locale. What it can't do on its own is write a good description for every page, pick a preview image per page, or decide which pages belong in search. This guide goes through each setting in order, for Docusaurus 3 (&lt;a href="https://docusaurus.io/docs/seo" rel="noopener noreferrer"&gt;Docusaurus: SEO&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Site URL and trailing slashes
&lt;/h2&gt;

&lt;p&gt;Every absolute URL Docusaurus writes (canonical, &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, sitemap entries) is built from &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;baseUrl&lt;/code&gt; in &lt;code&gt;docusaurus.config.js&lt;/code&gt;. Set them to the production address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://docs.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;trailingSlash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;trailingSlash&lt;/code&gt; explicitly. With the default (&lt;code&gt;undefined&lt;/code&gt;) Docusaurus leaves links as written and emits &lt;code&gt;/docs/intro/index.html&lt;/code&gt; for &lt;code&gt;docs/intro.md&lt;/code&gt;, but the canonical and sitemap URL is &lt;code&gt;/docs/intro&lt;/code&gt; without a slash. Many static hosts, GitHub Pages among them, answer that URL with a redirect to &lt;code&gt;/docs/intro/&lt;/code&gt;. The result is a canonical that points at a redirect, and Search Console reports such as "Page with redirect" or "Alternate page with proper canonical tag".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;File written for &lt;code&gt;docs/intro.md&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;URL in canonical and sitemap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/docs/intro/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/intro/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/docs/intro.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/intro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build/docs/intro/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/docs/intro&lt;/code&gt; (as linked)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick the value that matches how your host serves those files without redirecting; the Docusaurus deployment docs point to the &lt;a href="https://github.com/slorber/trailing-slash-guide" rel="noopener noreferrer"&gt;trailing slash guide&lt;/a&gt;, which lists the behavior of each host. Then check one page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://docs.example.com/docs/intro | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(HTTP|location)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 200 means the canonical URL is served directly. A 301 or 308 means &lt;code&gt;trailingSlash&lt;/code&gt; and your host disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Site-wide metadata and the default preview image
&lt;/h2&gt;

&lt;p&gt;Two settings in &lt;code&gt;themeConfig&lt;/code&gt; apply to every page (&lt;a href="https://docusaurus.io/docs/api/themes/configuration" rel="noopener noreferrer"&gt;theme configuration&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;themeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img/social-card.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twitter:site&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image&lt;/code&gt;&lt;/strong&gt; is the default &lt;code&gt;og:image&lt;/code&gt; and &lt;code&gt;twitter:image&lt;/code&gt;, a path relative to the &lt;code&gt;static&lt;/code&gt; folder (or a full URL). Docusaurus makes it absolute from &lt;code&gt;url&lt;/code&gt;. It can't be an SVG; Facebook, LinkedIn and X won't render one. Use a PNG or JPG around 1200×630.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/strong&gt; is a list of &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags; every field is passed straight to the tag, and an entry overrides a default with the same name, such as &lt;code&gt;twitter:card&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without &lt;code&gt;image&lt;/code&gt;, pages have no preview image at all unless the doc sets one. If a card still comes out without a picture, &lt;a href="https://headlark.com/guides/og-image-not-showing" rel="noopener noreferrer"&gt;og:image not showing&lt;/a&gt; covers the other causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Per-page front matter
&lt;/h2&gt;

&lt;p&gt;The docs, blog and pages plugins all accept the same four SEO fields (&lt;a href="https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter" rel="noopener noreferrer"&gt;docs front matter&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure the CLI&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Every option in the config file, with defaults and an example for monorepos.&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/img/social/configure.png&lt;/span&gt;
&lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cli&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;sidebar_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configuration&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;title&lt;/code&gt;&lt;/strong&gt; becomes &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; (followed by the site title) and &lt;code&gt;og:title&lt;/code&gt;. Without it Docusaurus uses the Markdown &lt;code&gt;#&lt;/code&gt; heading, then the doc id. &lt;code&gt;sidebar_label&lt;/code&gt; keeps a short name in the sidebar while the title says what the page is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt;&lt;/strong&gt; becomes both &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt; and &lt;code&gt;og:description&lt;/code&gt;. Front matter is the better place for it than a hand-written tag, which would need both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;image&lt;/code&gt;&lt;/strong&gt; becomes this page's &lt;code&gt;og:image&lt;/code&gt;, replacing &lt;code&gt;themeConfig.image&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;keywords&lt;/code&gt;&lt;/strong&gt; becomes a &lt;code&gt;&amp;lt;meta name="keywords"&amp;gt;&lt;/code&gt; tag. Google doesn't use that tag for ranking, so leave it out unless your own site search reads it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The first-line description
&lt;/h3&gt;

&lt;p&gt;If a doc has no &lt;code&gt;description&lt;/code&gt;, Docusaurus uses the first line of Markdown content. Imports, code blocks and the &lt;code&gt;#&lt;/code&gt; title are skipped, but other headings, admonitions and short intro sentences aren't. A page that opens with &lt;code&gt;## Prerequisites&lt;/code&gt; gets "Prerequisites" as its description, and so does every other page that opens the same way. To see what your build produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhoE&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="description" content="[^"]*"'&lt;/span&gt; build | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any description with a count above 1 is shared. &lt;a href="https://headlark.com/guides/duplicate-meta-descriptions-docs" rel="noopener noreferrer"&gt;Duplicate titles and descriptions on a docs site&lt;/a&gt; goes through finding and fixing them, including versioned docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Other head tags
&lt;/h2&gt;

&lt;p&gt;For tags the front matter doesn't cover, there are three places, from widest to narrowest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every page:&lt;/strong&gt; &lt;code&gt;headTags&lt;/code&gt; in &lt;code&gt;docusaurus.config.js&lt;/code&gt;, a list of objects with &lt;code&gt;tagName&lt;/code&gt;, &lt;code&gt;attributes&lt;/code&gt; and optionally &lt;code&gt;innerHTML&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preconnect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://fonts.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One Markdown or MDX page:&lt;/strong&gt; a &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; block in the file itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Migrating from v2

&amp;lt;head&amp;gt;
  &amp;lt;meta property="og:type" content="article" /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A React page&lt;/strong&gt; in &lt;code&gt;src/pages&lt;/code&gt;: the &lt;code&gt;@docusaurus/Head&lt;/code&gt; component. The default &lt;code&gt;Layout&lt;/code&gt; also takes &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; props, which fill the same tags as front matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Layout&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@theme/Layout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Head&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@docusaurus/Head&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Pricing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pricing"&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Plans and limits for the hosted version."&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://docs.example.com/img/social/pricing.png"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* page content */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags set this way go into the static HTML at build time, so crawlers see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keeping pages out of search
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One page:&lt;/strong&gt; &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;meta name="robots" content="noindex" /&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt; in the file. The sitemap plugin leaves out pages with a &lt;code&gt;noindex&lt;/code&gt; robots tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A page you link to privately:&lt;/strong&gt; &lt;code&gt;unlisted: true&lt;/code&gt; in front matter. It's built, but hidden from the sidebar, marked not to be indexed and left out of the sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An old docs version:&lt;/strong&gt; &lt;code&gt;noIndex: true&lt;/code&gt; on that version in the docs plugin's &lt;code&gt;versions&lt;/code&gt; option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The whole site&lt;/strong&gt; (a staging deployment): &lt;code&gt;noIndex: true&lt;/code&gt; at the top level of the config adds &lt;code&gt;noindex, nofollow&lt;/code&gt; to every page and skips the sitemap. Make sure it's off in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't use &lt;code&gt;robots.txt&lt;/code&gt; for this: it stops crawling, not indexing, and a blocked page can't show Google its &lt;code&gt;noindex&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The sitemap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@docusaurus/plugin-sitemap&lt;/code&gt; is part of &lt;code&gt;preset-classic&lt;/code&gt; and writes &lt;code&gt;sitemap.xml&lt;/code&gt; during &lt;code&gt;docusaurus build&lt;/code&gt; (&lt;a href="https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-sitemap" rel="noopener noreferrer"&gt;plugin-sitemap&lt;/a&gt;). Configure it through the preset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;classic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;lastmod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;changefreq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ignorePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tags/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/page/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ignorePatterns&lt;/code&gt;&lt;/strong&gt; takes globs matched against route paths. With a &lt;code&gt;baseUrl&lt;/code&gt; other than &lt;code&gt;/&lt;/code&gt;, include it in the pattern (&lt;code&gt;/repo/tags/**&lt;/code&gt;). Tag and paginated list pages are the usual candidates: thin pages that compete with your docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lastmod&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;'date'&lt;/code&gt; or &lt;code&gt;'datetime'&lt;/code&gt;) adds &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; from each file's last update. Google uses &lt;code&gt;lastmod&lt;/code&gt; when it's consistently accurate and ignores &lt;code&gt;changefreq&lt;/code&gt; and &lt;code&gt;priority&lt;/code&gt;, so setting those two to &lt;code&gt;null&lt;/code&gt; just makes the file smaller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;createSitemapItems&lt;/code&gt;&lt;/strong&gt; is a function for filtering or changing items when globs aren't enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point crawlers at it with a line in &lt;code&gt;static/robots.txt&lt;/code&gt;, &lt;code&gt;Sitemap: https://docs.example.com/sitemap.xml&lt;/code&gt;, and submit it in Search Console.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Structured data
&lt;/h2&gt;

&lt;p&gt;Recent Docusaurus 3 releases already emit &lt;code&gt;BreadcrumbList&lt;/code&gt; JSON-LD on docs pages and &lt;code&gt;BlogPosting&lt;/code&gt; on blog posts, so check a built page before adding your own. Site-wide data such as an &lt;code&gt;Organization&lt;/code&gt; goes in &lt;code&gt;headTags&lt;/code&gt; as a &lt;code&gt;script&lt;/code&gt; with &lt;code&gt;type: 'application/ld+json'&lt;/code&gt;; one-off data goes in the page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; block. &lt;a href="https://headlark.com/guides/json-ld-astro-docusaurus" rel="noopener noreferrer"&gt;JSON-LD for Astro and Docusaurus&lt;/a&gt; has working code, the types worth using and how to validate them.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Check the build
&lt;/h2&gt;

&lt;p&gt;Everything above ends up in the HTML in &lt;code&gt;build&lt;/code&gt;, so check that rather than the config. After &lt;code&gt;npm run build&lt;/code&gt;, list pages without a description and pages without a preview image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rL&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'name="description"'&lt;/span&gt; build
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rL&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.html'&lt;/span&gt; &lt;span class="s1"&gt;'property="og:image"'&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check a few deployed pages with the &lt;a href="https://headlark.com/tools/seo-checker" rel="noopener noreferrer"&gt;free SEO checker&lt;/a&gt;, which shows the Google result and the LinkedIn, Slack and X cards each page produces, or the &lt;a href="https://headlark.com/tools/site-checker" rel="noopener noreferrer"&gt;site checker&lt;/a&gt; for up to 50 pages from your sitemap at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating it with Headlark
&lt;/h2&gt;

&lt;p&gt;The settings above are one-time work. Writing a distinct title, description and image alt text for each of hundreds of docs is the part that doesn't get done. Headlark's &lt;a href="https://headlark.com/docs/build" rel="noopener noreferrer"&gt;build step&lt;/a&gt; runs after &lt;code&gt;docusaurus build&lt;/code&gt; on the HTML in &lt;code&gt;build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docusaurus build &amp;amp;&amp;amp; headlark build --site-url https://docs.example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;build&lt;/code&gt; after &lt;code&gt;headlark&lt;/code&gt; is the output folder. If you set a &lt;code&gt;baseUrl&lt;/code&gt; such as &lt;code&gt;/repo/&lt;/code&gt;, include it: &lt;code&gt;--site-url https://example.github.io/repo/&lt;/code&gt;. Set &lt;code&gt;HEADLARK_KEY&lt;/code&gt; in the build environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each page gets a title, a description and Open Graph and X tags written from its own content, and alt text for images that have none.&lt;/li&gt;
&lt;li&gt;Your canonical, robots directives and JSON-LD types are kept, so the Docusaurus defaults and your &lt;code&gt;noindex&lt;/code&gt; pages stay as they are.&lt;/li&gt;
&lt;li&gt;An existing &lt;code&gt;og:image&lt;/code&gt;, from front matter or &lt;code&gt;themeConfig.image&lt;/code&gt;, is kept. Headlark doesn't create images: with only &lt;code&gt;themeConfig.image&lt;/code&gt; set, every page keeps that one image, so set &lt;code&gt;image&lt;/code&gt; on the docs that deserve their own card.&lt;/li&gt;
&lt;li&gt;Docusaurus switches pages in the browser after the first load and applies its own title from route data, so the tab may show your original title after a click. Crawlers and link previewers load each page's HTML and get the rewritten tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The free plan covers one site and 25 pages. &lt;a href="https://headlark.com/app" rel="noopener noreferrer"&gt;Add your site&lt;/a&gt;, copy its secret key, and your next build ships with SEO.&lt;/p&gt;

</description>
      <category>docusaurus</category>
      <category>seo</category>
      <category>documentation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>og:image not showing in Slack, LinkedIn or X</title>
      <dc:creator>Headlark</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:36:18 +0000</pubDate>
      <link>https://dev.to/headlark/ogimage-not-showing-in-slack-linkedin-or-x-5c2i</link>
      <guid>https://dev.to/headlark/ogimage-not-showing-in-slack-linkedin-or-x-5c2i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://headlark.com/guides/og-image-not-showing" rel="noopener noreferrer"&gt;headlark.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You share a link and the card has a title and a description but no picture, or a tiny thumbnail instead of a large image. The image in a link preview comes from the page's &lt;code&gt;og:image&lt;/code&gt; tag (and &lt;code&gt;twitter:image&lt;/code&gt; for X). When it's missing, unreachable or in a format the previewer rejects, the card drops it without telling you why. This guide covers the image specifically; for the full set of tags, see &lt;a href="https://headlark.com/guides/open-graph-tags" rel="noopener noreferrer"&gt;Open Graph tags for every page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual causes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A relative URL.&lt;/strong&gt; &lt;code&gt;&amp;lt;meta property="og:image" content="/og.png"&amp;gt;&lt;/code&gt; doesn't work reliably. The &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph protocol&lt;/a&gt; expects a full URL, starting with &lt;code&gt;https://&lt;/code&gt; and your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wrong domain.&lt;/strong&gt; The URL points to &lt;code&gt;localhost&lt;/code&gt;, a preview deployment or a staging host, because the site URL wasn't set for the production build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An SVG.&lt;/strong&gt; Facebook, LinkedIn and X don't render SVG preview images. Use JPG, PNG, WebP or GIF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too small or too heavy.&lt;/strong&gt; LinkedIn shows images under 401 pixels wide as a thumbnail and recommends 1200×627 at most 5 MB (&lt;a href="https://www.linkedin.com/help/linkedin/answer/46687" rel="noopener noreferrer"&gt;LinkedIn: make your website shareable&lt;/a&gt;). An image around 1200×630 works on every network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The image is blocked.&lt;/strong&gt; The page loads in your browser, but the previewer's request for the image gets a 403, a login page or a bot challenge: password-protected previews, firewall bot rules, or hotlink protection on the image host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added by JavaScript.&lt;/strong&gt; Previewers read the HTML the server sends and don't run scripts. A tag set at runtime in a single-page app never reaches them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cached card.&lt;/strong&gt; LinkedIn and Facebook keep a preview for days. You fixed the tag, but the card still shows the old one until you ask them to fetch the page again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; instead of &lt;code&gt;property&lt;/code&gt;.&lt;/strong&gt; The Open Graph spec uses &lt;code&gt;property="og:image"&lt;/code&gt;; X's tags use &lt;code&gt;name="twitter:image"&lt;/code&gt;. Some previewers accept either, but not all, so follow the spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check what the previewer gets
&lt;/h2&gt;

&lt;p&gt;First the tag, from the raw HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/blog/post | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta[^&amp;gt;]+(og|twitter):image[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the image itself: it should return 200 and an image content type, with no redirect to a login page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com/og/post.png | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(HTTP|content-type|content-length)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://headlark.com/tools/seo-checker" rel="noopener noreferrer"&gt;free SEO checker&lt;/a&gt; does both from a server, the way previewers do, and shows the LinkedIn, Facebook, Slack and X cards it would produce. After a fix, refresh the cached card in &lt;a href="https://www.linkedin.com/post-inspector/" rel="noopener noreferrer"&gt;LinkedIn Post Inspector&lt;/a&gt; and the &lt;a href="https://developers.facebook.com/tools/debug/" rel="noopener noreferrer"&gt;Facebook Sharing Debugger&lt;/a&gt; (Scrape Again).&lt;/p&gt;

&lt;h2&gt;
  
  
  Astro
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;site&lt;/code&gt; in &lt;code&gt;astro.config.mjs&lt;/code&gt; to your production URL, then build the absolute URL in your layout with &lt;code&gt;Astro.site&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/layouts/Base.astro
const { title, description, image = '/og/default.png' } = Astro.props;
const imageURL = new URL(image, Astro.site).href;
---
&amp;lt;head&amp;gt;
  &amp;lt;meta property="og:title" content={title} /&amp;gt;
  &amp;lt;meta property="og:description" content={description} /&amp;gt;
  &amp;lt;meta property="og:image" content={imageURL} /&amp;gt;
  &amp;lt;meta name="twitter:card" content="summary_large_image" /&amp;gt;
  &amp;lt;meta name="twitter:image" content={imageURL} /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;site&lt;/code&gt;, &lt;code&gt;Astro.site&lt;/code&gt; is undefined and &lt;code&gt;new URL&lt;/code&gt; throws on a relative path, which is better than shipping a broken tag. Put shared images in &lt;code&gt;public/&lt;/code&gt; so their URL doesn't change between builds; an image imported through &lt;code&gt;astro:assets&lt;/code&gt; gets a hashed file name, so pass its &lt;code&gt;.src&lt;/code&gt; to &lt;code&gt;new URL&lt;/code&gt; rather than writing the path by hand.&lt;/p&gt;

&lt;p&gt;Starlight sets &lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt; and &lt;code&gt;twitter:card&lt;/code&gt; but no image by default. Add one for every page with the &lt;code&gt;head&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;starlight&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;og:image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://docs.example.com/og.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Next.js (App Router)
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;metadataBase&lt;/code&gt; once in the root layout. Relative image paths in metadata are then resolved against it; without it, a relative path fails the build (&lt;a href="https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase" rel="noopener noreferrer"&gt;Next.js: metadataBase&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;metadataBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/og/default.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pitfall: metadata from layouts and pages is merged shallowly. A page that sets any &lt;code&gt;openGraph&lt;/code&gt; field replaces the whole &lt;code&gt;openGraph&lt;/code&gt; object from the layout, image included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx: this page loses the layout's og:image&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat the image in the page's &lt;code&gt;openGraph&lt;/code&gt;, spread a shared object into it, or use the file convention: an &lt;code&gt;opengraph-image.png&lt;/code&gt; (or &lt;code&gt;.jpg&lt;/code&gt;) in a route folder applies to that route and the routes below it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hugo
&lt;/h2&gt;

&lt;p&gt;Use Hugo's embedded templates in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ partial "opengraph.html" . }}
{{ partial "twitter_cards.html" . }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They take the image from the page's &lt;code&gt;images&lt;/code&gt; front matter, then from a page resource whose name contains &lt;code&gt;feature&lt;/code&gt;, &lt;code&gt;cover&lt;/code&gt; or &lt;code&gt;thumbnail&lt;/code&gt;, then from &lt;code&gt;params.images&lt;/code&gt; in your site configuration (&lt;a href="https://gohugo.io/templates/embedded/" rel="noopener noreferrer"&gt;Hugo: embedded templates&lt;/a&gt;). Give the site a default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# hugo.toml&lt;/span&gt;
&lt;span class="py"&gt;baseURL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'https://example.com/'&lt;/span&gt;
&lt;span class="nn"&gt;[params]&lt;/span&gt;
  &lt;span class="py"&gt;images&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'og/default.png'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My post&lt;/span&gt;
&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;og/my-post.png'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Absolute URLs are built from &lt;code&gt;baseURL&lt;/code&gt;, so check it's the production address in the environment that deploys. Older themes call the templates as &lt;code&gt;{{ template "_internal/opengraph.html" . }}&lt;/code&gt;; recent Hugo versions use the partial form shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the tag is right and the card still has no image
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the image URL in a private window. If you see a login page, so does the previewer.&lt;/li&gt;
&lt;li&gt;Check your host's bot protection and any password protection on the domain you shared.&lt;/li&gt;
&lt;li&gt;Look for two &lt;code&gt;og:image&lt;/code&gt; tags with different values, for example one from your theme and one from a plugin. Previewers usually take the first.&lt;/li&gt;
&lt;li&gt;Ask LinkedIn and Facebook to fetch the page again. Slack and X also cache cards, so a link shared before the fix may keep its old preview.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Automating it with Headlark
&lt;/h2&gt;

&lt;p&gt;Headlark's &lt;a href="https://headlark.com/docs/build" rel="noopener noreferrer"&gt;build step&lt;/a&gt; writes the link preview tags into each page's HTML after your build. For the image it's deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It keeps your page's existing &lt;code&gt;og:image&lt;/code&gt; if there is one, otherwise it picks the page's most representative image.&lt;/li&gt;
&lt;li&gt;It makes the URL absolute from your &lt;code&gt;--site-url&lt;/code&gt; and never uses an SVG or a &lt;code&gt;data:&lt;/code&gt; URL, so a page whose only image is an SVG logo gets a small card.&lt;/li&gt;
&lt;li&gt;It doesn't create images. If you want branded cards, generate them yourself and set &lt;code&gt;og:image&lt;/code&gt;; Headlark then fills in the title, description and the rest around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The free plan covers one site and 25 pages. &lt;a href="https://headlark.com/app" rel="noopener noreferrer"&gt;Add your site&lt;/a&gt;, copy its secret key, and your next build ships with SEO.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>opengraph</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
