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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file.

## 2026-07-12: Sticky nav, profile/social sync

- Made the portfolio nav sticky and horizontally resilient on small screens so the site sections stay available while scrolling.
- Added LinkedIn, X, and Substack as explicit social/content sources in homepage chips, footer links, and Person JSON-LD `sameAs`.
- Added a local profile image slot with dimensions and alt text so Ryan's LinkedIn headshot can be dropped in without layout shift; the current checked-in asset is an honest placeholder because LinkedIn image fetches are blocked in this environment.

## 2026-07-12: Copy, trust, and answer-engine pass

- Tightened homepage, about, writing, and demos copy so claims point back to visible work, live demos, decks, or durable career facts.
- Added homepage proof points for Google Maps Platform reach, Mapbox growth-stage work, and Caterpillar patents.
- Improved answer-engine structured data with a ProfilePage summary and Person knowsAbout topics for maps, developer experience, MCP, agent skills, evals, and AI-native tools.
- Kept the portfolio performance posture intact: no new client-side JavaScript, no new dependencies, and no fabricated visuals.

## 2026-07-12: UI/UX + copy refresh, CI hang fix, agentic loop

- Fixed Cloud Run auto-deployment workflow (skipped status) by correcting the repository filter in `.github/workflows/deploy.yml` to check for `ryanbaumann/Portfolio` instead of the legacy `ryanbaumann/trails.ninja` repository.
Expand Down
7 changes: 7 additions & 0 deletions LEARNINGS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Learnings

## 2026-07-12: AEO works best when the page and schema say the same thing

Context: A repo-wide copy pass needed to improve search and answer-engine clarity without adding vague keyword stuffing.
Learning: For a personal portfolio, the strongest answer-engine optimization is a tight visible summary plus matching structured data. The homepage should say who Ryan is, what he works on, and what proof exists; JSON-LD should reinforce that same claim with `ProfilePage`, `Person`, and concrete `knowsAbout` topics.
Evidence: Today's pass added visible proof points in `site.json` and rendered them on the homepage, then aligned `ProfilePage` and `Person` JSON-LD around the same answer-engine summary instead of adding hidden claims.
Use next time: Do not hide AEO in metadata only. Put the answer on the page first, keep it verifiable, then mirror it in schema.

## 2026-07-11 — Vite environment variables overriding dynamic behavior

Context: We were trying to configure the Strava OAuth app to use a dynamic redirect URI `new URL(..., window.location.origin).href` so that it would work seamlessly on both `localhost:8080` and the production domain `ryanbaumann-portfolio.com`.
Expand Down
8 changes: 8 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Repo copy and trust pass TODO

- [x] Review portfolio copy for Ryan's direct voice, clear claims, and verifiable proof.
- [x] Tighten site metadata for search, answer engines, and accurate summaries.
- [x] Prefer show-don't-tell artifacts over vague claims where the site already has proof.
- [x] Keep security posture explicit: no secrets in browser copy, server-side proxy language where relevant.
- [x] Preserve performance defaults: zero-dependency portfolio, no client JavaScript, no decorative assets.
- [x] Run a narrow portfolio build and update CHANGELOG / LEARNINGS if the pass changes user-visible behavior.
51 changes: 48 additions & 3 deletions portfolio/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ ${articleTags ? articleTags + '\n' : ''}<link rel="icon" href="${BASE}favicon.sv
${robotsTag ? robotsTag + '\n' : ''}${jsonLdTag ? jsonLdTag + '\n' : ''}<style>${CSS}</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<a class="site-name" href="${BASE}">${escapeHtml(site.name)}</a>
<nav aria-label="Site">${nav}</nav>
Expand All @@ -388,6 +389,8 @@ ${content}
<p class="footer-links">
<a href="${site.links.github}" rel="noopener">GitHub</a>
<a href="${site.links.linkedin}" rel="noopener">LinkedIn</a>
${site.links.x ? `<a href="${site.links.x}" rel="noopener">X</a>` : ''}
${site.links.substack ? `<a href="${site.links.substack}" rel="noopener">Substack</a>` : ''}
<a href="mailto:${site.links.email}">Email</a>
</p>
</footer>
Expand Down Expand Up @@ -549,11 +552,23 @@ function jsonLdPerson() {
name: site.name,
jobTitle: site.role,
url: absoluteUrl('/'),
description: site.answerEngineSummary || site.description,
knowsAbout: [
'Google Maps Platform',
'developer experience',
'agent-ready documentation',
'model context protocol',
'agentic evals',
'AI-native developer tools',
'geospatial applications',
],
sameAs: [
site.links.github,
site.links.linkedin,
site.links.x,
site.links.substack,
].filter(Boolean),
image: absoluteUrl(site.defaultShareImage),
image: absoluteUrl(site.profileImage || site.defaultShareImage),
};
}

Expand All @@ -563,11 +578,23 @@ function jsonLdWebSite() {
'@type': 'WebSite',
name: site.name,
url: absoluteUrl('/'),
description: site.description,
description: site.answerEngineSummary || site.description,
author: { '@type': 'Person', name: site.name },
};
}

function jsonLdHomePage() {
return {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
name: `${site.name} portfolio`,
url: absoluteUrl('/'),
description: site.answerEngineSummary || site.description,
about: { '@type': 'Person', name: site.name, url: absoluteUrl('/') },
mainEntity: jsonLdPerson(),
};
}

function jsonLdBlogPosting(entry, pageUrl) {
const ld = {
'@context': 'https://schema.org',
Expand Down Expand Up @@ -677,6 +704,8 @@ function buildHome(collections) {
...(demos.length ? [{ label: 'Demos', href: `${BASE}demos/` }] : []),
{ label: 'GitHub', href: site.links.github, external: true },
{ label: 'LinkedIn', href: site.links.linkedin, external: true },
...(site.links.x ? [{ label: 'X', href: site.links.x, external: true }] : []),
...(site.links.substack ? [{ label: 'Substack', href: site.links.substack, external: true }] : []),
{ label: 'Email', href: `mailto:${site.links.email}` },
];

Expand All @@ -692,9 +721,24 @@ function buildHome(collections) {
`
: '';

const proofPoints = Array.isArray(site.proofPoints) && site.proofPoints.length
? `<dl class="proof-grid" aria-label="Proof points">
${site.proofPoints.map((point) => `<div>
<dt>${escapeHtml(point.label)}</dt>
<dd>${escapeHtml(point.text)}</dd>
</div>`).join('\n')}
</dl>`
: '';
const profileImagePath = site.profileImage?.startsWith('/') ? join(STATIC_DIR, site.profileImage.slice(1)) : null;
const profileImageDims = profileImagePath && existsSync(profileImagePath) ? getImageDimensions(profileImagePath) : { width: 800, height: 800 };
const profileImage = site.profileImage
? `<img class="profile-image" src="${rebase(site.profileImage)}" alt="${escapeHtml(site.profileImageAlt || `${site.name} profile image`)}" width="${profileImageDims.width}" height="${profileImageDims.height}" loading="eager" />`
: '';

const content = `
<section class="hero hero-split">
<div>
${profileImage}
<p class="eyebrow">${escapeHtml(site.tagline)}</p>
<h1>${escapeHtml(site.name)}</h1>
<p class="lede">${escapeHtml(site.intro)}</p>
Expand All @@ -708,6 +752,7 @@ function buildHome(collections) {
<p class="hero-image-caption">From the lab: <a href="${rebase('/strava-explorer/')}">Strava 3D Explorer</a></p>
</figure>
</section>
${proofPoints}

<section>
${sectionHeader('Selected work', '', `${BASE}work/`, 'All work')}
Expand Down Expand Up @@ -745,7 +790,7 @@ ${demosSection}
canonical: absoluteUrl('/'),
ogImage: site.defaultShareImage,
ogImageAlt: `${site.name} — ${site.role}`,
jsonLd: [jsonLdPerson(), jsonLdWebSite()],
jsonLd: [jsonLdHomePage(), jsonLdWebSite()],
}));
}

Expand Down
6 changes: 3 additions & 3 deletions portfolio/content/pages/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ image: /previews/portfolio.jpg
imageAlt: The home page of ryanbaumann-portfolio.com, the site this repo builds and serves
---

Engineer, builder, tinkerer. I have spent 15+ years building developer platforms at Google, Mapbox, and Instabase. I set strategy, and I stay in the work: I dogfood the products, review traces, write evals, ship reference apps, and show up where developers are. The proof lives on this site. [The lab](/demos/) is a set of live apps I build and run myself.
Engineer, builder, tinkerer. I have spent 15+ years building developer platforms at Google, Mapbox, Instabase, and Caterpillar. I set strategy, and I stay in the work: I dogfood products, review traces, write evals, ship reference apps, and show up where developers are. The proof lives on this site. [The lab](/demos/) is a set of live apps I build and run myself.

My operating thesis: **developer experience is a growth discipline.** The platforms that win don't just write better docs. They work backwards from adoption and revenue goals to the product, context, distribution, and ecosystem investments that move them. That holds whether the platform sells maps, document AI, or anything else.

## Google, 2021 to present

**Head of Developer Experience, Google Maps Platform** (2022 to present). I lead Developer Experience Engineering for a platform that powers over 10 million websites and apps. The team spans engineering, product management, technical writing, and developer relations, and operates as one unit with Product and Engineering.

Our current focus is making AI agents first-class users of the platform: agent-ready documentation, curated context, and evals that hold every launch to a measured bar. The launches I led with this team, [Code Assist](https://developers.google.com/maps/ai/code-assist), [agent skills](https://github.com/googlemaps/agent-skills), the agentic eval suite, and distribution into AI Studio, Replit, and Lovable, are detailed in [Work](/work/).
Our current focus is making AI agents first-class users of the platform: agent-ready documentation, curated context, portable skills, and evals that hold every launch to a measured bar. The launches I led with this team, [Code Assist](https://developers.google.com/maps/ai/code-assist), [agent skills](https://github.com/googlemaps/agent-skills), the agentic eval suite, and distribution into AI Studio, Replit, and Lovable, are detailed in [Work](/work/).

**Manufacturing Industry Solutions Manager, Google Cloud** (2021 to 2022). I led product and engineering for Google Cloud's first manufacturing industry solution, [Intelligent Product Essentials](https://cloud.google.com/blog/products/data-analytics/introducing-intelligent-products-essentials-for-manufacturers), from zero to launch with GE Appliances in nine months.

## Instabase, 2020 to 2021

**Director, Solutions Architecture.** I joined the Series-B AI platform for unstructured document automation, built the solutions architecture team, and ran the technical go-to-market motion: demos, proof-of-value, and post-sale implementation. We exceeded the FY2020 revenue quota.
**Director, Solutions Architecture.** I joined the Series-B AI platform for unstructured document automation, built the solutions architecture team, and ran the technical go-to-market motion: demos, proof-of-value, and post-sale implementation. We exceeded the FY2020 revenue target.

## Mapbox, 2016 to 2020

Expand Down
35 changes: 27 additions & 8 deletions portfolio/content/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,44 @@
"role": "Head of Developer Experience, Google Maps Platform",
"tagline": "Maps · Developer Experience · Agent-ready platforms",
"location": "San Francisco, CA",
"intro": "I run Developer Experience for Google Maps Platform, and I still ship. 15+ years building developer platforms at Google, Mapbox, and Instabase, across engineering, product, and go-to-market. This is my home page: the work, the writing, and a lab of live apps I build and run myself.",
"headline": "I treat developer experience as a growth discipline. Each entry covers the goal, what shipped, and what I learned doing it.",
"description": "Ryan Baumann, Head of Developer Experience at Google Maps Platform. Maps, developer experience, and agent-ready platforms from 15+ years at Google, Mapbox, Instabase, and Caterpillar.",
"aboutTeaser": "First customer-facing engineer at Mapbox, where I grew the team from 1 to 15 as the company passed $100M ARR. Director at Series-B Instabase. Industrial IoT at Caterpillar, with 3 patents. Before any of that, two years as a professional cyclist. Racing taught me pacing, measurement, and marginal gains, and I have been applying them ever since.",
"intro": "I run Developer Experience for Google Maps Platform, and I still ship. I have spent 15+ years building developer platforms at Google, Mapbox, Instabase, and Caterpillar. This site stays synced to the places I actually publish: LinkedIn for weekly notes, Substack for longer experiments, X when it is useful, and GitHub for the code.",
"headline": "Selected work, written from the artifact back: the goal, what shipped, and what I learned.",
"description": "Ryan Baumann leads Developer Experience for Google Maps Platform and writes about maps, developer experience, agent-ready platforms, evals, and AI-native distribution.",
"aboutTeaser": "First customer-facing engineer at Mapbox, where I grew the team from 1 to 15 as the company passed $100M ARR. Director at Series-B Instabase. Industrial IoT at Caterpillar, with 3 patents. Before that, two years as a professional cyclist. Same operating system throughout: measure honestly, ship the next artifact, learn in public.",
"sectionIntros": {
"work": "Selected platform, product, and growth work. Each entry covers the goal it served, what actually shipped, and the lesson I took from it.",
"writing": "Notes on developer experience, applied AI, and building platforms agents can use. The weekly series, #ThisWeeksLearnings, lives on LinkedIn.",
"writing": "Notes on developer experience, applied AI, evals, maps, and building platforms agents can use. Claims point back to shipped work, talks, or weekly learning notes.",
"talks": "Talks and videos. I present demo-first: working product over slides, then how it's built, then why it matters.",
"demos": "Live apps I build and run myself: real OAuth, real quotas, real cold starts. One container, one Cloud Run service, no secrets in the browser."
"demos": "Live apps I build and run myself: real OAuth, real quotas, real cold starts. One Cloud Run service, server-side secrets only, and no client JavaScript on the portfolio."
},
"links": {
"github": "https://github.com/ryanbaumann",
"linkedin": "https://www.linkedin.com/in/ryanbaumann/",
"email": "rsbaumann@gmail.com"
"email": "rsbaumann@gmail.com",
"x": "https://x.com/RyanBaumann",
"substack": "https://ryanbaumann.substack.com/"
},
"siteUrl": "https://www.ryanbaumann-portfolio.com/",
"canonicalHost": "www.ryanbaumann-portfolio.com",
"brand": "Ryan Baumann Portfolio",
"positioning": "Maps, developer experience, and agent-ready platforms.",
"defaultShareImage": "/previews/portfolio.jpg",
"socialHandle": "@ryanbaumann"
"socialHandle": "@ryanbaumann",
"proofPoints": [
{
"label": "10M+",
"text": "websites and apps powered by Google Maps Platform"
},
{
"label": "$100M+",
"text": "ARR stage crossed while I grew Mapbox customer engineering"
},
{
"label": "3",
"text": "US patents from industrial IoT work at Caterpillar"
}
],
"answerEngineSummary": "Ryan Baumann leads Developer Experience for Google Maps Platform. He focuses on maps, developer experience, agent-ready documentation, evals, MCP tools, agent skills, and AI-native distribution. His portfolio is a zero-client-JavaScript static site plus live demo apps served from one Cloud Run container.",
"profileImage": "/img/ryan-baumann-profile.svg",
"profileImageAlt": "Ryan Baumann profile image"
}
8 changes: 8 additions & 0 deletions portfolio/static/img/ryan-baumann-profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 60 additions & 2 deletions portfolio/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,29 @@ img { max-width: 100%; height: auto; border-radius: 0.6rem; }
}

.site-header {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
padding-block: 1.5rem;
background: color-mix(in srgb, var(--bg) 92%, transparent);
backdrop-filter: blur(14px);
border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
}
.site-name { font-weight: 700; text-decoration: none; letter-spacing: -0.01em; }
.site-header nav { display: flex; gap: 1.35rem; }
.site-header nav {
display: flex;
gap: clamp(0.75rem, 3vw, 1.35rem);
max-width: 100%;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.site-header nav::-webkit-scrollbar { display: none; }
.site-header nav a {
text-decoration: none;
color: var(--muted);
Expand Down Expand Up @@ -147,6 +161,14 @@ main > section { margin-bottom: clamp(3rem, 7vw, 4.5rem); }
}
.footer-links { display: flex; gap: 1.25rem; }
@media (max-width: 480px) {
.site-header {
align-items: start;
padding-block: 0.85rem;
}
.site-header nav {
width: 100%;
padding-bottom: 0.1rem;
}
.site-footer { flex-direction: column; gap: 0.5rem; }
}

Expand All @@ -172,6 +194,16 @@ main > section { margin-bottom: clamp(3rem, 7vw, 4.5rem); }
max-width: 46rem;
}
.hero-meta { color: var(--muted); font-size: 0.95rem; }
.profile-image {
display: block;
width: clamp(4.5rem, 18vw, 6.5rem);
height: auto;
aspect-ratio: 1;
object-fit: cover;
border-radius: 999px;
border: 1px solid var(--line);
margin-bottom: 1rem;
}

.section-header {
display: flex;
Expand Down Expand Up @@ -368,6 +400,33 @@ main > section { margin-bottom: clamp(3rem, 7vw, 4.5rem); }
.placeholder-note { color: var(--faint); font-size: 0.9rem; margin-top: 1rem; }
.about-teaser .lede { color: var(--muted); }

.proof-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
margin: -1.5rem 0 clamp(3rem, 7vw, 4.5rem);
}
.proof-grid div {
padding: 1rem 1.1rem;
border: 1px solid var(--line);
border-radius: 0.9rem;
background: var(--surface);
}
.proof-grid dt {
font-size: clamp(1.35rem, 4vw, 1.9rem);
line-height: 1;
font-weight: 750;
letter-spacing: -0.04em;
}
.proof-grid dd {
margin: 0.35rem 0 0;
color: var(--muted);
font-size: 0.92rem;
}
@media (min-width: 640px) {
.proof-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}
Expand Down Expand Up @@ -395,4 +454,3 @@ main > section { margin-bottom: clamp(3rem, 7vw, 4.5rem); }
grid-template-columns: minmax(0, 1.15fr) minmax(18rem, .85fr);
}
}

Loading