Skip to content

Commit 2cb77ab

Browse files
committed
add structured data and fix up styles
1 parent 2115987 commit 2cb77ab

11 files changed

Lines changed: 144 additions & 25 deletions

File tree

‎scripts/prepare-og.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ async function walk(dir) {
1515
const file = `${dir}/${entry.name}`;
1616
if (entry.isDirectory()) { await walk(file); continue; }
1717
if (!/\.mdx?$/.test(file)) continue;
18+
// Starlight's error page is not an article or an OG/Markdown export.
19+
if (file === 'src/content/docs/404.mdx') continue;
1820
const { data } = matter(await fs.readFile(file, 'utf8'));
1921
if (typeof data.slug !== 'string') throw new Error(`Explicit slug required: ${file}`);
2022
const route = data.slug ? `/${data.slug}/` : '/';

‎src/components/Breadcrumbs.astro‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
---
2-
import navigation from '../navigation.mjs';
2+
import { breadcrumbs } from '../lib/breadcrumbs.mjs';
33
const { entry } = Astro.locals.starlightRoute;
44
const route = Astro.url.pathname;
5-
type Crumb = { label: string; link: string };
6-
type NavItem = { label: string; link?: string; items?: NavItem[] };
7-
const candidates: Crumb[][] = [];
8-
function findPaths(items: NavItem[], parents: Crumb[] = []) {
9-
for (const item of items) {
10-
if (item.link === route) candidates.push(parents);
11-
if (item.items) {
12-
const overview = item.items.find(child => child.label === 'Overview' && child.link);
13-
const ancestors = overview?.link ? [...parents, { label: item.label, link: overview.link }] : parents;
14-
findPaths(item.items, ancestors);
15-
}
16-
}
17-
}
18-
findPaths(navigation);
19-
// A page can appear in more than one sidebar group. Prefer its URL hierarchy.
20-
const score = (path: Crumb[]) => path.filter(crumb => route.startsWith(crumb.link)).length;
21-
const parents = entry.data.changelog
22-
? [{ label: 'Changelog', link: '/changelog/' }]
23-
: (candidates.sort((a, b) => score(b) - score(a))[0] || []);
24-
const crumbs = [{ label: 'Docs', link: '/' }, ...parents.filter(crumb => crumb.link !== route && crumb.link !== '/')];
5+
const crumbs = breadcrumbs(route, entry.data.page_title ?? entry.data.title, Boolean(entry.data.changelog)).slice(0, -1);
256
---
267
{route !== '/' && <nav class="breadcrumbs" aria-label="Breadcrumb">
278
<ol>

‎src/components/Head.astro‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
---
22
import pages from '../generated/pages.json';
3+
import { structuredData, serializeStructuredData } from '../lib/structured-data.mjs';
34
const page = pages.find(page => page.route === Astro.url.pathname);
45
const { head, entry } = Astro.locals.starlightRoute;
6+
const jsonLd = page && structuredData({
7+
origin: Astro.site?.href ?? Astro.url.origin,
8+
route: page.route,
9+
title: entry.data.page_title ?? entry.data.title,
10+
description: entry.data.description,
11+
image: page.og.image,
12+
datePublished: entry.data.changelog?.date,
13+
overview: page.route === '/' || page.route === '/changelog/' || /\/index\.mdx?$/.test(page.file),
14+
});
515
const siteTitle = 'HTML/CSS to Image API Docs';
616
const brandedTitle = (title: string) => title === siteTitle || title.endsWith(` | ${siteTitle}`)
717
? title : `${title} | ${siteTitle}`;
@@ -13,6 +23,8 @@ const tags = head.filter(tag => tag.tag !== 'title' &&
1323
: tag);
1424
---
1525
<title>{brandedTitle(entry.data.title)}</title>
26+
{jsonLd && <script type="application/ld+json" set:html={serializeStructuredData(jsonLd)} />}
27+
{entry.data.changelog && <meta property="article:published_time" content={entry.data.changelog.date} />}
1628
<meta data-pagefind-filter="kind[content]" content={entry.data.changelog ? 'changelog' : 'docs'} />
1729
<link rel="preconnect" href="https://fonts.googleapis.com" />
1830
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -24,6 +36,8 @@ const tags = head.filter(tag => tag.tag !== 'title' &&
2436
{page?.og.image && <>
2537
<meta property="og:image" content={page.og.image} />
2638
<meta name="twitter:image" content={page.og.image} />
39+
<meta property="og:image:alt" content={page.og.title} />
40+
<meta name="twitter:image:alt" content={page.og.title} />
2741
<meta property="og:image:width" content="1200" />
2842
<meta property="og:image:height" content="630" />
2943
</>}

‎src/components/PageTitle.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const markdownPath = route === '/' ? '/index.md' : `${route.replace(/\/$/, '')}.
1010
<Breadcrumbs />
1111
<div class="page-heading">
1212
{entry.data.page_title ? <h1 id="_top">{pageTitle}</h1> : <DefaultPageTitle />}
13-
<PageActions markdownPath={markdownPath} title={pageTitle} />
13+
{entry.id !== '404' && <PageActions markdownPath={markdownPath} title={pageTitle} />}
1414
</div>
1515
{entry.data.changelog && <p class="published"><time data-changelog-date datetime={entry.data.changelog.date}>{new Date(`${entry.data.changelog.date}T00:00:00Z`).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'UTC' })}</time></p>}
1616
<style>

‎src/content.config.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineCollection, z } from 'astro:content';
2-
import { docsLoader } from '@astrojs/starlight/loaders';
3-
import { docsSchema } from '@astrojs/starlight/schema';
2+
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
3+
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
44

55
// Native Markdown/MDX only. Jekyll sources are never loaded by Astro.
66
export const collections = { docs: defineCollection({
@@ -16,4 +16,4 @@ export const collections = { docs: defineCollection({
1616
anchor: z.string().regex(/^[a-z0-9_-]+$/),
1717
}).optional(),
1818
}) }),
19-
}) };
19+
}), i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }) };

‎src/content/docs/404.mdx‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: Page not found
3+
description: Find your way back to the HTML/CSS to Image documentation.
4+
# Starlight reads this entry directly for /404.html; don't also publish /404/.
5+
draft: true
6+
editUrl: false
7+
tableOfContents: false
8+
sidebar:
9+
hidden: true
10+
head:
11+
- tag: meta
12+
attrs:
13+
name: robots
14+
content: noindex, nofollow
15+
---
16+
17+
We couldn't find that page. It may have moved, or the link may be incorrect.
18+
19+
Try searching the docs, or head back to the [documentation homepage](/).

‎src/content/i18n/en.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"search.label": "Search docs"
3+
}

‎src/lib/breadcrumbs.mjs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import navigation from '../navigation.mjs';
2+
3+
export function breadcrumbs(route, title, isChangelog = false) {
4+
if (route === '/') return [];
5+
const candidates = [];
6+
function findPaths(items, parents = []) {
7+
for (const item of items) {
8+
if (item.link === route) candidates.push(parents);
9+
if (item.items) {
10+
const overview = item.items.find(child => child.label === 'Overview' && child.link);
11+
findPaths(item.items, overview?.link ? [...parents, { label: item.label, link: overview.link }] : parents);
12+
}
13+
}
14+
}
15+
findPaths(navigation);
16+
const score = path => path.filter(crumb => route.startsWith(crumb.link)).length;
17+
const parents = isChangelog ? [{ label: 'Changelog', link: '/changelog/' }]
18+
: (candidates.sort((a, b) => score(b) - score(a))[0] || []);
19+
return [{ label: 'Docs', link: '/' }, ...parents.filter(crumb => crumb.link !== route && crumb.link !== '/'), { label: title, link: route }];
20+
}

‎src/lib/structured-data.mjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { breadcrumbs } from './breadcrumbs.mjs';
2+
3+
export function structuredData({ origin, route, title, description, image, datePublished, overview = false }) {
4+
const url = new URL(route, origin).href;
5+
const home = new URL('/', origin).href;
6+
const publisher = {
7+
'@type': 'Organization', '@id': 'https://htmlcsstoimage.com/#organization',
8+
name: 'HTML/CSS to Image API', alternateName: 'HCTI', url: 'https://htmlcsstoimage.com/',
9+
description: 'Capture website screenshots, render HTML/CSS, or create templated graphics. Use MCP with AI, no-code automation, or the REST API. No browsers needed.',
10+
logo: { '@type': 'ImageObject', url: 'https://htmlcsstoimage.com/images/logo_with_black_bg.png', width: 1024, height: 1024 },
11+
sameAs: ['https://twitter.com/htmlcsstoimage', 'https://github.com/htmlcsstoimage', 'https://www.linkedin.com/company/html-css-to-image/'],
12+
contactPoint: { '@type': 'ContactPoint', contactType: 'customer support', email: 'support@htmlcsstoimage.com' },
13+
};
14+
const website = {
15+
'@type': 'WebSite', '@id': `${home}#website`, url: home,
16+
name: 'HTML/CSS to Image API Docs', inLanguage: 'en', publisher: { '@id': publisher['@id'] },
17+
};
18+
const crumbs = breadcrumbs(route, title, Boolean(datePublished));
19+
const breadcrumb = crumbs.length ? {
20+
'@type': 'BreadcrumbList', '@id': `${url}#breadcrumbs`,
21+
itemListElement: crumbs.map((crumb, index) => ({
22+
'@type': 'ListItem', position: index + 1, name: crumb.label, item: new URL(crumb.link, origin).href,
23+
})),
24+
} : undefined;
25+
const page = {
26+
'@type': overview ? 'CollectionPage' : 'WebPage', '@id': `${url}#webpage`, url,
27+
name: title, description, inLanguage: 'en', isPartOf: { '@id': website['@id'] },
28+
...(breadcrumb ? { breadcrumb: { '@id': breadcrumb['@id'] } } : {}),
29+
...(image ? { primaryImageOfPage: { '@type': 'ImageObject', url: image, width: 1200, height: 630 } } : {}),
30+
};
31+
const article = overview ? undefined : {
32+
'@type': datePublished ? 'BlogPosting' : 'TechArticle', '@id': `${url}#article`, url,
33+
headline: title, description, inLanguage: 'en', mainEntityOfPage: { '@id': page['@id'] },
34+
publisher: { '@id': publisher['@id'] },
35+
...(image ? { image } : {}), ...(datePublished ? { datePublished } : {}),
36+
};
37+
return { '@context': 'https://schema.org', '@graph': [publisher, website, page, breadcrumb, article].filter(Boolean) };
38+
}
39+
40+
// JSON-LD is embedded in a script element, so content must not close that element.
41+
export const serializeStructuredData = data => JSON.stringify(data).replace(/</g, '\\u003c');

‎src/styles/global.css‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
:root[data-theme='light'] { --sl-color-accent-low:#e4f6eb; --sl-color-accent:#007a4e; --sl-color-accent-high:#075836; --sl-color-white:#171b22; --sl-color-gray-1:#28332d; --sl-color-gray-2:#435148; --sl-color-gray-3:#617067; --sl-color-gray-4:#87988d; --sl-color-gray-5:#dce5dd; --sl-color-gray-6:#f7f9f6; --sl-color-bg:#ffffff; --sl-color-bg-nav:#ffffff; --sl-color-bg-sidebar:#f7f9f6; --sl-color-hairline:#e3e9e3; }
1212
html { scroll-padding-top:6rem; }
1313
h1,h2,h3 { letter-spacing:-.025em; }
14+
strong, b { font-weight:600; }
1415
main > .content-panel { padding-block:1rem; }
1516
/* Image zoom inserts a hidden element before the article. */
1617
.sl-container > .sl-markdown-content { margin-top:0; }

0 commit comments

Comments
 (0)