Kotlin Open Graph Meta Tags: Add OG to Ktor and Spring Boot

How to add og:title, og:image, og:description, and Twitter card meta tags to Kotlin web apps using Ktor, Spring Boot MVC, and Thymeleaf.

OG tags with Ktor + Freemarker/HTML DSL

Ktor's HTML DSL lets you inject OG meta tags server-side for any route:

// Application.kt
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.routing.*
import kotlinx.html.*

fun Application.configureRouting() {
    routing {
        get("/blog/{slug}") {
            val slug = call.parameters["slug"] ?: return@get
            val post = BlogService.getPost(slug)

            call.respondHtml {
                head {
                    title { +post.title }
                    meta(name = "description", content = post.excerpt)

                    // Open Graph
                    meta(content = post.title) { attributes["property"] = "og:title" }
                    meta(content = post.excerpt) { attributes["property"] = "og:description" }
                    meta(content = "https://myapp.com/og/${slug}.png") { attributes["property"] = "og:image" }
                    meta(content = "https://myapp.com/blog/${slug}") { attributes["property"] = "og:url" }
                    meta(content = "article") { attributes["property"] = "og:type" }

                    // Twitter / X
                    meta(name = "twitter:card", content = "summary_large_image")
                    meta(name = "twitter:title", content = post.title)
                    meta(name = "twitter:description", content = post.excerpt)
                    meta(name = "twitter:image", content = "https://myapp.com/og/${slug}.png")

                    link(rel = "canonical", href = "https://myapp.com/blog/${slug}")
                }
                body {
                    h1 { +post.title }
                    p { +post.body }
                }
            }
        }
    }
}

OG tags with Spring Boot + Thymeleaf

If you're using Spring Boot with Thymeleaf for SSR, add OG tags to your layout template:

<!-- src/main/resources/templates/layout.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8" />
  <title th:text="${pageTitle} ?: 'My App'">My App</title>
  <meta name="description" th:content="${pageDescription} ?: 'Default description'" />

  <!-- Open Graph -->
  <meta property="og:title"       th:content="${ogTitle} ?: ${pageTitle} ?: 'My App'" />
  <meta property="og:description" th:content="${ogDescription} ?: ${pageDescription}" />
  <meta property="og:image"       th:content="${ogImage} ?: 'https://myapp.com/images/default-og.png'" />
  <meta property="og:url"         th:content="${ogUrl} ?: '/'" />
  <meta property="og:type"        th:content="${ogType} ?: 'website'" />

  <!-- Twitter -->
  <meta name="twitter:card"        content="summary_large_image" />
  <meta name="twitter:title"       th:content="${ogTitle} ?: ${pageTitle}" />
  <meta name="twitter:description" th:content="${ogDescription} ?: ${pageDescription}" />
  <meta name="twitter:image"       th:content="${ogImage}" />

  <link rel="canonical" th:href="${ogUrl}" />
</head>
<body th:replace="~{::body}">
  <!-- page content -->
</body>
</html>
// PostController.kt
@Controller
@RequestMapping("/blog")
class PostController(private val blogService: BlogService) {

    @GetMapping("/{slug}")
    fun showPost(@PathVariable slug: String, model: Model): String {
        val post = blogService.getPost(slug)

        model.addAttribute("pageTitle", post.title)
        model.addAttribute("ogTitle", post.title)
        model.addAttribute("ogDescription", post.excerpt.take(160))
        model.addAttribute("ogImage", "https://myapp.com/og/${slug}.png")
        model.addAttribute("ogUrl", "https://myapp.com/blog/${slug}")
        model.addAttribute("ogType", "article")
        model.addAttribute("post", post)

        return "blog/show"
    }
}

Kotlin/Ktor as API backend (Next.js frontend)

If Ktor is your backend API and a Next.js or other SSR frontend handles rendering, add OG meta tags in the frontend layer:

// app/blog/[slug]/page.tsx (Next.js App Router)
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.myapp.com/blog/${params.slug}`).then(r => r.json());

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: `https://myapp.com/og/${params.slug}.png`, width: 1200, height: 630 }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [`https://myapp.com/og/${params.slug}.png`],
    },
  };
}

Common Kotlin OG mistakes

  • JSON API only, no SSR — social crawlers can't read OG tags from JSON responses. If Ktor/Spring Boot serves only JSON, OG must live in your frontend SSR layer.
  • Relative image URLsog:image must be an absolute HTTPS URL. Scrapers don't resolve relative paths.
  • Wrong image dimensions — use 1200×630px (1.91:1 ratio).
  • Missing Twitter fallback — always include both OG and Twitter card tags; some platforms prefer one over the other.

Verify your Kotlin OG tags

After deploying, paste your URL into OGFixer to see exactly how your Kotlin app's link previews appear on Twitter, LinkedIn, Slack, and Discord — and catch missing or broken tags before they go live.