Lit Open Graph Meta Tags: OG for Lit Web Components

Lit and LitElement render in the browser — here's how to ensure og:title, og:image, og:description, and Twitter card meta tags are served in server-rendered HTML for social crawlers.

Why Lit OG tags must be server-side

Lit is a Google-built library for creating fast, lightweight web components. Like all web component libraries, Lit renders in the browser — your <my-app> element only produces its shadow DOM after JavaScript runs.

Social crawlers (Twitterbot, LinkedInBot, Discordbot, Slackbot) fetch raw HTML and parse it without executing JavaScript. This means any OG tags you set inside a Lit component are invisible to crawlers. The solution: serve OG tags in the server-rendered HTML shell, not from within Lit components.

You have three paths: (1) hard-code OG tags in a static HTML shell, (2) use @lit-labs/ssr for server-side rendering with dynamic tags, or (3) use a traditional server (Express, Django, Rails) to inject per-route meta tags before serving the HTML.

Static HTML approach

For simple Lit apps with a small number of shareable pages, the fastest solution is a static HTML file per route with OG tags hard-coded in the <head>. Lit components hydrate on top.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Lit App — Web Components Made Easy</title>
    <meta name="description" content="Build fast web components with Lit and LitElement." />

    <!-- Open Graph -->
    <meta property="og:type"        content="website" />
    <meta property="og:title"       content="My Lit App — Web Components Made Easy" />
    <meta property="og:description" content="Build fast web components with Lit." />
    <meta property="og:image"       content="https://yourdomain.com/og/home.png" />
    <meta property="og:url"         content="https://yourdomain.com/" />

    <!-- Twitter Card -->
    <meta name="twitter:card"        content="summary_large_image" />
    <meta name="twitter:title"       content="My Lit App — Web Components Made Easy" />
    <meta name="twitter:description" content="Build fast web components with Lit." />
    <meta name="twitter:image"       content="https://yourdomain.com/og/home.png" />

    <!-- Lit component -->
    <script type="module" src="/src/my-app.js"></script>
  </head>
  <body>
    <my-app></my-app>
  </body>
</html>

SSR with @lit-labs/ssr

@lit-labs/ssr is an official Lit package that renders Lit components to a stream of HTML on the server using Node.js. You can inject OG tags into the HTML stream before the component output.

// server.js (Node.js + Express)
import express from 'express';
import { render } from '@lit-labs/ssr';
import { collectResultSync } from '@lit-labs/ssr/lib/render-result.js';
import { html } from 'lit';

// Import your Lit components so they register
import './src/my-app.js';

const app = express();

app.get('/blog/:slug', async (req, res) => {
  const post = await fetchPost(req.params.slug);

  // Render the Lit component to an HTML string
  const ssrResult = render(html`<my-blog-post .post=${post}></my-blog-post>`);
  const body = collectResultSync(ssrResult);

  res.send(`<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>${escapeHtml(post.title)} | My Blog</title>
    <meta name="description" content="${escapeHtml(post.excerpt)}" />
    <meta property="og:type"        content="article" />
    <meta property="og:title"       content="${escapeHtml(post.title)}" />
    <meta property="og:description" content="${escapeHtml(post.excerpt)}" />
    <meta property="og:image"       content="${escapeHtml(post.ogImage)}" />
    <meta property="og:url"         content="https://yourdomain.com/blog/${post.slug}" />
    <meta name="twitter:card"       content="summary_large_image" />
    <meta name="twitter:title"      content="${escapeHtml(post.title)}" />
    <meta name="twitter:image"      content="${escapeHtml(post.ogImage)}" />
    <script type="module" src="/src/my-app.js"></script>
  </head>
  <body>
    ${body}
  </body>
</html>`);
});

app.listen(3000);

Per-route OG with a backend server

If you use a Lit app in front of an existing backend (Django, Rails, FastAPI, etc.), the backend can inject per-route OG tags into the HTML template it serves to the browser. Lit hydrates client-side without needing to know about the meta tags at all.

# Django view (Python)
from django.shortcuts import render
from .models import BlogPost

def post_detail(request, slug):
    post = BlogPost.objects.get(slug=slug)
    return render(request, 'post.html', {
        'title': post.title,
        'description': post.excerpt,
        'og_image': post.og_image_url,
        'og_url': request.build_absolute_uri(),
    })

# templates/post.html
# <head>
#   <meta property="og:title"       content="{{ title }}" />
#   <meta property="og:description" content="{{ description }}" />
#   <meta property="og:image"       content="{{ og_image }}" />
#   <meta property="og:url"         content="{{ og_url }}" />
#   <script type="module" src="/static/my-app.js"></script>
# </head>
# <body>
#   <my-blog-post slug="{{ slug }}"></my-blog-post>
# </body>

OG tag checklist for Lit

  • Never set OG tags from inside a Lit component — crawlers won't see them.
  • Always inject OG tags in the server-rendered HTML <head>.
  • Use @lit-labs/ssr for fully dynamic SSR, or a backend template engine for simpler setups.
  • Use absolute URLs for og:image.
  • Image dimensions: 1200×630 px, PNG or JPG, under 5 MB.
  • Set twitter:card to summary_large_image.
  • Verify with OGFixer after each deploy.

Check your Lit app's social previews

Paste any Lit app URL into OGFixer to see how it looks on Twitter, LinkedIn, Discord, and Slack — free, no login.

Related Guides