← The daily panel

Building My Portfolio Website: From Design to a Complete Web Application

  • #java
  • #spring boot
  • #web development
  • #thymeleaf
  • #mongodb
  • #portfolio
Photo accompanying Building My Portfolio Website: From Design to a Complete Web Application

From an idea to a working website — what I built, how the important features work, the problems I faced, and what I learned along the way.

When I started working on my portfolio website, my goal was not simply to create a webpage containing my name, skills, and project links.

I wanted something more useful: a personal website where I could document my projects, research, learning journey, and everyday progress, while managing everything through my own admin dashboard.

I also wanted to understand how a real web application works behind the scenes.

How does a browser request become a webpage? How do we store project information? How does an admin dashboard update public content? How do we handle images, authentication, forms, and database operations?

This project gave me the opportunity to explore those questions through implementation.

The finished website is available at jubayer.host.

In this post, I'll explain the development side of the journey. My next post will cover deployment on Azure, Docker, MongoDB Atlas, SSH, Nginx, HTTPS, troubleshooting, and backups.


01. Understanding What I Wanted to Build

The first decision was identifying the difference between a static portfolio and a dynamic web application.

In a static portfolio, project details are often written directly inside HTML.

Whenever I want to add a new project, I have to modify the code and publish the website again.

That wasn't the experience I wanted.

I wanted to update the content without modifying the source code every time.

So I divided the website into two main parts.

Public website

Visitors can:

  • Read my introduction and current activities.
  • Explore projects and their detailed descriptions.
  • View research work and publications.
  • Follow my academic and learning journey.
  • Read posts in the Daily Log.
  • Send messages through the contact form.
  • Access my CV when it is available.

Private admin dashboard

As the website owner, I can:

  • Update my profile and skills.
  • Create, edit, publish, or remove projects.
  • Manage research entries.
  • Update Journey information.
  • Write and publish Daily Log posts.
  • Upload images and CV files.
  • Read messages sent by visitors.

This immediately introduced an important requirement:

Public users should be able to read content, but only the administrator should be able to change it.

That requirement influenced the entire application structure.


02. Choosing How the Website Would Work

I built the application using:

  • Java 26 and Spring Boot 4.1.2 for the backend.
  • Spring MVC for handling website requests.
  • Thymeleaf for generating dynamic HTML.
  • HTML, CSS, and JavaScript for the frontend.
  • MongoDB for storing website content.
  • A session-based admin system for authentication.

I chose a server-rendered approach rather than building a separate frontend application.

This means Spring Boot handles the request, gets the required data, and renders an HTML page using Thymeleaf.

The basic process looks like this:

Visitor opens a page
        ↓
Browser sends an HTTP request
        ↓
Spring Boot Controller receives it
        ↓
Service processes the request
        ↓
Repository retrieves data
        ↓
MongoDB returns the records
        ↓
Thymeleaf generates HTML
        ↓
Browser displays the webpage

For a personal website with one owner and multiple public sections, this approach kept the system understandable without introducing unnecessary complexity.

What I learned: A technology is useful when it solves a particular problem. Adding more technologies does not automatically make an application better.


03. Organizing the Backend Properly

One of the most important things I worked on was keeping the backend organized.

Instead of putting database operations, page rendering, validation, and business rules inside the same Java class, I separated their responsibilities.

My project includes these main layers:

controller / web
       ↓
     service
       ↓
   repository
       ↓
     MongoDB

Controller: Handles incoming requests and determines which page or response should be returned.

Service: Contains the application logic, such as filtering published content, saving messages, and updating projects.

Repository: Handles data access.

Model/domain: Defines the structure of projects, posts, skills, messages, and other records.

Templates: Contains the HTML pages rendered by Thymeleaf.

For example, when a visitor opens the Projects page:

  1. The Projects Controller receives the request.
  2. The Project Service retrieves the relevant projects.
  3. The repository communicates with MongoDB.
  4. The controller passes the results to Thymeleaf.
  5. Thymeleaf generates the project cards.

I found this separation useful while debugging. If a page displays the wrong information, I can investigate the controller and service before changing the frontend.

Lesson: Good project structure makes development easier, especially when features grow beyond the initial plan.


04. Building Dynamic Pages with Spring MVC and Thymeleaf

One of my first major implementation goals was making the website display database content rather than hard-coded information.

For example, my homepage needs featured projects, recent posts, research highlights, and statistics.

The important idea in the actual homepage controller looks like this:

@GetMapping("/")
public String home(
        @ModelAttribute("site") Profile site,
        Model model) {

    HomePageDataService.HomeData data = homePageData.load();

    model.addAttribute("featuredProjects", data.featuredProjects());
    model.addAttribute("latestPosts", data.latestPosts());
    model.addAttribute("stats", data.stats());

    return "index";
}

This is a shortened excerpt showing the important logic.

Here:

  • @GetMapping("/") handles visits to the homepage.
  • homePageData.load() prepares the homepage information.
  • model.addAttribute() passes information to the HTML template.
  • return "index" tells Spring to render the homepage template.

The controller does not manually construct an entire HTML document.

Instead, Thymeleaf takes care of displaying the data.

For example:

<th:block th:each="p : ${featuredProjects}">
    <a th:replace="~{fragments/cards :: projectCard(${p}, ${issueNumbers[p.id]})}"></a>
</th:block>

This loops through the featured projects and renders a reusable project card for each one.

Why was this useful?

Because adding a new project does not require copying and pasting another HTML card into the source code.

The database changes, and the website displays the appropriate content.


05. Designing the Frontend

I wanted the website to have a recognizable visual identity rather than looking like a default template.

The design uses:

  • A warm cream-colored background.
  • Bold, dark typography.
  • Yellow, red, and blue accents.
  • Strong borders and button styles.
  • Large display headings.
  • Card-based sections.
  • A consistent visual style across public and admin pages.

I used CSS to organize reusable design values, including colors, typography, spacing, and components.

This meant I could change the appearance of a shared element without editing every page individually.

For example, my website uses reusable project cards, navigation elements, and other shared sections.

Why reusable components matter

Imagine having ten different pages with the same navigation bar.

If every page contains a separate copy, changing one navigation item means editing ten files.

Using shared Thymeleaf fragments avoids that problem.

I also worked on responsive layouts so content remains readable on different screen sizes.

Some practical CSS issues I encountered included:

  • Long text overflowing its container.
  • Tall images being cropped.
  • Preview panels expanding beyond their intended space.
  • Elements behaving differently on smaller screens.

The solution was not simply making everything smaller.

I had to think about container widths, wrapping, image sizing, overflow behavior, and how people actually use the interface.


06. Making the Projects Section Dynamic

The Projects section was one of the most important features.

I wanted each project to contain more than a title.

A project record could include:

  • Project name and description.
  • Category, status, and technologies.
  • Cover image.
  • Detailed content.
  • Relevant links.
  • Publishing status.

The public page shows project cards, while the detail page displays the complete information.

To make project URLs readable, I used slugs.

Instead of a URL like:

/projects/4

A project can use a more descriptive URL:

/projects/my-third-year-portfolio

This makes links easier to understand and share.

On the backend, the service is responsible for the project data and rules, while the controller decides what to display.

For example, unpublished projects should not automatically appear as public content just because they exist in MongoDB.

Important lesson: Content visibility is a backend rule, not just a CSS rule.

Hiding an unpublished card visually is not enough. The application also needs to control whether the record can be accessed publicly.


07. Building the Research and Journey Sections

For Research, I wanted a place to organize papers and ongoing research activities.

Each research entry contains structured information that can be displayed on a listing page and a detailed page.

I also separated research records according to their status.

For Journey, I wanted a timeline-style experience that could represent education, activities, and other important experiences.

The main development idea was the same:

Keep the information in a structured record and use templates to decide how it appears.

This made both sections manageable through the admin dashboard.

If I update an experience, I should not need to modify the Journey page's HTML manually.


08. Building the Daily Log

The Daily Log became one of the most useful parts of the website.

I wanted somewhere to record project updates, learning notes, research ideas, tutorials, and personal experiences.

Each post can have:

  • A title.
  • Full article content.
  • Category.
  • Tags.
  • Publication date.
  • Optional image.
  • A readable URL.
  • Visible/draft status.
  • Demo/example status.

I also wanted visitors to browse posts by category.

The first version revealed a problem.

Problem: Clicking a category filter jumped the page back to the top.

Technically, the filtering worked. But the user experience was annoying.

The problem came from relying on a full-page navigation every time the visitor selected a category.

What I changed

I used JavaScript to request the new results and update the relevant section without replacing the entire page.

The key concepts were:

fetch(url.pathname + url.search)
history.pushState({ blogNavigation: true }, "", url.href);

The actual implementation also handles response errors, previous requests, scroll position, browser history, and keyboard focus.

The goal was to make filtering feel smoother while keeping normal server-side links as a fallback.

What I learned: A feature can be technically correct but still feel broken to users. Small interaction problems matter.


09. Adding a Markdown Post Editor

I didn't want to write every Daily Log post directly in HTML.

Writing long articles that way would be inconvenient.

So I added support for Markdown.

Markdown allows me to write:

## My Heading

This is **important text**.

- First point
- Second point

Here is `some code`.

The website converts that content into formatted HTML for readers.

I also added a Write / Preview interface in the admin editor.

This introduced another issue.

Problem: Long previews expanded beyond the editor container.

When the preview contained a lot of text, the layout became difficult to use.

I fixed this by keeping the preview within a controlled area that can scroll independently.

I also needed to consider security.

Markdown content should not automatically be allowed to execute arbitrary HTML or unsafe links.

In the application, the Markdown renderer uses options such as:

.escapeHtml(true)
.sanitizeUrls(true)

These are important because rendering user-provided content without appropriate restrictions can introduce security problems.

What I learned: Any feature that accepts and displays text needs to consider both formatting and security.


10. Creating the Admin Dashboard

The admin dashboard was a major step because it changed the website from a fixed collection of pages into something I could maintain myself.

I created administrative interfaces for:

  • Daily Log posts.
  • Projects.
  • Research entries.
  • Journey entries.
  • Skills.
  • Profile information.
  • Messages.

The most important operations are often called CRUD:

  • Create a new record.
  • Read existing records.
  • Update a record.
  • Delete a record.

I learned that a functioning admin button usually involves multiple parts.

For example, when I click Save Project:

  1. The browser sends the form data.
  2. The controller receives and validates it.
  3. The service applies the appropriate rules.
  4. The repository saves it in MongoDB.
  5. The application returns a result or redirects me.
  6. The public website displays the updated information.

At one point, some admin buttons such as creating or editing content were returning error pages or not performing the expected action.

The important lesson was to inspect the complete flow instead of treating every failure as a frontend problem.

A button can look perfect while its backend endpoint is missing, incorrect, or unable to save data.


11. Protecting the Admin Dashboard

A public visitor shouldn't be able to open an admin URL and edit the website.

For this application, I chose session-based authentication.

There was only one administrator, so I did not need public account registration or a complicated multi-role user system.

The basic process is:

Admin enters password
        ↓
Server validates credentials
        ↓
Successful login creates an authenticated session
        ↓
Browser retains a session cookie
        ↓
Protected admin requests check that session

The application also uses security measures such as:

  • Restricted admin routes.
  • Password validation.
  • Login attempt limiting.
  • CSRF protection for state-changing requests.
  • Session renewal on successful login.
  • Secure cookie settings in production.
  • Preventing sensitive admin pages from being cached.

One important part of the login logic is:

request.getSession(true)
       .setAttribute("portfolio.admin", Boolean.TRUE);

This represents the authenticated state stored in the server-side session.

Why not JWT?

JWT is useful in certain architectures, but it wasn't automatically necessary for a single-owner, server-rendered website.

A session-based approach matched the application more naturally.

Security lesson: Choose authentication based on how the application works, not simply because a particular authentication technology is popular.


12. Building the Contact Form and Admin Inbox

The contact form allows visitors to send messages without registering or logging in.

At first, I faced two important problems.

Problem 1: The page jumped after submission.

A normal form submission refreshed or redirected the page, making the experience less smooth.

I introduced a JavaScript-enhanced submission flow so the visitor could submit the form and receive feedback without losing their position.

A traditional form submission remains available as a fallback.

Problem 2: A message could appear successful even when it wasn't saved.

This was more important than the scrolling issue.

I had to ensure that the application only reported success after the message had actually been stored.

The basic server-side sequence became:

Receive form
     ↓
Validate required fields
     ↓
Check spam protection
     ↓
Save message in MongoDB
     ↓
Return actual success response

The code uses a separate message service, form validation, and a hidden spam-trap field.

For example, the server rejects invalid form data rather than blindly accepting it.

After deployment, I tested the actual contact form and verified that messages appeared inside the admin inbox.

What I learned: Showing a success message is not proof that a feature works. Always verify the database result.


13. Handling Project Images and CV Files

Another important concept was understanding the difference between storing information and storing files.

In my application:

MongoDB stores information about the image, such as its filename or URL.

The actual image file is stored separately on disk.

For example:

MongoDB project record
        ↓
Image URL: /media/generated-file.png
        ↓
Application image endpoint
        ↓
Uploaded file on disk
        ↓
Browser displays the image

I used generated filenames so uploads would not rely directly on the original filenames supplied by users.

The application also validates uploaded files before storing them.

This helps reduce problems involving unsupported files, unsafe filenames, and incorrect paths.

For blog posts, I kept the upload feature simple: one image per post.

For images in project cards and detail pages, I adjusted how the image fits inside its container to avoid unnecessary cropping.

Important lesson: A database record containing an image URL is not the same as having the actual image file.

This became especially important later during deployment.


14. Managing Drafts and Demo Content

While developing a personal website, I used sample records to see how pages would look with real content.

However, I didn't want readers to confuse examples with completed work.

So I kept separate concepts for:

  • Published content: Visible to visitors.
  • Draft content: Saved but not publicly visible.
  • Demo/example content: Clearly labeled as sample material.

This mattered for projects, posts, and other sections.

It also allowed me to prepare content through the admin panel before deciding whether to publish it.

What I learned: A personal website should distinguish between examples, work in progress, and actual achievements.


15. Testing the Features

One of my biggest lessons was that a website is not complete simply because the homepage looks correct.

I had to verify actual behavior.

Some of the things I tested included:

  • Page navigation.
  • Project and research detail links.
  • Blog filtering and pagination.
  • Admin login and logout.
  • Creating, editing, and deleting content.
  • Draft and publishing behavior.
  • Form validation.
  • Contact message storage.
  • Image upload and retrieval.
  • Error handling.

I also used automated tests with a separate local MongoDB database.

That separation was important: I didn't want tests creating or deleting records inside my production database.

Before the final deployment, the application passed its automated test suite. After the performance changes, I ran the tests again before rebuilding the Docker image.

The development process taught me to test both the backend rules and the experience in a real browser.


16. The Biggest Performance Lesson

After deploying the website, I noticed that page navigation felt slow.

At first, the website worked correctly, but loading a page took longer than expected.

Instead of immediately upgrading the server, I measured where the time was being spent.

The results were revealing:

  • A static resource loaded in approximately 0.003 seconds.
  • The homepage initially took about 1.56 seconds directly on the server.
  • The homepage took approximately 2.59 seconds over HTTPS from my Mac.

This suggested that the problem wasn't simply the frontend design.

The backend was performing repeated database operations to prepare the pages.

What I changed

I introduced a dedicated homepage data service that retrieves the required records and prepares multiple homepage sections from those results.

Instead of asking MongoDB for overlapping information repeatedly, I reused the information already retrieved.

I also added caching for appropriate public content.

The important concept is:

@Cacheable(
    cacheNames = "publicContent",
    key = "'home.overview'"
)

The production cache keeps a small amount of public information in memory.

It uses a 15-minute expiration, and successful admin changes invalidate the relevant public-content cache.

I deliberately avoided treating private admin pages, contact messages, or rendered session-specific HTML as public cache entries.

The results

After deploying the optimization:

  • Homepage response directly on Azure dropped to approximately 28–29 milliseconds with a warm cache.
  • Average homepage HTML response over HTTPS dropped from approximately 2.59 seconds to 0.96 seconds.

That was around a 63% improvement in the measured remote HTML response time.

This does not mean every browser will load the entire page in 0.96 seconds. Images, scripts, rendering, and network conditions also matter.

But the difference in actual use was noticeable.

What I learned: Before buying more resources, understand where the time is going. Efficient queries and caching can make a surprisingly large difference.


17. What This Project Taught Me

Looking back, I learned much more than how to write a website.

I learned how to connect frontend pages with backend logic, organize services and repositories, manage content through a database, handle user input, protect admin operations, and investigate performance problems.

The most valuable lessons were:

  • Start with the features and user experience, not just the technology stack.
  • Separate responsibilities so controllers, services, and repositories remain understandable.
  • Make important content dynamic instead of hard-coding everything.
  • Test the entire workflow, not only whether a button or page looks correct.
  • Protect sensitive operations even when there is only one administrator.
  • Measure performance before guessing at solutions.
  • Build features that are easy to maintain, not just features that work once.

There are things I would improve in future versions, but I now have a working personal platform that I can maintain through my own dashboard.

The most interesting part was discovering that building the application and making it work reliably on a real server are two different challenges.

That's what I'll discuss in the next post.

Further Reading

For anyone interested in exploring these concepts more deeply:

Next post: From Localhost to a Live Website: My Complete Azure Deployment Journey

Project: jubayer.host

Who goes there?

This door leads to the editor's desk, where new issues get written.