Back to the blog

How I organized this portfolio’s architecture with Next.js

Routes, components, translations, and content: a tour of this portfolio’s code and the addition of an MDX blog.

Guilherme Caramori ·

How the folders fit together

This portfolio uses Next.js with the App Router and TypeScript. To follow a screen’s implementation, I start with its route file. That leads to the components that build the interface, whose imports point to the text and functions the page uses.

URLs and metadata live in app/. The interface lives in components/, while lib/ holds content and shared functions. Images, documents, and 3D models are in public/. Articles have their own folder, content/blog/, with MDX files separated by language.

The route file

Pages use the filename page.tsx. The about page is in app/about/page.tsx, and the project listing is in app/projects/page.tsx. The file at app/projects/[slug]/page.tsx receives a project identifier through the segment in brackets. Following a URL through the folders leads to its page entry.

The Portuguese home page imports Index and defines metadata with getHomeMetadata('pt-BR'). It also renders LatestPost, passing the language explicitly to show the most recent article. The English entry, app/en/page.tsx, uses the same components.

The root layout handles what these pages share. It configures the Manrope and Fraunces fonts, imports global CSS, and includes the top bar and language context provider. Route content sits inside a main element within that layout. Navigation and the background follow this shared structure when another page opens.

Finding a component

In components/, I grouped files by area of the site: topbar, index, about, projects, resume, and blog. The projects folder contains components for the details of each project, its visual presentation, and the contact area.

The copy-email button and reading animations sit directly in the components folder because they are shared. The level of detail varies with usage: some sections contain substantial presentation, while a reused interaction has its own file. When I need to understand a screen, I follow imports from the route until I reach the part responsible for it.

Page copy and article text

Copy for the portfolio’s main pages lives in lib/languages.ts, in a record typed by language. It includes navigation labels, home page content, professional background, and project information. The modules lib/projects.ts and lib/contact.ts provide other data and functions used by the interface.

On the home page, IndexContent reads the language context and accesses siteContent[language].home. The JSX then uses the set of text for the selected language.

Articles have a different format. The body lives in MDX, and a separate catalog holds the title, description, date, and tags. The presentation component uses those values to build the header. Both translations share that component; each MDX file contains only the article text.

Switching between Portuguese and English

Portuguese pages use URLs without a prefix. English versions start with /en. In lib/locale.ts, functions detect and remove the prefix before constructing a path in the selected language. The English path is built with this code:

if (locale === 'en') {
  return basePath === '/' ? localePrefix : `${localePrefix}${basePath}`
}

The switch reads the URL with usePathname() and navigates with router.push(). Both versions of this article use the slug arquitetura-do-portfolio. Switching languages therefore keeps the article open and changes only the prefix. Publishing both translations under the same slug preserves that behavior.

The server also needs a language for its initial response. The file proxy.ts puts the request pathname in the x-pathname header. The layout reads it with headers() and sets the HTML lang attribute. It still uses this request information when blog slugs are declared during the build; the entire tree is not forced to be static.

Server rendering and browser interactions

The layout and blog pages are Server Components. The catalog stays on the server and imports MDX files explicitly. Articles ship with the code in the deployment, without a CMS request or a production directory scan to find their content.

The top bar and language switch are Client Components: they use context, navigation, and interactions. The home page also has a client layer. For the blog, the layout’s client provider wraps content rendered on the server, which remains a Server Component.

The reading area has its own styles for headings, lists, links, and images. Code blocks use a monospace font and a contrasting background, without syntax highlighting in this version. They accept keyboard focus and scroll horizontally so long lines fit within the screen.

Publishing an article

The Next.js configuration uses @next/mdx. The file mdx-components.tsx defines shared rendering adaptations, and lib/blog.ts associates each translation with its compiled article component. The listing filters out drafts and sorts publications by date:

return posts.filter((post) => post.locale === locale && !post.draft)
  .sort((a, b) => b.date.localeCompare(a.date))

Detail pages use the same selection. An unknown slug or a draft returns 404. The function generateStaticParams declares published identifiers, and the home page uses the first article in the list as its featured post.

Each article has a canonical URL, language alternatives, an author, and article-type Open Graph metadata. A route generates the sharing image with the title and author. The sitemap includes listings and published articles with their editorial dates.

To publish, I add both translations and their catalog entries. After review, the text ships with the repository through the Git and deployment workflow.