Personal portfolio for product design, mobile development, and frontend engineering work. Built with Remix, Three.js, and Framer Motion. Typeset in Inter (variable, Latin subset).
Portfolio content is data-driven:
app/data/project-content.js— project copy, slugs, roles, summaries, and section structure. Also defineslegacyProjectSlugRedirectsfor permanent slug renames.app/data/projects.js— binds project content to imported assets (app/assets/).app/routes/projects.$slug/route.jsx— single dynamic route that handles every project, including 301 redirects for legacy slugs.app/routes/projects/project-case.jsx— maps each sectiontype(hero-image,gallery,image,image-text,comparison-columns,timeline,metrics,project-grid,outcome, default text) to the right layout component.
Assets are prefixed by project slug (porsche-…, posture-…, trekassist-…, little-lemon-…, sayit-…). Each visual ships in three variants — base (name.jpg), name-large.jpg for retina/desktop, and name-placeholder.jpg for the LQIP fade-in. New assets should follow the same convention.
- Add assets to
app/assets/using the<slug>-<purpose>naming convention. - Add the project entry to
portfolioProjectsinapp/data/project-content.js. - Bind its images and homepage preview textures in
app/data/projects.js. - Add the slug to
featuredProjectSlugsonly if it should appear on the homepage. - If the project should be excluded from the public sitemap, add the slug to
SITEMAP_EXCLUDED_SLUGSinscripts/generate-sitemap.cjs. - Run
npm testandnpm run build(build also regeneratespublic/sitemap.xml).
Use the Node version declared in .node-version, then install dependencies:
nvm use
npm installStart the local development server:
npm run devRun the data contract tests:
npm testBuild for production:
npm run buildThe project is configured for Cloudflare Pages:
npm run deployThe contact form uses AWS SES through Cloudflare environment variables. Copy .dev.vars.example to .dev.vars for local development and configure the same variables in Cloudflare Pages for production.
