feat(playground): add SEO metadata and social sharing card

Search engines and social crawlers previously saw an empty page: the
playground shipped a bare <div id="root"> with no description, canonical,
or Open Graph tags — so shared links (the playground's own core feature)
rendered without a preview card.

- Add meta description, canonical URL, and Open Graph / Twitter cards
  with a generated 1200x630 preview image
- Ship static fallback content inside #root, replaced by React on mount,
  so crawlers and card bots (which never run JS) read real content
- Promote the app title to a proper <h1>
- Register /playground/ in the sitemap during deploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pull/1245/head
mingrammer 4 weeks ago
parent a13ce09940
commit 8cef7984f6

@ -69,6 +69,23 @@ jobs:
mkdir -p _site/playground mkdir -p _site/playground
cp -a playground/dist/. _site/playground/ cp -a playground/dist/. _site/playground/
test -f _site/CNAME || echo "diagrams.mingrammer.com" > _site/CNAME test -f _site/CNAME || echo "diagrams.mingrammer.com" > _site/CNAME
# Docusaurus only lists its own pages, so add the playground to the
# sitemap it generated.
python3 - <<'PY'
from pathlib import Path
sitemap = Path("_site/sitemap.xml")
entry = (
"<url><loc>https://diagrams.mingrammer.com/playground/</loc>"
"<changefreq>weekly</changefreq><priority>0.8</priority></url>"
)
xml = sitemap.read_text()
if "/playground/" not in xml:
sitemap.write_text(xml.replace("</urlset>", entry + "</urlset>"))
print("added playground to sitemap")
else:
print("playground already in sitemap")
PY
- name: Deploy to gh-pages - name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4 uses: peaceiris/actions-gh-pages@v4

@ -9,3 +9,4 @@ playwright-report/
vite.config.js vite.config.js
vite.config.d.ts vite.config.d.ts
.omc/ .omc/
.vite/

@ -3,9 +3,41 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagrams Playground</title> <title>Diagrams Playground — Diagram as Code in your browser</title>
<meta
name="description"
content="Write Python and watch cloud architecture diagrams render instantly in your browser — nothing to install. Powered by the diagrams library running on Pyodide."
/>
<link rel="canonical" href="https://diagrams.mingrammer.com/playground/" />
<!-- Real diagrams project logo; relative href survives sub-path deploys. --> <!-- Real diagrams project logo; relative href survives sub-path deploys. -->
<link rel="icon" type="image/png" href="diagrams-logo.png" /> <link rel="icon" type="image/png" href="diagrams-logo.png" />
<!-- Social cards. These need absolute URLs, and the crawlers that read
them never execute JavaScript — which is also why #root ships with
static fallback content below. -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Diagrams" />
<meta property="og:url" content="https://diagrams.mingrammer.com/playground/" />
<meta property="og:title" content="Diagrams Playground — Diagram as Code in your browser" />
<meta
property="og:description"
content="Write Python and watch cloud architecture diagrams render instantly in your browser — nothing to install."
/>
<meta property="og:image" content="https://diagrams.mingrammer.com/playground/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="The Diagrams Playground: Python code on the left, a rendered cloud architecture diagram on the right."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Diagrams Playground — Diagram as Code in your browser" />
<meta
name="twitter:description"
content="Write Python and watch cloud architecture diagrams render instantly in your browser — nothing to install."
/>
<meta name="twitter:image" content="https://diagrams.mingrammer.com/playground/og-image.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
@ -25,7 +57,42 @@
</script> </script>
</head> </head>
<body> <body>
<div id="root"></div> <!-- React replaces this subtree on mount (createRoot clears the
container). Until then it gives crawlers and social-card bots real
content to read, and gives visitors something better than a blank
page while the bundle loads. Styles are inline so it renders even
before the CSS bundle arrives. -->
<div id="root">
<main
style="
max-width: 40rem;
margin: 0 auto;
padding: 4rem 1.5rem;
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
color: #111827;
line-height: 1.6;
"
>
<h1 style="font-size: 1.6rem; margin: 0 0 1rem">Diagrams Playground</h1>
<p style="margin: 0 0 1rem">
Write <strong>diagrams</strong> code in Python and see the rendered cloud architecture diagram
instantly — right in your browser, with nothing to install.
</p>
<p style="margin: 0 0 1rem; color: #6b7280">
Loading the playground…
</p>
<noscript>
<p style="margin: 0 0 1rem; color: #b91c1c">
JavaScript is required to run the playground.
</p>
</noscript>
<p style="margin: 0; color: #6b7280">
<a href="https://diagrams.mingrammer.com/">Documentation</a>
·
<a href="https://github.com/mingrammer/diagrams">GitHub</a>
</p>
</main>
</div>
<script type="module" src="./src/main.tsx"></script> <script type="module" src="./src/main.tsx"></script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

@ -101,7 +101,9 @@ export default function Toolbar({ status, onShare, shared }: Props) {
<span className="app-mark" aria-hidden="true"> <span className="app-mark" aria-hidden="true">
<img src="diagrams-logo.png" alt="" width={22} height={22} /> <img src="diagrams-logo.png" alt="" width={22} height={22} />
</span> </span>
<strong className="toolbar-title">Diagrams Playground</strong> {/* The page's only h1 the app is a single tool, and crawlers that do
run JS should still find a real heading. */}
<h1 className="toolbar-title">Diagrams Playground</h1>
<div className={`status-pill status-pill--${variant}`}> <div className={`status-pill status-pill--${variant}`}>
<span className="status-dot" aria-hidden="true" /> <span className="status-dot" aria-hidden="true" />
<span data-testid="status">{status}</span> <span data-testid="status">{status}</span>

Loading…
Cancel
Save