Personal portfolio site for Alireza Afshan, built with React Router, TypeScript, Tailwind CSS, markdown-backed content, Docker, Caddy, and a signed deploy webhook.
- React Router SSR
- TypeScript
- Tailwind CSS
- Markdown content with frontmatter
- Docker production image
- GitHub Actions validation and deploy webhook
Install dependencies:
npm installStart the development server:
npm run devThe app runs at http://localhost:5173.
Run checks before pushing:
npm run typecheck
npm run buildTo build and run using Docker:
docker build -t website-app .
docker run -p 127.0.0.1:3000:3000 website-appPushes to master run .github/workflows/deploy.yml. The workflow installs
dependencies, runs npm run typecheck, runs npm run build, and only then
calls the server deploy webhook.
Required GitHub secrets:
DEPLOY_WEBHOOK_URL: HTTPS URL for the deploy webhook, ending in/deploy.DEPLOY_WEBHOOK_SECRET: shared HMAC signing secret.
The webhook validates X-Hub-Signature-256, accepts only the configured event
and branch, and runs deploy/deploy.sh.
Server secrets and deployment settings belong in /etc/website-deploy.env, not
in git. Start from deploy/website-deploy.env.example.
The Caddy config is intentionally server-managed. Caddy should terminate TLS and
reverse proxy the public site to the app container's local port, for example
127.0.0.1:3000, and proxy the deploy webhook URL to the local webhook service,
for example 127.0.0.1:9000.
Authored content lives in-repo.
- Projects live in
content/projects/*.md - The Showcase page pulls its live links from project
liveUrlfrontmatter - Archived blog posts remain in
content/blog/*.md, but are no longer routed publicly - Project images live in
public/images/projects/... - The resume page content lives in
app/content/resume.ts
Project markdown uses frontmatter plus personal-notes and ai-summary body
sections. Image references should use site-relative paths such as
/images/projects/my-project/cover.png.
- Every standalone public site exposes a visible link back to
https://alirezaafshan.com. - Every deployed public site has a project entry with a
liveUrl, which makes it available from Showcase. - Confirm each repository's actual deploy branch before release; do not assume
mainormaster. - Do not add a
liveUrlfor a prototype until the public deployment exists.
Use this section as a quick map when refreshing page copy.
- Home page: replace the hero eyebrow in
app/routes/home.tsxwith the exact positioning you want visitors to remember. - Home cards: rewrite the card summaries for About, Showcase, Projects, and Resume in
app/routes/home.tsx. - About page: replace the page intro, profile paragraphs, and focus tags in
app/routes/about.tsx. - Projects page: replace the page intro in
app/routes/projects.tsx. - Project entries: rewrite the frontmatter summaries and markdown body sections in
content/projects/*.md. - Showcase page: update live-site copy and the construction crew in
app/routes/showcase.tsx. - Archived blog URLs redirect to Showcase through
app/routes/blog.tsxandapp/routes/blog-post.tsx. - Resume page: update the intro in
app/routes/resume.tsxand the structured resume data inapp/content/resume.ts. - Footer/contact: confirm the displayed footer email in
app/components/page-shell.tsxand resume contact links inapp/content/resume.ts. - Metadata: update route
meta()titles and descriptions in each route after the visible copy is final.
flowchart TD
Visitor["Visitor browser"] --> DNS["DNS: alirezaafshan.com"]
DNS --> Caddy["Caddy on Ubuntu VPS\nTLS termination + reverse proxy"]
Caddy --> AppPort["127.0.0.1:3000"]
AppPort --> Container["Docker container\nReact Router SSR app"]
Container --> StaticAssets["Built assets in /app/build"]
Container --> Content["Markdown content in /app/content"]
Developer["Developer pushes to master"] --> GitHub["GitHub repository"]
GitHub --> Actions["GitHub Actions\nnpm ci + typecheck + build"]
Actions --> SignedWebhook["Signed deploy webhook request"]
SignedWebhook --> CaddyDeploy["Caddy /deploy route"]
CaddyDeploy --> Webhook["Webhook service\n127.0.0.1:9000"]
Webhook --> DeployScript["deploy/deploy.sh"]
DeployScript --> Pull["git fetch + reset to requested SHA"]
Pull --> BuildImage["docker build website-app:timestamp"]
BuildImage --> Restart["stop old container + run new container"]
Restart --> Health["curl health check /"]
Health --> AppPort
Maintained as a personal portfolio and deployment reference.