A portfolio without a working contact form is just a gallery. This template is vitepress-marketing's sibling, built for a freelancer or small studio: a home page, real case studies, and a form that actually reaches you.
By the end you will have a home page, eight example case studies in a generated grid, and a working contact form, built with VitePress and deployed automatically by GitHub Actions.
You need:
- A free GitHub account.
- Node.js 20 or later installed on your computer.
- Some comfort with a terminal.
git clone,npm install,npm run dev, that level.
Don't have some of this yet, or not sure how? Setting up your computer walks through installing Git and Node.js, a basic terminal tutorial, and installing and using VS Code, from scratch.
If any of that sounds like too much right now, first-website needs none of it and still ends with a real site and a working form.
Click Use this template, then Create a new repository. Name it vitepress-portfolio, or
whatever you like.
Naming it something other than
vitepress-portfolio? Opendocs/.vitepress/config.mtsand changebase: '/vitepress-portfolio/'to match (/your-repo-name/). Get this wrong and the deployed site looks completely unstyled, whilenpm run devstill works fine locally, since the dev server never usesbase.
Then clone your new repository to your computer:
git clone https://github.com/yourname/vitepress-portfolio.git
cd vitepress-portfolio
npm install
npm run devOpen the address it prints. That's your site, running on your own computer. It still says Maya Torres. We'll fix that next.
Look for the lines marked CHANGE ME, across a few files:
docs/.vitepress/config.mts: your name, site description, and the nav links.docs/public/logo.svg: your logo. Replace the file itself (any image format works, keep the filenamelogo.svgor update the two places that reference it, listed inAGENTS.md) and it updates in both the header and the footer.docs/.vitepress/theme/style.css: your brand colour. One value, used everywhere: buttons, links, VitePress's own accent colour.docs/index.md: the home page. Theheroandfeaturesblocks are plain YAML frontmatter. The "Selected work" cards below them, and the Work page's own grid, are both generated: neither needs editing when you add or remove a case study.docs/work/*.md: eight example case studies, enough to see the grid at its full 4-column width. Each one's frontmatter (title,description,image, and an optionallinkto a live site) is what shows up on its card; the markdown below that is the page itself, with the sameimagereused as a hero banner at the top. The images that ship with this template are real photos, not mockups: free-to-use (Unsplash License, via Picsum Photos), so the demo looks like a real site, but they're standing in for actual screenshots of your own work. Replace them before this goes live. Delete the ones you don't need; the grid and column count adjust automatically.docs/contact.md: the hero heading, and the location, phone and email in the left-hand info column (or delete any of those three blocks you don't want to list).
Adding a new case study: copy one of the example files, change its frontmatter and body, and you're done. It appears as a card on both the home page and the Work page automatically, with no other file to touch. Deleting a case study file removes its card the same way.
Picking a colour is the hard part. If you have no idea,
--color-brand-500: #2563eb(a plain blue) is a safe default.
Here is the bit most tutorials skip.
A static site has no program running behind it, so there is nowhere for a form to send anything. We will use BootForm, because you can point a form at it and it works immediately, with no account.
3a. Generate your form ID.
It is just a random UUID, and it has to be yours alone. Open BootForm's own UUID
generator and copy what it shows you (or run
crypto.randomUUID() in any browser console). You'll get something like
11111111-1111-4111-8111-111111111111, though a real one won't repeat digits like that.
Use your own. Do not use the one printed above, and do not use a friend's. Whoever claims a form ID first owns it, and everything sent to it goes to them. Yours should be a fresh random one that nobody else has seen.
3b. Paste it into docs/contact.md, replacing __YOUR_FORM_ID__ in the action attribute.
3c. Try it, then claim it at the claim link it gives you, before the site goes live publicly. Held submissions are kept for 48 hours before they're deleted.
git add -A
git commit -m "Make it mine"
git pushIn your repository, Settings → Pages → Source → GitHub Actions (not "deploy from a branch").
The included workflow (.github/workflows/deploy.yml) builds and deploys on every push to main.
Optional: a real domain. The same steps as
one-page-site's Step 5
apply here: a CNAME file in docs/public/, DNS records at your registrar, then Enforce HTTPS
in the same Pages settings. Also change base: '/vitepress-portfolio/' to base: '/' in
docs/.vitepress/config.mts, since a custom domain serves from the root, not a subpath.
You have a real portfolio, with case studies and a working contact form, built the way VitePress is meant to be used for this, not the docs-site defaults.
| A blog | vitepress-blog if you want to write about your work too, with real post tags and RSS. |
| A fuller marketing site | vitepress-marketing if you need a pricing page and more sections than a portfolio calls for. |
| More form options | File uploads, autoresponders, Discord and Slack: bootform.com/docs. |
Open an issue and say what happened.
npm run build fails. Check node --version; this needs Node.js 20 or later.
The site deploys but looks completely unstyled. base in docs/.vitepress/config.mts almost
certainly doesn't match your actual repo name. See the note in Step 1.
A new case study doesn't show up as a card. Check its frontmatter has title, description
and image set, and that the file lives directly in docs/work/ (not a subfolder), matching
work/work.data.ts's work/*.md pattern.
A card's image is broken, or a card links nowhere. These are both built from runtime data
(ProductCard.vue's props), not markdown text, so if you're editing that component directly
rather than just adding a case study file, see AGENTS.md's link-handling section: both :src
and :href need withBase(...), not a plain string.
The form shows raw text instead of a nice page. You are seeing the response as JSON. That is normal for now. Once you claim the form you can set a redirect so people land back on your site.
MIT. Do whatever you like with it, including using it for a real business.