Web

Add HTML Head Link Tags (Canonical) in Plotly Dash Guide

Three ways to add <link> tags (including canonical links) into the HTML head of a Plotly Dash app: override app.index_string, use Dash hooks, or subclass Dash.

1 answer 1 view

How to add a <link> tag to the HTML <head> section in a Plotly Dash application?

I know how to add <meta> tags using the meta_tags parameter in the Dash constructor:

python
from dash import Dash, html

app = Dash(
 __name__, 
 meta_tags= [
 {'name': 'description', 'content': 'Description of the site'},
 {'http-equiv': 'content-language', 'content': 'pt-br'},
 {'http-equiv': 'Link', 'content': '<https://example.com>; rel="canonical"'}
 ]
)

app.layout = [
 html.Header(
 html.Link(href='https://example.com', rel='canonical')
 ),
 html.P('Some text')
]

if __name__ == '__main__':
 app.run(debug=True)

However, adding a Link meta tag does not work correctly, and using html.Link in the layout places it in the body instead of the <head>. How can I properly include <link> tags like a canonical link in the <head> section?

To properly add a <link> tag, like for a canonical url, into the html head of your Plotly Dash app, skip html.Link in the layout—it dumps everything into the body—and bypass meta_tags, which handles only <meta> elements. Instead, tweak the app’s underlying HTML template using app.index_string for a quick static fix or Dash’s hooks for something more elegant and dynamic. This keeps your rel canonical url where browsers and SEO crawlers expect it: right in the html head.


Contents


Why Standard Methods Fail for Link Tags

Ever inspect the page source and spot your html.Link(rel='canonical', href='https://example.com') chilling in the <body>? That’s Dash for you. Components like html.Link are meant for dynamic content inside the layout—they render wherever you drop them, not in the static html head.

And meta_tags? Great for viewport or description meta elements, as your code shows, but try slipping in a <link> via {'http-equiv': 'Link', 'content': '<https://example.com>; rel="canonical"'}. It mangles. Browsers ignore it or treat it wrong because <link> isn’t a meta tag—it’s its own beast for stylesheets, icons, or that crucial canonical url to dodge duplicate content penalties.

The html head needs static insertion before the <body> kicks off. Dash serves a template via app.index_string, so we hack that. No big deal, but pick your method based on static vs. dynamic needs. Static? String replace. Dynamic per route? Hooks or subclassing.


Method 1: Override app.index_string

Simplest hack, no subclassing required. Grab Dash’s default HTML skeleton, swap in your html head link, and set it back. Works for any static <link>.

python
from dash import Dash, html

app = Dash(__name__)

# Your canonical or whatever
canonical_link = '<link rel="canonical" href="https://example.com/page">'

# Inject before </head>
app.index_string = app.index_string.replace("</head>", f"{canonical_link}</head>")

app.layout = html.Div("Your app here")

if __name__ == "__main__":
 app.run(debug=True)

Boom. View source—it’s there in the html head. The StackOverflow thread on placing link tags nails this for quick wins. Add multiple? Chain replaces or build a snippet.

But watch out: debug mode regenerates the template sometimes. Production? Fine. Rerun the app, and it’s baked in. Need a favicon too? Toss <link rel="icon" href="/favicon.ico"> alongside.

This scales poorly for route-specific rel canonical urls, though. Page changes? Hardcode hell. Next methods fix that.


Method 2: Use Dash Hooks (Recommended) {#dash-hooks)

Dash 2.0+ brought hooks—decorators for lifecycle tweaks without mess. @hooks.index() lets you intercept and modify the full HTML string post-render, perfect for injecting into html head.

python
from dash import Dash, html, hooks

app = Dash(__name__)

@hooks.index()
def inject_canonical(app_index: str) -> str:
 head_start = app_index.find("<head>") + len("<head>")
 canonical = '<link rel="canonical" href="https://example.com/current-page">'
 return app_index[:head_start] + canonical + app_index[head_start:]

app.layout = html.Div("Dynamic head magic")

if __name__ == "__main__":
 app.run(debug=True)

Finds <head>, splices your html head link. The Plotly community forum demos this for responsive tweaks, but it shines for canonical url. Dynamic? Pull request.url from Flask’s request (Dash sits on Flask).

python
from flask import request

@hooks.index()
def dynamic_canonical(app_index: str) -> str:
 canonical_href = request.url # Or build from path
 link = f'<link rel="canonical" href="{canonical_href}">'
 # Same insertion logic...

Clean. No template breakage. Hooks run per request—ideal for multi-page apps.


Method 3: Subclass Dash for Full Control

Want total ownership? Subclass Dash and override index(). Builds the html head from scratch, weaving in scripts, CSS, and your links.

python
from dash import Dash, html
import plotly.dash as dash # For internals

class CustomDash(Dash):
 def index(self, *args, **kwargs):
 scripts = self._generate_scripts_html()
 css = self._generate_css_dist_html()
 canonical = '<link rel="canonical" href="https://example.com">'
 
 head = f"""
 <head>
 {css}
 {canonical}
 <title>{self.title}</title>
 {scripts}
 </head>
 """.strip()
 
 return super().index(head=head) # Or craft full HTML

app = CustomDash(__name__)
app.layout = html.Div("Custom head")

Drawn from Dash GitHub issue #170, this mirrors how Dash builds pages. Flexible for head icon html or stylesheets. Downside? Duplicates internals if Dash updates. Hooks edge it for maintenance.

Pro tip: Combine with external_stylesheets for CSS links, but <link rel="canonical"> stays custom.


Making Canonical Links Dynamic

Canonical url per page? Essential for SPAs or multi-route Dash. Use Flask’s request in hooks or subclass.

python
from dash import Dash, html, hooks
from flask import request
from urllib.parse import urlparse

@hooks.index()
def smart_canonical(app_index: str) -> str:
 base_url = "https://example.com"
 path = request.path # /page1, /page2
 canonical_href = f"{base_url}{path}"
 link = f'<link rel="canonical" href="{canonical_href}">'
 
 head_idx = app_index.find("</head>")
 return app_index[:head_idx] + link + app_index[head_idx:]

app = Dash(__name__)
# Your layout...

Parses the current path, builds rel canonical url. Test in incognito—inspect html head, see it adapt. For Dash Pages (multipage), hook into dcc.Location callbacks too, but hooks handle server-side.

Edge cases? Trailing slashes? Normalize with urlparse. HTTPS only? Force it. SEO tools like Google Search Console love this—prevents indexing duplicates.

The Dash meta docs warn against body-placed tags, pushing template hacks like these.


Sources

  1. Place a link tag in the HTML head tag - StackOverflow
  2. Passing meta tags to head and footer - Dash GitHub Issue #170
  3. Meta Dash for Python Documentation
  4. Adding meta tags to head - Plotly Community Forum
  5. Allow optional header and footer - Dash GitHub PR #171

Conclusion

Nail that html head link in Dash with hooks for most cases—they’re modern, dynamic, and low-friction. Static needs? app.index_string wins on speed. Your canonical url now fights duplicate content like a champ, boosting SEO without body clutter. Test across routes, inspect source, and you’re golden—browsers and bots will thank you.

Authors
Verified by moderation
Moderation
Add HTML Head Link Tags (Canonical) in Plotly Dash Guide