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
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,27 @@ npm test # browser tests, then Lighthouse budgets, on the last buil
| `src/layouts/` | The HTML shell: metadata, fonts, theme selection |
| `src/styles/` | Design tokens for the four themes, and base styles |
| `src/assets/` | Portrait, badges and logos; optimised at build time (AVIF, WebP) |
| `public/` | Files served as they are: favicons |
| `public/` | Files served as they are: favicons, PDFs, robots.txt, sitemap |
| `scripts/` | `og-image.mjs` draws the card shown where the site is shared |
| `.github/workflows/` | `ci.yml` checks pull requests; `deploy.yml` publishes `main` to Pages |

To update the site, edit `src/data/profile.ts`. A new badge or logo goes in
`src/assets/` and is imported there.
`src/assets/` and is imported there. After a change to the name, the role or
the portrait, redraw the sharing card with `npm run og-image`.

## Design

The palette is an autumn / winter colour analysis. The page picks one of four
themes from the visitor's clock, before the first paint:

| Theme | When | Background | Accent |
| --------------- | ----------------------------- | ---------- | ---------- |
| Autumn, day | April to November, 7:00–19:00 | cream | terracotta |
| Autumn, evening | April to November, otherwise | espresso | terracotta |
| Winter, day | December to March, 7:00–19:00 | ivory | blackberry |
| Winter, evening | December to March, otherwise | navy | blackberry |
themes before the first paint: the season from the month, day or evening from
whether the sun is up where the visitor is (Brussels for Belgium and its
neighbours).

| Theme | When | Background | Accent |
| --------------- | --------------------------- | ---------- | ---------- |
| Autumn, day | April to November, sun up | cream | terracotta |
| Autumn, evening | April to November, sun down | espresso | terracotta |
| Winter, day | December to March, sun up | ivory | blackberry |
| Winter, evening | December to March, sun down | navy | blackberry |

Every text colour meets WCAG AA (4.5:1) in every theme. Fonts: Cinzel Decorative
for the name only, Plus Jakarta Sans for headings, Inter for text; all three are
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"check": "astro check",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "playwright test"
"test": "playwright test",
"og-image": "node scripts/og-image.mjs"
},
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
Expand Down
1 change: 1 addition & 0 deletions public/google7caeff32a4565021.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
google-site-verification: google7caeff32a4565021.html
2 changes: 2 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://rememberyou.github.io/sitemap.xml
6 changes: 6 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://rememberyou.github.io/</loc>
</url>
</urlset>
46 changes: 46 additions & 0 deletions scripts/og-image.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Draws the image shown when the site is shared (LinkedIn, Slack, WhatsApp…):
// 1200×630, as the hero looks by day, with the site's own fonts. Run it again
// after a change to the name, the role or the photo:
// npm run og-image
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

import { chromium } from '@playwright/test';

const require = createRequire(import.meta.url);
const asDataUrl = (path, type) => `data:${type};base64,${readFileSync(path).toString('base64')}`;
const font = (pkg) => asDataUrl(require.resolve(pkg), 'font/woff2');

const html = `<!doctype html>
<style>
@font-face { font-family: Name; src: url(${font('@fontsource/cinzel-decorative/files/cinzel-decorative-latin-400-normal.woff2')}); }
@font-face { font-family: Title; font-weight: 200 800; src: url(${font('@fontsource-variable/plus-jakarta-sans/files/plus-jakarta-sans-latin-wght-normal.woff2')}); }
@font-face { font-family: Body; font-weight: 100 900; src: url(${font('@fontsource-variable/inter/files/inter-latin-wght-normal.woff2')}); }
* { margin: 0; box-sizing: border-box; }
body {
width: 1200px; height: 630px; padding: 0 80px;
display: flex; align-items: center; justify-content: space-between; gap: 56px;
background: #f4f0e2; color: #282019; font-family: Body;
border-bottom: 12px solid #a3583e;
}
h1 { font: 400 76px/1.04 Name; letter-spacing: 1px; padding-left: 0.09em; }
.role { margin-top: 26px; font: 600 28px Title; letter-spacing: -0.3px; white-space: nowrap; }
.role span { color: #a3583e; }
.site { margin-top: 30px; font: 500 22px Body; color: #585042; }
img { width: 380px; height: 380px; border-radius: 36px; object-fit: cover; flex: none;
box-shadow: 0 24px 48px -16px rgb(40 32 25 / 0.35); }
</style>
<div>
<h1>Terencio<br>Agozzino</h1>
<p class="role">Solutions Architect <span>|</span> AWS &amp; Azure Certified</p>
<p class="site">rememberyou.github.io</p>
</div>
<img src="${asDataUrl('src/assets/portrait.png', 'image/png')}" alt="">`;

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });
await page.setContent(html);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'src/assets/og.png' });
await browser.close();
console.log('wrote src/assets/og.png');
Binary file added src/assets/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/data/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,26 @@ export const person = {
credentials: 'AWS & Azure Certified',
country: 'Belgium',
region: 'Wallonia',
/** What search engines show under the title: at most about 155 characters. */
description:
'Solutions Architect, AWS & Azure certified: secure cloud foundations, data platforms and the automation that runs them, with a research background.',
pitch:
'5+ years across cloud, data and software: secure AWS & Azure foundations, data platforms, and the automation that runs them — with a research background in knowledge graphs.',
email: 'terencio.agozzino@gmail.com',
linkedin: 'https://www.linkedin.com/in/terencioagozzino/',
github: 'https://github.com/rememberYou',
credly: 'https://www.credly.com/users/terencio-agozzino/badges',
employer: { name: 'CBTW', url: 'https://cbtw.tech' },
school: 'Haute École en Hainaut',
/** What search engines associate with the name. */
topics: [
'Cloud architecture',
'Amazon Web Services',
'Microsoft Azure',
'Infrastructure as code',
'Data engineering',
'Knowledge graphs',
],
/** Served from public/; replace the file there to update it. */
cv: '/terencio-agozzino-cv.pdf',
thesis: '/terencio-agozzino-master-thesis.pdf',
Expand Down
42 changes: 37 additions & 5 deletions src/layouts/Base.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
import { Font, getImage } from 'astro:assets';

import og from '../assets/og.png';
import portrait from '../assets/portrait.png';
import { person } from '../data/profile';
import { certifications, person } from '../data/profile';
import '../styles/global.css';

interface Props {
Expand All @@ -12,19 +13,43 @@ interface Props {

const { title, description } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
const ogImage = await getImage({ src: portrait, width: 800, format: 'jpg' });
// The card shown where the site is shared; the portrait, where search engines
// show the person.
const ogImage = await getImage({ src: og, format: 'jpg' });
const ogImageUrl = new URL(ogImage.src, Astro.site);
const photo = await getImage({ src: portrait, width: 800, format: 'jpg' });

// Who the page is about, for search engines: a Person with their work,
// school, topics, credentials and the profiles that are also them.
const issuers: Record<string, string> = {
AWS: 'Amazon Web Services',
Microsoft: 'Microsoft',
'Scrum.org': 'Scrum.org',
};
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Person',
name: `${person.firstName} ${person.lastName}`,
givenName: person.firstName,
familyName: person.lastName,
jobTitle: person.role,
description,
url: canonical.href,
image: ogImageUrl.href,
image: new URL(photo.src, Astro.site).href,
email: `mailto:${person.email}`,
address: { '@type': 'PostalAddress', addressCountry: 'BE', addressRegion: person.region },
sameAs: [person.linkedin, person.github],
worksFor: { '@type': 'Organization', ...person.employer },
alumniOf: { '@type': 'CollegeOrUniversity', name: person.school },
knowsAbout: person.topics,
hasCredential: certifications
.flatMap(({ items }) => items)
.map(({ name, issuer }) => ({
'@type': 'EducationalOccupationalCredential',
name,
credentialCategory: 'certification',
recognizedBy: { '@type': 'Organization', name: issuers[issuer] ?? issuer },
})),
sameAs: [person.linkedin, person.github, person.credly],
};
---

Expand Down Expand Up @@ -94,6 +119,7 @@ const jsonLd = {
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<link rel="sitemap" href="/sitemap.xml" />

<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
Expand All @@ -106,9 +132,15 @@ const jsonLd = {
<meta property="og:image" content={ogImageUrl} />
<meta property="og:image:width" content={String(ogImage.attributes.width)} />
<meta property="og:image:height" content={String(ogImage.attributes.height)} />
<meta
property="og:image:alt"
content={`${person.firstName} ${person.lastName}, ${person.role}`}
/>
<meta property="og:site_name" content={`${person.firstName} ${person.lastName}`} />
<meta property="og:locale" content="en_GB" />
<meta property="profile:first_name" content={person.firstName} />
<meta property="profile:last_name" content={person.lastName} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content="summary_large_image" />

<Font cssVariable="--font-name" preload />
<Font cssVariable="--font-title" preload />
Expand Down
3 changes: 1 addition & 2 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import { person } from '../data/profile';
import Base from '../layouts/Base.astro';

const title = `${person.firstName} ${person.lastName} — ${person.role}`;
const description = `${person.role}, ${person.credentials}. ${person.pitch}`;
---

<Base title={title} description={description}>
<Base title={title} description={person.description}>
<Nav />
<main>
<Hero />
Expand Down
35 changes: 35 additions & 0 deletions tests/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
const broken = await page.evaluate(() =>
[...document.images].filter((img) => !img.complete || !img.naturalWidth).map((img) => img.src),
);
expect(broken).toEqual([]);

Check failure on line 57 in tests/site.spec.ts

View workflow job for this annotation

GitHub Actions / Format, types, build, tests, Lighthouse

[safari] › tests/site.spec.ts:49:1 › loads every image

1) [safari] › tests/site.spec.ts:49:1 › loads every image ──────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 6 - Array [] + Array [ + "http://localhost:4329/_astro/aws-cloud-practitioner.D9svnEg8_Zh46xb.webp", + "http://localhost:4329/_astro/azure-fundamentals.DxqeRKeW_1E4hzt.webp", + "http://localhost:4329/_astro/azure-ai.B7hIyBB9_jIi4G.webp", + "http://localhost:4329/_astro/psm1.BXuuTSv__Z2hsrOS.webp", + ] 55 | [...document.images].filter((img) => !img.complete || !img.naturalWidth).map((img) => img.src), 56 | ); > 57 | expect(broken).toEqual([]); | ^ 58 | }); 59 | 60 | test('serves the CV, the thesis and the icons', async ({ request }) => { at /home/runner/work/rememberYou.github.io/rememberYou.github.io/tests/site.spec.ts:57:18
});

test('serves the CV, the thesis and the icons', async ({ request }) => {
Expand All @@ -72,6 +72,41 @@
}
});

test('tells search engines and link previews who the page is about', async ({ page, request }) => {
await page.goto('/');
const meta = (selector: string) => page.locator(selector).getAttribute('content');

// Google shows about 155 characters of the description.
expect((await meta('meta[name="description"]'))!.length).toBeLessThanOrEqual(155);

// A wide card, so LinkedIn shows it large rather than as a thumbnail.
expect(await meta('meta[property="og:image:width"]')).toBe('1200');
expect(await meta('meta[property="og:image:height"]')).toBe('630');
const card = await request.get(new URL((await meta('meta[property="og:image"]'))!).pathname);
expect(card.status()).toBe(200);

const person = JSON.parse(
(await page.locator('script[type="application/ld+json"]').textContent())!,
);
expect(person).toMatchObject({
'@type': 'Person',
worksFor: { name: 'CBTW' },
alumniOf: { name: 'Haute École en Hainaut' },
});
expect(person.hasCredential.length).toBeGreaterThan(5);

// The sitemap, announced in robots.txt, and Search Console's proof of ownership.
expect(await (await request.get('/robots.txt')).text()).toContain(
'Sitemap: https://rememberyou.github.io/sitemap.xml',
);
expect(await (await request.get('/sitemap.xml')).text()).toContain(
'<loc>https://rememberyou.github.io/</loc>',
);
expect(await (await request.get('/google7caeff32a4565021.html')).text()).toContain(
'google-site-verification: google7caeff32a4565021.html',
);
});

test('links only to anchors that exist, and to secure sites', async ({ page }) => {
await page.goto('/');
const hrefs = await page
Expand Down
Loading