Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Josselin Perrus's personal site: an Astro 7 static site served by Cloudflare Wor

## Layout

- `src/`: pages, layouts and components.
- `src/`: pages, layouts and components. Pages are built in the `Frame` component, which holds the header and the footer; the portfolio view of `/documents/` has its own.
- `src/styles/`: `tokens.css` holds the colours and fonts as custom properties, `global.css` the base styles, and `legacy.css` the content styles of the pages not yet rebuilt in the redesign.
- `src/fonts/`: the self-hosted fonts, DejaVu Sans Mono and Newsreader, subset by `scripts/subset-fonts.py`, with their licences. The site loads no outside fonts.
- `src/content/writing/`: git submodule of [meaningfool-writing](https://github.com/meaningfool/meaningfool-writing), which holds every article, activity log and image. Read its `AGENTS.md` before editing content.
- `src/content.config.ts`: the content collection, built from `articles/*.md` and `daily-logs/*.md` in the submodule.
- `docs/document-sharing.md`: how the encrypted portfolio links on `/documents/` work and how to create them.
Expand Down
7 changes: 4 additions & 3 deletions public/_headers
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Cloudflare Workers reads this file; it isn't served there.

# Every response from the site. The Content-Security-Policy only reports for now:
# it lists what pages load (Google Fonts, YouTube embeds, outside images and
# Cloudflare's analytics beacon, which reports to /cdn-cgi/rum on this domain).
# it lists what pages load (YouTube embeds, outside images and Cloudflare's
# analytics beacon, which reports to /cdn-cgi/rum on this domain). Fonts are
# self-hosted.
# Inline styles stay allowed because code highlighting uses style attributes.
/*
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self'; frame-src https://www.youtube.com https://www.youtube-nocookie.com; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https:; media-src 'self'; frame-src https://www.youtube.com https://www.youtube-nocookie.com; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

# Fingerprinted build files: their names change whenever their content does.
/_astro/*
Expand Down
11 changes: 0 additions & 11 deletions public/fonts/space-mono-400.woff2

This file was deleted.

11 changes: 0 additions & 11 deletions public/fonts/space-mono-500.woff2

This file was deleted.

105 changes: 105 additions & 0 deletions scripts/subset-fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Make the site's self-hosted fonts in src/fonts/ from the original font files.

- DejaVu Sans Mono 2.37, Book: the text of everything but articles and About.
- Newsreader 1.003, regular and italic: the text of articles and About. Its weight is fixed at 400 and its optical size
axis (6 to 72) is kept, as Google Fonts serves it to the prototype.

Each font keeps only the characters in its RANGES, which the @font-face rules in src/styles/tokens.css repeat as their
unicode-range, so anything else falls back to the next font in the stack. DejaVu Sans Mono leaves out the symbols that
are also emoji, such as the star or the check mark, so they show as emoji. The fonts keep their whole name table, with
the copyright and licence. The licences are next to the fonts: LICENSE-DejaVu.txt and OFL-Newsreader.txt.

Run from the repository root, with fontTools and brotli installed (pip install fonttools brotli):

python3 scripts/subset-fonts.py

It downloads the originals, checks their SHA-256, and overwrites the .woff2 files in src/fonts/. The same originals
give the same files.
"""
import hashlib
import io
import os
import tarfile
import urllib.request

from fontTools import subset
from fontTools.ttLib import TTFont
from fontTools.varLib import instancer

OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src", "fonts")

# Latin, as Google Fonts subsets it: ASCII, Latin-1, the French and other Western European extras, punctuation, euro.
LATIN = "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
# The mono font also draws arrows (← → ↗) and box drawing (├── └──) for code and labels.
RANGES = {
"mono": LATIN + ", U+2190-21FF, U+2500-257F",
"serif": LATIN,
}

FONTS = [
{
"out": "dejavu-sans-mono.woff2",
"url": "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2",
"member": "dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono.ttf",
"sha256": "b4a6c3e4faab8773f4ff761d56451646409f29abedd68f05d38c2df667d3c582",
"ranges": RANGES["mono"],
},
{
"out": "newsreader.woff2",
"url": "https://raw.githubusercontent.com/google/fonts/main/ofl/newsreader/Newsreader%5Bopsz,wght%5D.ttf",
"sha256": "8a08d13f8a6c0d51be379a60af84f945f65369a67e509ee3c3bdcc421254d7c1",
"ranges": RANGES["serif"],
"pin": {"wght": 400},
},
{
"out": "newsreader-italic.woff2",
"url": "https://raw.githubusercontent.com/google/fonts/main/ofl/newsreader/Newsreader-Italic%5Bopsz,wght%5D.ttf",
"sha256": "796668611f80b64d5adf182fde3b6f29ed83b4e7cbec7b96937e84ac01364792",
"ranges": RANGES["serif"],
"pin": {"wght": 400},
},
]


def unicodes(ranges):
"""The code points of a CSS unicode-range list."""
points = []
for part in ranges.split(","):
lo, _, hi = part.strip()[2:].partition("-")
points.extend(range(int(lo, 16), int(hi or lo, 16) + 1))
return points


def original(spec):
data = urllib.request.urlopen(spec["url"]).read()
if "member" in spec:
data = tarfile.open(fileobj=io.BytesIO(data)).extractfile(spec["member"]).read()
digest = hashlib.sha256(data).hexdigest()
if digest != spec["sha256"]:
raise SystemExit(f"{spec['out']}: the original's SHA-256 is {digest}, expected {spec['sha256']}")
return TTFont(io.BytesIO(data))


def main():
for spec in FONTS:
font = original(spec)
if "pin" in spec:
font = instancer.instantiateVariableFont(font, spec["pin"])
options = subset.Options()
options.flavor = "woff2"
options.name_IDs = ["*"]
options.name_languages = ["*"]
options.layout_features = ["*"]
options.notdef_outline = True
subsetter = subset.Subsetter(options)
subsetter.populate(unicodes=unicodes(spec["ranges"]))
subsetter.subset(font)
path = os.path.join(OUT, spec["out"])
font.flavor = "woff2"
font.recalcTimestamp = False # keep the original's date, so the same originals give the same files
font.save(path)
print(spec["out"], os.path.getsize(path) // 1024, "KB")


if __name__ == "__main__":
main()
1 change: 0 additions & 1 deletion src/components/ArticlesList.astro
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const { articles } = Astro.props;
}

.articles-list .post-title:hover {
color: var(--accent-hover);
text-decoration: underline;
}

Expand Down
53 changes: 53 additions & 0 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
// The site footer: the wordmark, then About and the profiles elsewhere. On phones the links go under the wordmark.
import Wordmark from './Wordmark.astro';

const links = [
{ href: '/about/', label: 'About' },
{ href: 'https://www.linkedin.com/in/josselinperrus/', label: 'LinkedIn' },
{ href: 'https://github.com/meaningfool', label: 'GitHub' },
{ href: 'https://x.com/nonils', label: 'X' },
];
---

<footer class="foot">
<Wordmark small />
<nav class="links" aria-label="Footer">
{links.map(({ href, label }) => <a href={href}>{label}</a>)}
</nav>
</footer>

<style>
.foot {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-top: 96px;
padding: 28px 0 48px;
border-top: 1px solid var(--line);
}

.links {
display: flex;
flex-wrap: wrap;
gap: 6px 22px;
font-size: 12px;
}

.links a {
color: var(--muted);
text-decoration: none;
}

.links a:hover {
color: var(--ink);
}

@media (max-width: 900px) {
.foot {
flex-direction: column;
align-items: start;
}
}
</style>
22 changes: 22 additions & 0 deletions src/components/Frame.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
// The frame the site's pages are built in: up to 1240px wide with 24px side padding (more where a phone's notch needs
// it), the header at the top and the footer at the bottom. It sits above the page background.
import Header from './Header.astro';
import Footer from './Footer.astro';
---

<div class="frame">
<Header />
<slot />
<Footer />
</div>

<style>
.frame {
position: relative;
z-index: 1;
max-width: 1240px;
margin-inline: auto;
padding-inline: max(24px, env(safe-area-inset-left, 0px)) max(24px, env(safe-area-inset-right, 0px));
}
</style>
78 changes: 37 additions & 41 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
@@ -1,57 +1,53 @@
<header>
<div class="container">
<div class="site-title"><a href="/">meaningfool</a></div>
<nav>
<a href="/about/">About</a>
</nav>
</div>
---
// The site header: the wordmark on the left, the navigation on the right. The current page's link is in ink,
// underlined in the accent colour. The navigation stays visible on phones.
import Wordmark from './Wordmark.astro';

const links = [
{ href: '/', label: 'Home' },
{ href: '/about/', label: 'About' },
];
const here = Astro.url.pathname.replace(/\/?$/, '/');
---

<header class="head">
<Wordmark />
<nav class="nav" aria-label="Main">
{links.map(({ href, label }) => (
<a href={href} aria-current={href === here ? 'page' : undefined}>{label}</a>
))}
</nav>
</header>
<div class="header-separator container"></div>

<style>
header {
padding: 1.25rem 0;
}

.header-separator {
border-bottom: 1px solid var(--border);
margin-bottom: 2rem;
}

.container {
.head {
display: flex;
justify-content: space-between;
align-items: baseline;
align-items: center;
gap: 16px;
min-height: 84px;
border-bottom: 1px solid var(--line);
}

.site-title {
margin: 0;
font-family: var(--font-title);
font-size: 1.5rem;
font-weight: var(--font-weight-medium);
line-height: 1;
.nav {
display: flex;
gap: 22px;
font-size: 13px;
}

.site-title a {
color: var(--text);
.nav a {
color: var(--muted);
text-decoration: none;
}

nav a {
color: var(--accent);
text-decoration: none;
font-size: 1rem;
line-height: 1;
.nav a:hover {
color: var(--ink);
}

nav a:hover {
color: var(--accent-hover);
.nav a[aria-current='page'] {
color: var(--ink);
text-decoration: underline;
text-decoration-color: var(--accent);
text-underline-offset: 6px;
}

@media (max-width: 768px) {
.site-title {
font-size: 1rem;
}
}
</style>
</style>
Loading
Loading