Skip to content

Commit f719b9e

Browse files
committed
better font handling
1 parent 3d868bd commit f719b9e

11 files changed

Lines changed: 151 additions & 22 deletions

File tree

‎README.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ requires all 148 original routes to exist or redirect to a native page. It check
4242
old anchors, internal links, assets, external-link attributes, and Markdown
4343
exports, and writes `migration-report.json`. CI runs this complete-site gate.
4444

45+
## Fonts
46+
47+
IBM Plex Sans and Mono are configured through Astro's built-in Fontsource provider
48+
in `astro.config.mjs`. Astro downloads and caches fonts during the build, then
49+
serves fingerprinted WOFF2 files from `/_astro/fonts/` with immutable caching.
50+
Visitors make no Google Fonts requests. The font license is in
51+
`public/assets/fonts/OFL.txt`.
52+
53+
`<Font />` supplies font-face declarations and adjusted fallbacks. Only the normal
54+
Latin Sans face is preloaded; other subsets, italics, and Mono load as needed.
55+
Docs and OG cards share the same self-hosted Sans font. New/uncached font builds
56+
need network access to Fontsource; rendering the deployed site does not.
57+
4558
## Page prefetching
4659

4760
`src/scripts/prefetch.ts` warms internal HTML pages on hover, focus, or pointer down
@@ -66,7 +79,7 @@ and inspect imports to find large dependencies. Compression sizes are estimates,
6679
not measured Cloudflare transfers.
6780

6881
Reports are ignored by Git and stay outside `dist`, so they are never deployed.
69-
Normal builds do not run the analyzer. Google Fonts, runtime-loaded Pagefind
82+
Normal builds do not run the analyzer. Font files, runtime-loaded Pagefind
7083
assets, and copied public assets need separate browser network measurements.
7184

7285
## Page titles

‎astro.config.mjs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig } from 'astro/config';
1+
import { defineConfig, fontProviders } from 'astro/config';
22
import starlight from '@astrojs/starlight';
33
import tailwindcss from '@tailwindcss/vite';
44
import navigation from './src/navigation.mjs';
@@ -11,6 +11,22 @@ import bundleAnalysis from './scripts/bundle-analysis.mjs';
1111
export default defineConfig({
1212
site: process.env.DOCS_ORIGIN || 'http://localhost:4321',
1313
output: 'static',
14+
fonts: [
15+
{
16+
provider: fontProviders.fontsource(), name: 'IBM Plex Sans',
17+
cssVariable: '--font-ibm-plex-sans', weights: ['100 700'],
18+
styles: ['normal', 'italic'],
19+
subsets: ['latin', 'latin-ext', 'cyrillic', 'cyrillic-ext', 'greek', 'vietnamese'],
20+
formats: ['woff2'], display: 'swap',
21+
},
22+
{
23+
provider: fontProviders.fontsource(), name: 'IBM Plex Mono',
24+
cssVariable: '--font-ibm-plex-mono', weights: [400, 500, 600, 700],
25+
styles: ['normal', 'italic'],
26+
subsets: ['latin', 'latin-ext', 'cyrillic', 'cyrillic-ext', 'vietnamese'],
27+
formats: ['woff2'], display: 'swap', fallbacks: ['monospace'],
28+
},
29+
],
1430
// Our fetch-based prefetch matches ClientRouter's request and Vary: Accept.
1531
// Native link-prefetch requests use a different browser cache variant.
1632
prefetch: false,

‎public/_headers‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ https://:version.:worker.:account.workers.dev/*
1313
# Generated OG rendering pages are never indexed, including in production.
1414
/_og/*
1515
X-Robots-Tag: noindex, nofollow
16+
17+
# Astro fingerprints these font files; changed fonts receive new URLs.
18+
/_astro/fonts/*
19+
Cache-Control: public, max-age=31536000, immutable

‎public/assets/fonts/OFL.txt‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
2+
3+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4+
5+
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6+
7+
8+
-----------------------------------------------------------
9+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+
-----------------------------------------------------------
11+
12+
PREAMBLE
13+
The goals of the Open Font License (OFL) are to stimulate worldwide
14+
development of collaborative font projects, to support the font creation
15+
efforts of academic and linguistic communities, and to provide a free and
16+
open framework in which fonts may be shared and improved in partnership
17+
with others.
18+
19+
The OFL allows the licensed fonts to be used, studied, modified and
20+
redistributed freely as long as they are not sold by themselves. The
21+
fonts, including any derivative works, can be bundled, embedded,
22+
redistributed and/or sold with any software provided that any reserved
23+
names are not used by derivative works. The fonts and derivatives,
24+
however, cannot be released under any other type of license. The
25+
requirement for fonts to remain under this license does not apply
26+
to any document created using the fonts or their derivatives.
27+
28+
DEFINITIONS
29+
"Font Software" refers to the set of files released by the Copyright
30+
Holder(s) under this license and clearly marked as such. This may
31+
include source files, build scripts and documentation.
32+
33+
"Reserved Font Name" refers to any names specified as such after the
34+
copyright statement(s).
35+
36+
"Original Version" refers to the collection of Font Software components as
37+
distributed by the Copyright Holder(s).
38+
39+
"Modified Version" refers to any derivative made by adding to, deleting,
40+
or substituting -- in part or in whole -- any of the components of the
41+
Original Version, by changing formats or by porting the Font Software to a
42+
new environment.
43+
44+
"Author" refers to any designer, engineer, programmer, technical
45+
writer or other person who contributed to the Font Software.
46+
47+
PERMISSION & CONDITIONS
48+
Permission is hereby granted, free of charge, to any person obtaining
49+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+
redistribute, and sell modified and unmodified copies of the Font
51+
Software, subject to the following conditions:
52+
53+
1) Neither the Font Software nor any of its individual components,
54+
in Original or Modified Versions, may be sold by itself.
55+
56+
2) Original or Modified Versions of the Font Software may be bundled,
57+
redistributed and/or sold with any software, provided that each copy
58+
contains the above copyright notice and this license. These can be
59+
included either as stand-alone text files, human-readable headers or
60+
in the appropriate machine-readable metadata fields within text or
61+
binary files as long as those fields can be easily viewed by the user.
62+
63+
3) No Modified Version of the Font Software may use the Reserved Font
64+
Name(s) unless explicit written permission is granted by the corresponding
65+
Copyright Holder. This restriction only applies to the primary font name as
66+
presented to the users.
67+
68+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+
Software shall not be used to promote, endorse or advertise any
70+
Modified Version, except to acknowledge the contribution(s) of the
71+
Copyright Holder(s) and the Author(s) or with their explicit written
72+
permission.
73+
74+
5) The Font Software, modified or unmodified, in part or in whole,
75+
must be distributed entirely under this license, and must not be
76+
distributed under any other license. The requirement for fonts to
77+
remain under this license does not apply to any document created
78+
using the Font Software.
79+
80+
TERMINATION
81+
This license becomes null and void if any of the above conditions are
82+
not met.
83+
84+
DISCLAIMER
85+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+
OTHER DEALINGS IN THE FONT SOFTWARE.

‎src/components/Breadcrumbs.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const crumbs = breadcrumbs(route, entry.data.page_title ?? entry.data.title, Boo
1111
</ol>
1212
</nav>}
1313
<style>
14-
.breadcrumbs { margin-bottom:.5rem; font-size:.8rem; color:var(--sl-color-gray-3); }
14+
.breadcrumbs { margin-bottom:0; font-size:.8rem; color:var(--sl-color-gray-3); }
1515
ol { display:flex; flex-wrap:wrap; align-items:baseline; gap:.35rem .5rem; list-style:none; padding:0; margin:0; }
1616
li { display:flex; align-items:baseline; gap:.5rem; min-width:0; max-width:100%; }
1717
li + li::before { content:'›'; color:var(--sl-color-gray-3); }

‎src/components/Head.astro‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import pages from '../generated/pages.json';
3+
import { Font } from 'astro:assets';
34
import { ClientRouter } from 'astro:transitions';
45
import NavigationLifecycle from './NavigationLifecycle.astro';
56
import { structuredData, serializeStructuredData } from '../lib/structured-data.mjs';
@@ -18,21 +19,22 @@ const siteTitle = 'HTML/CSS to Image API Docs';
1819
const brandedTitle = (title: string) => title === siteTitle || title.endsWith(` | ${siteTitle}`)
1920
? title : `${title} | ${siteTitle}`;
2021
const tags = head.filter(tag => tag.tag !== 'title' &&
22+
!(tag.tag === 'meta' && tag.attrs?.charset) &&
2123
!(tag.tag === 'meta' && tag.attrs?.name === 'generator'))
2224
.map(tag => tag.tag === 'meta' &&
2325
(tag.attrs?.property === 'og:title' || tag.attrs?.name === 'twitter:title')
2426
? { ...tag, attrs: { ...tag.attrs, content: brandedTitle(entry.data.og_title ?? entry.data.title) } }
2527
: tag);
2628
---
29+
<meta charset="utf-8" />
2730
<title>{brandedTitle(entry.data.title)}</title>
2831
<ClientRouter fallback="swap" />
2932
<NavigationLifecycle />
3033
{jsonLd && <script type="application/ld+json" set:html={serializeStructuredData(jsonLd)} />}
3134
{entry.data.changelog && <meta property="article:published_time" content={entry.data.changelog.date} />}
3235
<meta data-pagefind-filter="kind[content]" content={entry.data.changelog ? 'changelog' : 'docs'} />
33-
<link rel="preconnect" href="https://fonts.googleapis.com" />
34-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
35-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap" />
36+
<Font cssVariable="--font-ibm-plex-sans" preload={[{ subset: 'latin', style: 'normal' }]} />
37+
<Font cssVariable="--font-ibm-plex-mono" />
3638
{tags.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
3739
{!tags.some(tag => tag.tag === 'meta' && tag.attrs?.name === 'twitter:title') &&
3840
<meta name="twitter:title" content={brandedTitle(entry.data.og_title ?? entry.data.title)} />}

‎src/components/PageActions.astro‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site || Astro.url.origin).href
44
const prompt = `Read from ${pageUrl} so I can ask questions about it.`;
55
---
66
<details class="page-actions" data-pagefind-ignore>
7-
<summary aria-label="Page actions"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg> Copy page <span aria-hidden="true">⌄</span></summary>
7+
<summary aria-label="Copy page and more options"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/></svg> Copy page <svg class="block shrink-0" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 9.5 5 5 5-5" /></svg></summary>
88
<div class="action-list">
99
<button type="button" data-copy-page={markdownPath}><strong>Copy page</strong><small>Copy Markdown for LLMs</small></button>
1010
<a href={markdownPath} data-astro-reload><strong>View as Markdown ↗</strong><small>Read this page as plain text</small></a>
@@ -42,7 +42,7 @@ const prompt = `Read from ${pageUrl} so I can ask questions about it.`;
4242
document.addEventListener('astro:page-load', initializePageActions);
4343
</script>
4444
<style>
45-
.page-actions { position:relative; flex-shrink:0; margin-top:.45rem; font-size:.8rem; z-index:10; }
45+
.page-actions { position:relative; flex-shrink:0; margin-top:0; font-size:.8rem; z-index:10; }
4646
summary { display:flex; align-items:center; gap:.5rem; list-style:none; border:1px solid var(--sl-color-gray-5); padding:.5rem .7rem; border-radius:.35rem; cursor:pointer; color:var(--sl-color-gray-2); white-space:nowrap; }
4747
summary::-webkit-details-marker { display:none; }
4848
summary:hover { background:var(--sl-color-bg-sidebar); }

‎src/components/PageTitle.astro‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ const pageTitle = entry.data.page_title ?? entry.data.title;
77
const route = Astro.url.pathname;
88
const markdownPath = route === '/' ? '/index.md' : `${route.replace(/\/$/, '')}.md`;
99
---
10-
<Breadcrumbs />
11-
<div class="page-heading">
12-
{entry.data.page_title ? <h1 id="_top">{pageTitle}</h1> : <DefaultPageTitle />}
13-
{entry.id !== '404' && <PageActions markdownPath={markdownPath} title={pageTitle} />}
10+
<div class="page-heading grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-2">
11+
{route !== '/' && <div class="col-start-1 row-start-1 min-w-0 min-[40rem]:col-span-2"><Breadcrumbs /></div>}
12+
<div class="col-span-2 col-start-1 row-start-2 min-w-0 min-[40rem]:col-span-1">
13+
{entry.data.page_title ? <h1 id="_top">{pageTitle}</h1> : <DefaultPageTitle />}
14+
</div>
15+
{entry.id !== '404' && <div class="col-start-2 row-start-1 self-start justify-self-end min-[40rem]:row-start-2 min-[40rem]:self-center"><PageActions markdownPath={markdownPath} title={pageTitle} /></div>}
1416
</div>
1517
{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>}
1618
<style>
1719
h1 { margin-top:1rem; font-size:var(--sl-text-h1); line-height:var(--sl-line-height-headings); font-weight:600; color:var(--sl-color-white); }
1820
.published { margin:.8rem 0 0; font-size:.9rem; color:var(--sl-color-gray-3); }
19-
.page-heading { display:flex; align-items:center; gap:1rem; justify-content:space-between; margin-top:.5rem; }
21+
.page-heading { margin-top:.5rem; }
2022
.page-heading :global(h1) { min-width:0; margin-top:0; }
21-
@media(max-width:40rem) { .page-heading { flex-wrap:wrap; } }
23+
2224
</style>

‎src/components/SiteTitle.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<a href="/" class="brand" aria-label="HTML/CSS to Image documentation home">
1+
<a href="/" class="brand" aria-label="HTML/CSS to Image Docs home">
22
<img src="/assets/images/hcti-logo.svg" width="50" height="50" alt="HTML/CSS to Image" />
33
<span>Docs</span>
44
</a>

‎src/pages/[...og].astro‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import pages from '../generated/pages.json';
3+
import { Font } from 'astro:assets';
34
// Astro ignores underscore-prefixed page folders. Generate only these internal
45
// /_og/<hash>/ routes explicitly instead.
56
export function getStaticPaths() {
@@ -23,9 +24,7 @@ const gradient = `linear-gradient(${angle}deg,${colors[0]} 0%,${colors[1]} 48%,$
2324
---
2425
<!doctype html>
2526
<html lang="en"><head><meta charset="utf-8" /><meta name="robots" content="noindex" /><title>{card.title}</title>
26-
<link rel="preconnect" href="https://fonts.googleapis.com" />
27-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
28-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&display=block" />
27+
<Font cssVariable="--font-ibm-plex-sans" preload={[{ subset: 'latin', style: 'normal' }]} />
2928
</head>
3029
<body data-pagefind-ignore>
3130
<main id="og-card" style={{ background: gradient }}>
@@ -41,7 +40,7 @@ const gradient = `linear-gradient(${angle}deg,${colors[0]} 0%,${colors[1]} 48%,$
4140
</body></html>
4241
<style>
4342
* { box-sizing:border-box; }
44-
body { margin:0; font-family:'IBM Plex Sans',sans-serif; }
43+
body { margin:0; font-family:var(--font-ibm-plex-sans); }
4544
#og-card { width:1200px; height:630px; padding:32px; color:#192521; }
4645
.panel { height:100%; padding:38px 48px 32px; display:flex; flex-direction:column; border:1px solid #dfe8e2; border-radius:8px; background-color:#fcfdfc; background-image:linear-gradient(#254d3a0a 1px,transparent 1px),linear-gradient(90deg,#254d3a0a 1px,transparent 1px); background-size:64px 64px; background-position:26px 26px; box-shadow:0 12px 32px #18382d30; }
4746
.brand { display:flex; align-items:center; gap:14px; font-weight:600; font-size:24px; letter-spacing:-.5px; }

0 commit comments

Comments
 (0)