My Gatsby Deployment pipeline / build-and-deploy (push) Successful in 1m4s
Replace the single Gatsby page with a dark, terminal-style agent that answers questions about Dennis's work from one curated knowledge base. There is no model and no network call, so answers cannot be invented. A plain "Classic view" and an optional light theme sit one click away. Site - Terminal UI (React island): typed answers, tool-call lines, slash commands, autocomplete, history, deep links, a short /oncall game - Content focused on what recruiters scan for; personal and employer-internal details are left out - No profile photo anywhere, including the new link-preview card and icons - Corrected facts from the old page (dates, titles, tenses, skill tiers) Tooling - Migrate Gatsby 5 to Astro 7 (Node 22.23+); builds in about 1 second - Self-host fonts, so visitors make no request to third parties - Link-preview card, favicon set, web manifest and a service-worker clean-up for returning visitors - Deploy workflow builds with npm ci on Node 22 and uploads dist/ - Track package-lock.json Quality - Tests for routing and for markup injection from visitor input - A content guard scans every reachable answer; the list of things that must never be published lives in a git-ignored local file Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
164 lines
4.5 KiB
React
164 lines
4.5 KiB
React
import React from 'react'
|
|
|
|
import {
|
|
CERTIFICATIONS,
|
|
EDUCATION,
|
|
IDENTITY,
|
|
LANGUAGES,
|
|
ROLES,
|
|
ROUTE,
|
|
SKILLS,
|
|
SUMMARY,
|
|
TIERS,
|
|
} from '../data/profile.mjs'
|
|
|
|
const Groups = ({ groups }) => (
|
|
<div className="stack">
|
|
{groups.map((group) => (
|
|
<div className="group" key={group.label}>
|
|
<p className="group__label">{group.label}</p>
|
|
<p className="group__tags">
|
|
{group.items.map((item, i) => (
|
|
<React.Fragment key={item}>
|
|
{i > 0 && ' · '}
|
|
<span className="t">{item}</span>
|
|
</React.Fragment>
|
|
))}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
|
|
const SKILL_GROUPS = TIERS.map((tier) => ({
|
|
label: tier.label,
|
|
items: SKILLS.filter((s) => s.tier === tier.id && s.listed !== false).map(
|
|
(s) => s.name
|
|
),
|
|
})).filter((g) => g.items.length)
|
|
|
|
const CREDENTIAL_GROUPS = [
|
|
{ label: 'Education', items: EDUCATION.map((e) => e.title) },
|
|
{
|
|
label: 'Certifications',
|
|
items: CERTIFICATIONS.map((c) => `${c.name} (${c.year})`),
|
|
},
|
|
{
|
|
label: 'Languages',
|
|
items: LANGUAGES.map(([name, level]) => `${name}, ${level}`),
|
|
},
|
|
]
|
|
|
|
// The same facts as the terminal, laid out as a page. It is also what search engines and
|
|
// visitors without JavaScript get.
|
|
const Classic = () => (
|
|
<div className="wrap">
|
|
<section className="hero">
|
|
<div className="hero__text">
|
|
<h1 className="hero__name">{IDENTITY.name}</h1>
|
|
<p className="hero__lead">
|
|
Data pipelines, data products and the automation that keeps them
|
|
running.
|
|
</p>
|
|
<p className="hero__meta">
|
|
<strong>
|
|
{IDENTITY.title} at {IDENTITY.employer}
|
|
</strong>
|
|
, based in {IDENTITY.location}, with {IDENTITY.experience} across data
|
|
engineering, ML infrastructure and DevOps.
|
|
</p>
|
|
<nav className="actions">
|
|
<a href={`mailto:${IDENTITY.email}`}>{IDENTITY.email}</a>
|
|
<a
|
|
href={IDENTITY.linkedinUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
LinkedIn
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="section">
|
|
<h2 className="section__label">Profile</h2>
|
|
<div className="prose">
|
|
{SUMMARY.map((text) => (
|
|
<p key={text}>{text}</p>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="section">
|
|
<h2 className="section__label">Experience</h2>
|
|
<div className="experience">
|
|
<p className="experience__route">
|
|
{ROUTE.map((stop) => stop.place).join(' → ')}
|
|
</p>
|
|
<ol className="roles">
|
|
{ROLES.map((role) => (
|
|
<li
|
|
className={`role${role.live ? ' role--live' : ''}`}
|
|
key={role.id}
|
|
>
|
|
<div className="role__rail" aria-hidden="true">
|
|
<span className="role__node" />
|
|
</div>
|
|
<div className="role__body">
|
|
<p className="role__year">{role.period}</p>
|
|
<div className="role__head">
|
|
<h3 className="role__co">{role.company}</h3>
|
|
<span className="role__loc">{role.place}</span>
|
|
</div>
|
|
<p className="role__title">{role.title}</p>
|
|
<p className="role__desc">{role.summary}</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="section">
|
|
<h2 className="section__label">Stack</h2>
|
|
<Groups groups={SKILL_GROUPS} />
|
|
</section>
|
|
|
|
<section className="section">
|
|
<h2 className="section__label">Credentials</h2>
|
|
<Groups groups={CREDENTIAL_GROUPS} />
|
|
</section>
|
|
|
|
<section className="section">
|
|
<h2 className="section__label">Contact</h2>
|
|
<div>
|
|
<p className="contact__intro">
|
|
Always happy to talk about data, ML and platform engineering. Email is
|
|
the fastest way to reach me.
|
|
</p>
|
|
<a className="contact__email" href={`mailto:${IDENTITY.email}`}>
|
|
{IDENTITY.email}
|
|
</a>
|
|
<div className="contact__links">
|
|
<a
|
|
href={IDENTITY.linkedinUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
LinkedIn
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer className="colophon">
|
|
<span>
|
|
© {new Date().getFullYear()} {IDENTITY.name}
|
|
</span>
|
|
<span>{IDENTITY.location}</span>
|
|
</footer>
|
|
</div>
|
|
)
|
|
|
|
export default Classic
|