A personal portfolio template built with React and Tailwind CSS 4. Fill in your name, photo, projects, blog posts and skills, and it builds a fast static site in English and Indonesian.
See it in use at https://fama.itsfaqih.dev/, the author's own portfolio.
- Static HTML. React renders every page at build time, one page per
language (
/and/id/). The browser gets about 4 KB of script. - Works without JavaScript. The menu, the language switch and every section work with scripts turned off. The entrance animations are plain CSS. Scripts add the reveal on scroll, smooth scrolling and keyboard focus in the menu.
- Fast on slow networks. A first visit downloads about 58 KB. In Lighthouse's simulated phone, the main content appears in 2.1 s on Regular 3G and 2.8 s on DevTools' slower 3G preset.
- Animated. The hero, sections and full-screen menu animate in, and the animations turn off for visitors who ask for reduced motion.
- Tested. Unit, component and built-site tests, plus lint and type checks, run in CI on every push.
You need Vite+ (vp). It reads .node-version and
package.json to provide the right Node.js (24 or newer) and pnpm (12).
-
Create your own copy of this repository, by forking it or with GitHub's Use this template button, and clone it.
-
Install the dependencies and the Chromium build the tests use:
vp run setup
-
Start the dev server:
vp run dev
It serves the site at https://fama.localhost through portless. The name comes from the
portlessfield inpackage.json. Change it to name your own local domain.
All personal content lives in a few files. Edit them, then run the tests (see Check your changes).
| What | Where |
|---|---|
| Name, job title and photo | src/data/header.ts and public/img/ |
| Resume link | src/data/about.ts |
| Projects and the See more link | src/data/projects.ts |
| Blog posts and the More article link | src/data/blogs.ts |
| Skill logos and their links | src/data/skills.tsx |
| Email and GitHub, LinkedIn, Dribbble links | src/data/contact.ts |
| Greeting, section titles, About and Skills text | src/localization/en/ and src/localization/id/ |
| Notice bar above the page | notice.tsx in each localization folder |
| Page title and description | index.html |
| App name for home-screen installs | public/manifest.json |
| README preview image | .github/preview.png |
The resume, See more and More article links are optional. Leave one out
and its button disappears. Set notice to null in both languages to remove the
notice bar. Each project's type picks the icon on its card:
github, dribbble, figma or web. The three social networks are fixed in
src/components/organisms/Contact/types.ts; change them there and in the
Contact component.
The photo ships in three files, so each screen downloads only the size it needs:
| File | Size | Format |
|---|---|---|
public/img/person.jpg |
480 × 480 | JPEG, for browsers without AVIF |
public/img/person.avif |
480 × 480 | AVIF, for large and high-density screens |
public/img/person-240.avif |
240 × 240 | AVIF, for phones |
To make them from a square photo with ffmpeg and
avifenc from libavif:
ffmpeg -i photo.jpg -vf scale=480:480:flags=lanczos -q:v 3 public/img/person.jpg
ffmpeg -i photo.jpg -vf scale=480:480:flags=lanczos /tmp/person-480.png
ffmpeg -i photo.jpg -vf scale=240:240:flags=lanczos /tmp/person-240.png
avifenc -q 85 -y 444 -s 0 /tmp/person-480.png public/img/person.avif
avifenc -q 85 -y 444 -s 0 /tmp/person-240.png public/img/person-240.avifKeep the file names, or update the paths in src/data/header.ts.
Logos are SVG files in public/icons/. To add one:
- Save it as
public/icons/<name>.svg. - Add its
viewBoxwidth and height to thelogostable insrc/components/atoms/Logo/index.tsx. A test fails if the two disagree. - Use it as
<Logo name="<name>" height={48} />insrc/data/skills.tsxor in the localized About and Skills text.
The site uses Inter, trimmed to only the characters
the pages show, which keeps the font at 25 KB. New text can bring in a character
the font doesn't have yet. When it does, the site test
Every character on / is in the subset font fails and lists the missing
characters. To add them:
- Add the characters to the
unicode-rangeof the@font-facerule insrc/assets/css/styles.css. - Run
vp run font. It downloads Inter and writes the trimmed font tosrc/assets/fonts/. It needs uv. - Commit the updated stylesheet and font.
Emoji are drawn by the system's emoji font and need no change.
The languages are listed in src/contexts/language.ts, and each gets its own
page. Their text lives in src/localization/<language>/. Button labels such as
See more are translated in the section components under
src/components/organisms/.
vp run lint
vp run test
vp run test:components
vp run test:sitetest:site builds the site and checks the real pages, with and without
JavaScript.
| Command | Description |
|---|---|
vp run setup |
Install dependencies and the Chromium build the tests use. |
vp run dev |
Start the dev server at https://fama.localhost (portless). |
vp run build |
Build the static site into dist/. |
vp run preview |
Serve the production build locally on a free port. |
vp run test |
Run the unit tests (shuffled). |
vp run test:components |
Run the component tests in headless Chromium. |
vp run test:site |
Build the site and test the generated pages. |
vp run lint |
Run every lint check: Oxlint, tsc, Oxfmt, actionlint (Docker). |
vp run lint:fix |
Apply Oxlint and Oxfmt fixes. |
vp run font |
Regenerate the trimmed Inter font from styles.css. |
vp run perf |
Measure page load with Lighthouse on simulated 3G. |
src/data/holds the site's content: profile, projects, blog posts, skills and contact.src/localization/holds the text for each language.src/components/holds the atoms, molecules, organisms and the page template.src/pages/Home/puts the content and the current language together.src/entry-server.tsxrenders a page to HTML at build time.src/client/holds the small browser scripts for the menu, language switch, reveal animations and smooth scrolling.src/assets/css/styles.cssis the Tailwind CSS 4 entry stylesheet, with the font and the animation styles.index.htmlis the HTML shell each page is rendered into.tools/static-site/writes one page per language at build time, and holds the built-site tests.tools/fonts/trims the Inter font.tools/perf/measures page load.docs/andAGENTS.mdhold the project's conventions, written for coding agents.
vp run build writes a static site to dist/, which any static host can serve.
The repository is set up for Cloudflare Pages
in wrangler.toml. Change the project name there to your own. With Cloudflare's
Git integration, use these build settings:
- Build command:
pnpm install --frozen-lockfile && pnpm run build - Build output directory:
dist - Production branch:
main
Cloudflare's build image has pnpm but not vp, so the build script runs the vp
binary from node_modules. To upload from your machine instead:
vp dlx wrangler pages project create <name> --production-branch main
vp dlx wrangler pages deploy --branch mainSkip the first command if the project already exists.
- React 19 and TypeScript 7, rendered to static HTML at build time
- Tailwind CSS 4, configured in CSS
- Vite 8 through Vite+, with Oxlint and Oxfmt
- Vitest with Playwright for component and built-site tests
MIT. Keep the license notice, and replace the author's content, photo and links with your own.
