docs/content/templates/social.md (3160B)
1 +++
2 title = "Social cards and structured data"
3 description = "Markup you own, from values that already exist"
4 weight = 50
5 toc = true
6 +++
7
8 sqzass injects nothing into `<head>`. There is no injection point and there will
9 not be one — a generator that quietly adds tags is a generator you cannot fully
10 read the output of. Everything below is markup you put in your own `base.html`,
11 built from values already in the template context.
12
13 ## OpenGraph and Twitter
14
15 Without these, every page of your site renders as a bare URL in Slack, Discord
16 and KakaoTalk.
17
18 ```html
19 <meta property="og:type" content="{{ "website" if page.is_section else "article" }}">
20 <meta property="og:site_name" content="{{ site.title }}">
21 <meta property="og:title" content="{{ page.title }}">
22 <meta property="og:url" content="{{ page.permalink }}">
23 <meta property="og:locale" content="{{ "ko_KR" if page.language == "ko" else "en_US" }}">
24 {%- if page.description %}
25 <meta property="og:description" content="{{ page.description }}">
26 {%- endif %}
27 <meta name="twitter:card" content="summary">
28 ```
29
30 `summary`, not `summary_large_image` — the large variant needs an image, and
31 declaring it without one produces an empty box rather than a nicer card. If you
32 have a per-page image, put it in front matter and switch:
33
34 ```toml
35 +++
36 title = "Installation"
37 [extra]
38 image = "/images/install.png"
39 +++
40 ```
41
42 ```html
43 {%- if page.extra.image %}
44 <meta property="og:image" content="{{ site.origin }}{{ page.extra.image }}">
45 <meta name="twitter:card" content="summary_large_image">
46 {%- else %}
47 <meta name="twitter:card" content="summary">
48 {%- endif %}
49 ```
50
51 `og:image` must be absolute, which is what `site.origin` is for.
52
53 ## Breadcrumbs as JSON-LD
54
55 This is the one piece of structured data that visibly changes a Google result
56 for a documentation site: the result shows `Home › Writing content › Front matter`
57 instead of a bare URL.
58
59 ```html
60 {%- if page.section %}
61 <script type="application/ld+json">
62 {
63 "@context": "https://schema.org",
64 "@type": "BreadcrumbList",
65 "itemListElement": [
66 {"@type": "ListItem", "position": 1, "name": "{{ site.title }}", "item": "{{ site.origin }}{{ site.base_path }}/"},
67 {"@type": "ListItem", "position": 2, "name": "{{ page.section.title }}", "item": "{{ site.origin }}{{ page.section.url }}"},
68 {"@type": "ListItem", "position": 3, "name": "{{ page.title }}", "item": "{{ page.permalink }}"}
69 ]
70 }
71 </script>
72 {%- endif %}
73 ```
74
75 Note the `{%- if page.section %}`: top-level pages have no section, and a
76 breadcrumb with a missing rung is worse than none.
77
78 > [!WARNING]
79 > This block is inside `<script>`, where HTML escaping is wrong — a title
80 > containing `"` produces invalid JSON. Keep titles free of quotes, or drop the
81 > structured data rather than shipping JSON that silently fails to parse. This
82 > is the one place in a template where our escaping does not protect you.
83
84 ## Site-level
85
86 ```html
87 <script type="application/ld+json">
88 {
89 "@context": "https://schema.org",
90 "@type": "WebSite",
91 "name": "{{ site.title }}",
92 "url": "{{ site.origin }}{{ site.base_path }}/"
93 }
94 </script>
95 ```
96
97 One block, on every page, is enough. Search engines read it once.