Skip to content
d.nikolaev
All projects
FlagshipProduct

Career Navigator

Regional job aggregator: legacy-to-Next.js migration focused on SEO

Role
Frontend engineer: migration, SEO, new features, maintenance
Period
Jun 2023 — present
Team
6 people
Company
LazurM

A job search platform with vacancies, resumes, applications, chats and career events. I moved the product from a legacy SPA to the Next.js App Router and built its SEO layer — from server-rendered metadata to a sitemap generated from the API.

What I did

  • Migrated a legacy SPA to the Next.js App Router without pausing product work
  • Server-side metadata across 15+ dynamic sections: vacancies, companies, applicants, articles, courses, events
  • The sitemap is generated from API entity identifiers instead of being maintained by hand
  • Around 70 rewrite rules: human-readable URLs for users, latin routes internally
  • Mattermost-based chats with a WebSocket client, applications and notifications
  • SMS and email authentication, VK sign-up, one profile model for applicants and employers

Context

The product

A regional job aggregator: applicants maintain resumes and apply, employers publish vacancies and process applications, and on top of that there are career events, articles, courses and an event map.

By the time I joined the product was already live, but written as a classic SPA: all content rendered on the client, metadata injected through react-helmet, and search engines saw an empty document.

Problem

The problem

For a job aggregator organic search is the main channel: every vacancy and every company page has to be indexable. In the legacy version that simply did not happen.

The second problem was speed. The first screen waited for the bundle and a chain of client-side requests, which was clearly noticeable on weaker devices and mobile networks.

The third was maintainability. Metadata logic lived in configuration objects next to page descriptions; canonical and noindex were set by hand and were easy to lose during changes.

Solution

The solution

I moved the application to the App Router and server-rendered the key sections: vacancy and resume lists, vacancy, company and applicant pages arrive as ready markup. Metadata is derived from the same server request that fetches the page content.

Metadata is described through generateMetadata: title and description are assembled from real entity data — position, salary range, city, the number of open vacancies at an employer. For list pages the description includes the current number of offers so the search snippet is not boilerplate.

The sitemap became a route that asks the API for the identifiers of all public entities and groups them by section. A new vacancy enters the sitemap automatically, with no manual edits.

Public URLs are human-readable: users and crawlers see transliterated paths such as /vakansii and /soiskatel/:id, while the app uses latin routes internally. The mapping is described by rewrite rules — about seventy of them.

On the product side I built Mattermost-based chats with a WebSocket client, applications, SMS and email authentication, VK sign-up, career-island landing pages for offline events and an event map on Leaflet. Errors go to Sentry, behaviour to Yandex.Metrica with session replay.

Outcome

The outcome

Vacancy and company pages became indexable, and the sitemap stopped being manual work. The first screen arrives from the server, so the main content is visible before JavaScript loads.

Metadata stopped being scattered settings: it is assembled in one place from entity data, and adding a new section no longer requires remembering a dozen small fields.

I carried the experience of this migration into this portfolio: the SEO layer here is built the same way, but with a single metadata factory that makes it impossible to forget canonical or language alternates.

Interface

Applicant profile: career path and development direction
Priority vacancies catalogue with search and filters

Related projects