diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts
index 3986b58..55c82a3 100644
--- a/e2e/smoke.spec.ts
+++ b/e2e/smoke.spec.ts
@@ -1,4 +1,9 @@
import { test, expect, devices, type ConsoleMessage, type Page } from '@playwright/test';
+import { bio } from '../src/data/about';
+
+// Stable, distinctive prefix of the first bio paragraph. Sourcing from the
+// shared module keeps this assertion in sync with copy changes.
+const BIO_OPENER = bio[0].slice(0, 40);
const IFRAME_APPS = [
{ id: 'dmarc-mx', name: 'dmarc.mx', url: 'https://dmarc.mx' },
@@ -67,7 +72,9 @@ test.describe('desktop golden path', () => {
// About auto-opens on first boot; clicking the icon focuses the singleton.
await page.locator('button[aria-label="Open About Cory"]').click();
- await expect(page.locator('section[aria-label="About Cory window"]')).toBeVisible();
+ const aboutWindow = page.locator('section[aria-label="About Cory window"]');
+ await expect(aboutWindow).toBeVisible();
+ await expect(aboutWindow.getByText(BIO_OPENER)).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('2-about-open.png') });
// Brand-mark regression: AboutApp header renders the full mark as an
, not an emoji.
@@ -100,6 +107,16 @@ test.describe('desktop golden path', () => {
});
});
+test.describe('about static page', () => {
+ test.use({ viewport: { width: 1440, height: 900 } });
+
+ test('renders shared bio on /about', async ({ page }) => {
+ await page.goto('/about', { waitUntil: 'domcontentloaded' });
+ await expect(page.getByRole('heading', { level: 1, name: 'Cory' })).toBeVisible();
+ await expect(page.getByText(BIO_OPENER)).toBeVisible();
+ });
+});
+
// Strip defaultBrowserType โ can't change browser at describe scope, only viewport/UA matter.
const { defaultBrowserType: _ignoredBrowser, ...iPhone14 } = devices['iPhone 14'];
diff --git a/src/components/os/apps/AboutApp.tsx b/src/components/os/apps/AboutApp.tsx
index 566a077..01cf5e8 100644
--- a/src/components/os/apps/AboutApp.tsx
+++ b/src/components/os/apps/AboutApp.tsx
@@ -1,3 +1,5 @@
+import { bio, currentFocus, socialLinks, tagline } from '../../../data/about';
+
export default function AboutApp() {
return (
@@ -11,38 +13,29 @@ export default function AboutApp() {
About
Cory
-
- Indie builder ยท Cloudflare-native ยท studio-of-one
-
+
{tagline}
-
- I make small, well-scoped software that does one job well and ships. Most of it lives on Cloudflare Workers and Pages, written in TypeScript, deployed continuously.
-
-
- The constant threads: DNS and email security, RSS and feed discovery, contact utilities, AI / Claude Code tooling, and occasional generative-art experiments that exist mostly to make me smile.
-
-
- CortechOS is my attempt at a single memorable home for all of it โ every icon on this desktop opens one of the things I've built, running live.
-
+ {bio.map((paragraph, index) => (
+ {paragraph}
+ ))}
Current focus
-
-
-
-
+ {currentFocus.map(({ badge, title, note }) => (
+
+ ))}
-
-
-
+ {socialLinks.map(({ label, href, value }) => (
+
+ ))}
);
diff --git a/src/data/about.test.ts b/src/data/about.test.ts
new file mode 100644
index 0000000..c34e6a9
--- /dev/null
+++ b/src/data/about.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest';
+import { bio, currentFocus, socialLinks, tagline } from './about';
+
+describe('about data', () => {
+ it('has a non-empty tagline', () => {
+ expect(tagline.trim().length).toBeGreaterThan(0);
+ });
+
+ it('has at least four non-empty bio paragraphs', () => {
+ expect(bio.length).toBeGreaterThanOrEqual(4);
+ for (const paragraph of bio) {
+ expect(paragraph.trim().length).toBeGreaterThan(0);
+ }
+ });
+
+ it('has at least one current-focus item with a badge, title, and note', () => {
+ expect(currentFocus.length).toBeGreaterThan(0);
+ for (const item of currentFocus) {
+ expect(item.badge.trim().length).toBeGreaterThan(0);
+ expect(item.title.trim().length).toBeGreaterThan(0);
+ expect(item.note.trim().length).toBeGreaterThan(0);
+ }
+ });
+
+ it('social links all use https and expose a visible value', () => {
+ expect(socialLinks.length).toBeGreaterThan(0);
+ for (const link of socialLinks) {
+ expect(link.href).toMatch(/^https:\/\//);
+ expect(link.label.trim().length).toBeGreaterThan(0);
+ expect(link.value.trim().length).toBeGreaterThan(0);
+ }
+ });
+});
diff --git a/src/data/about.ts b/src/data/about.ts
new file mode 100644
index 0000000..bea5ac5
--- /dev/null
+++ b/src/data/about.ts
@@ -0,0 +1,33 @@
+export const tagline = 'Indie builder ยท Cloudflare-native ยท studio-of-one';
+
+export const bio: readonly string[] = [
+ "I'm Cory. I build small, well-scoped software and ship it under cortech.online. Most of what I make is one person's worth of work: a DNS tool, an email-auth utility, a QR-code service, a stager for apartment photos. Some of it pays; most of it doesn't. That's fine โ the point is that it exists.",
+ '"Studio of one" is the honest label. No co-founder, no roadmap meetings, no quarterly planning. I pick a problem I actually have, build the smallest thing that solves it, and put it online. If it\'s useful to someone else, even better.',
+ 'The stack is Cloudflare โ Pages, Workers, D1, KV, R2. Not because it\'s trendy, but because one person can run a real product on it without paging themselves at 3 AM. Edge-by-default removes a whole class of infra decisions I\'d rather not be making.',
+ 'CortechOS, the desktop metaphor you\'re looking at, is a joke that kept going. A portfolio site should reflect the shape of the work. Mine is a pile of small windows that each do one thing. So: here\'s a pile of small windows.',
+];
+
+export type FocusItem = {
+ badge: string;
+ title: string;
+ note: string;
+};
+
+export const currentFocus: readonly FocusItem[] = [
+ { badge: '๐ก๏ธ', title: 'dmarc.mx', note: 'DMARC, SPF, DKIM, BIMI, MTA-STS in one scan.' },
+ { badge: '๐ฃ', title: 'donthype.me', note: 'RSS feed discovery and reading without the hype cycle.' },
+ { badge: '๐', title: 'q-r.contact', note: 'Contact QR codes generated locally; no tracking.' },
+ { badge: '๐ง ', title: 'Claude Code tooling', note: 'cclog, claude-view, claudzibit, karkinos โ sharper agents.' },
+];
+
+export type SocialLink = {
+ label: string;
+ href: string;
+ value: string;
+};
+
+export const socialLinks: readonly SocialLink[] = [
+ { label: 'GitHub', href: 'https://github.com/schmug', value: 'github.com/schmug' },
+ { label: 'LinkedIn', href: 'https://www.linkedin.com/in/cory-rankin/', value: 'linkedin.com/in/cory-rankin' },
+ { label: 'Sponsor', href: 'https://github.com/sponsors/schmug', value: 'github.com/sponsors/schmug' },
+];
diff --git a/src/pages/about.astro b/src/pages/about.astro
index 994e494..1315a98 100644
--- a/src/pages/about.astro
+++ b/src/pages/about.astro
@@ -1,5 +1,6 @@
---
import Base from '../layouts/Base.astro';
+import { bio, currentFocus, socialLinks, tagline } from '../data/about';
---
@@ -13,30 +14,36 @@ import Base from '../layouts/Base.astro';
-
- I'm a builder. I like making small, well-scoped things that do one job well and ship. My work lives mostly on Cloudflare (Workers, Pages) and tends to be TypeScript-first.
-
-
- Current focus: DNS and email security (dmarc.mx), contact-card utilities (q-r.contact), AI and Claude Code tooling (cclog, claude-view, claudzibit), and the occasional piece of generative art (AntArt, RepoGotchi).
-
-
- This page is about to turn into CortechOS โ a tiny branded desktop where every icon is one of those projects, running live in a window. Until then, here's the static version.
-
+ {bio.map((paragraph) =>
{paragraph}
)}
-
-
- GitHub
- github.com/schmug
-
-
- Sponsor
- github.com/sponsors/schmug
-
+
+
Current focus
+
+ {currentFocus.map(({ badge, title, note }) => (
+ -
+ {badge}
+
+
+ ))}
+
+
+
+
+ {socialLinks.map(({ label, href, value }) => (
+
+ {label}
+ {value}
+
+ ))}