How this website was made

A technical walkthrough of turning an empty repo, a custom domain, and Vercel into this personal website.

Abstract technical illustration of a Git repository flowing through deployment infrastructure into a custom domain.

This website started as an empty GitHub repository and a newly purchased domain: rogerribas.me. The goal was deliberately small for the first version: create a personal website with an About page, a CV page, and a Blog, then make the whole thing easy to change without introducing operational overhead.

The result is a static-first Next.js application deployed on Vercel, backed by a GitHub repository, and served through a Namecheap-managed domain. The interesting part is not that any individual component is complex. It is that the stack gives me a clean publishing loop: edit code or content, commit to GitHub, let Vercel build, and serve the result from the custom domain.

The first practical step was fixing the repository shape. The Git metadata had been created one directory too deep, so the .git folder was moved up to the intended project root. That made the repository match the folder Vercel would eventually build from.

Repository and project root

The repository began with a nesting problem: the outer folder was rogerribas.me, and inside it there was another rogerribas.me folder containing .git. That is harmless locally until tooling starts making assumptions about the project root.

Vercel, GitHub, package managers, and local scripts all work best when the repo root and application root are the same directory. The fix was to move .git up one level and remove the empty nested folder. After that, the project root was:

  • the Git working tree root;
  • the Next.js application root;
  • the Vercel project root;
  • the directory containing package.json.

That alignment matters. It means npm run build, Vercel's build detection, and GitHub's view of the repository all describe the same project.

Application framework

The site uses Next.js with the App Router. For this kind of personal website, that is a pragmatic choice because it gives me file-based routing, metadata support, static generation, and straightforward deployment on Vercel without needing a separate backend.

The initial routes are intentionally simple:

  • app/page.tsx renders the homepage.
  • app/about/page.tsx renders the About page.
  • app/cv/page.tsx renders the CV page.
  • app/blog/page.tsx renders the blog index.
  • app/blog/[slug]/page.tsx renders individual blog posts.

The App Router turns those files into routes. The dynamic blog route uses generateStaticParams() to enumerate the available posts at build time, so each post is prerendered as static HTML.

Content model

Blog posts live in content/blog as Markdown files with frontmatter. The frontmatter contains metadata such as title, description, date, image, and imageAlt; the body contains the article content.

The content loader in lib/posts.ts does three things:

  • reads the post files from disk using Node's fs module;
  • parses frontmatter with gray-matter;
  • validates the required metadata before returning typed post objects.

This keeps the content system small and explicit. There is no CMS, database, or admin UI yet. A new post is just a new Markdown file committed to the repository.

I initially considered rendering MDX directly. That would allow React components inside posts, but it also increases the execution surface. After dependency auditing, I chose a smaller local Markdown renderer instead. The current renderer supports the subset I actually need right now: headings, paragraphs, links, inline code, and lists.

That tradeoff is intentional. If future posts need diagrams, callouts, or richer components, I can either extend the renderer deliberately or reintroduce MDX with clear boundaries. For now, Markdown is enough.

Visual system

The site uses a warm editorial design rather than a default technical template. Most of the styling lives in app/globals.css: typography, page panels, navigation, cards, article layout, and the blog banner treatment.

The homepage also has a subtle reactive background. A small client component, components/ReactiveBackground.tsx, listens for pointer movement and updates CSS custom properties on the document root. CSS then uses those variables to position ambient radial gradients.

The important detail is that the interaction is isolated. The page content stays static and server-rendered; only the decorative background uses client-side JavaScript. It also respects prefers-reduced-motion, so the effect backs off for users who prefer less motion.

Deployment pipeline

The source of truth is GitHub. Once the initial site was built locally, the changes were committed and pushed to the main branch. Vercel was then connected to the project and used its standard Next.js build flow:

  • install dependencies from package-lock.json;
  • run npm run build;
  • prerender the static routes;
  • publish the production deployment.

The build output confirms that the public pages are prerendered as static content. The blog post route is generated through static site generation because the post slugs are known at build time.

There is still room for dynamic behaviour later. Vercel can run serverless functions, edge functions, cron jobs, and incremental regeneration. But for this first version, the static model is simpler and more robust.

Domain and DNS

The domain was purchased through Namecheap. In Vercel, both rogerribas.me and www.rogerribas.me were attached to the project. Namecheap remains the DNS provider, while Vercel handles the deployment, routing, and HTTPS certificate.

The production setup uses www.rogerribas.me as the canonical address. The apex domain, rogerribas.me, redirects to it. That gives a single canonical URL while still making both domain variants work.

After DNS propagated, the checks were straightforward:

  • https://www.rogerribas.me returns HTTP 200;
  • https://rogerribas.me returns a redirect to https://www.rogerribas.me/;
  • Vercel serves the response and manages HTTPS.

Vercel also created local project metadata in .vercel. That folder is useful for local deployments from this machine, but it should not be committed because it is environment-specific. It is therefore ignored in .gitignore.

Dependency and build hygiene

The project uses a small dependency set: Next.js, React, gray-matter, TypeScript and ESLint. After the first install, I ran the usual checks:

  • npm run lint;
  • npm run build;
  • npm audit --omit=dev.

The audit step influenced the implementation. A previous MDX rendering approach introduced a high-severity advisory through a runtime MDX dependency. Removing that dependency and using a local Markdown renderer reduced the production dependency surface.

There was also a moderate PostCSS advisory through Next's dependency tree at the time. An npm override pins PostCSS to a patched version while keeping the current Next.js version. That keeps the audit clean without downgrading the framework.

Blog images

This post also introduces the convention for article banners. A post can specify an image in frontmatter:

  • image points to a file under public;
  • imageAlt describes the image for accessibility;
  • the blog post page renders the image above the article body.

The banner for this entry was generated with OpenAI image generation and saved as a project asset under public/blog. The image is deliberately abstract: repository, build, deployment, and domain routing concepts without relying on specific company logos or fake screenshots.

What comes next

The site now has the basic shape I wanted: a technical profile, a CV page, a Git-backed blog, custom domain routing, and a deployment pipeline that requires very little maintenance.

The next improvements are likely to be incremental: polish the About page, keep the CV current, add richer post components if the writing needs them, and decide whether any genuinely dynamic features are worth introducing.