Task: Add multilingual support
Ворклоги
Half the job is done - added locales and routing

In my case, the implementation is non-standard because the Russian language will be on the current domain - https://vietnamguru.ru, while other languages will be on the new international domain - https://vietnamguru.travel
Now I need to add translation storage to the database, final logic, and translate all documents. Fortunately, according to my structure, almost everything is located within the page content itself, so it will be relatively easy to do all this.
Added a resolver that translates a card into two languages at once - en, vi (in a single request).
It translates. It cost about 10 rubles per translation.

It takes 4 fields as input: name, description, intro, content, with intro and content containing markdown mixed with HTML. content is generally quite a complex field in itself because it also contains templating elements.
The LLM returns the response in yaml format, since this lowers the risk of getting a formatting error due to some unclosed quote. The result is approximately the following response:
responseContent en:
name: |
Dalat
description: |
A mountain city at an altitude of 1,500 m in Vietnam's Central Highlands, known for its French colonial architecture, cool climate, and natural attractions. A popular tourist destination for travelers seeking a break from the coastal heat.
intro: |
Dalat is a city on the Langbiang Plateau at an altitude of 1,500 m in Lâm Đồng Province. It is known for its unique "eternal spring" microclimate with temperatures of 18–21 °C, French colonial-era architecture, and surrounding nature with waterfalls, lakes, and pine forests. Until July 2025, it served as the capital of Lâm Đồng Province.
content: |
<page-hero data-from="#1e3a5f" data-to="#0f172a" data-accent="#3b82f6">
<page-hero-crumbs>
[Home](/) / [Locations](/city) / Dalat
</page-hero-crumbs>
vi:
name: |
Đà Lạt
description: |
Thành phố miền núi ở độ cao 1.500 m tại cao nguyên trung bộ Việt Nam, nổi tiếng với kiến trúc thuộc địa Pháp, khí hậu mát mẻ và các danh thắng thiên nhiên. Điểm đến du lịch phổ biến cho những du khách tìm kiếm sự nghỉ ngơi khỏi cái nóng ven biển.
intro: |
Đà Lạt là thành phố trên cao nguyên Langbiang ở độ cao 1.500 m thuộc tỉnh Lâm Đồng. Nổi tiếng với vi khí hậu "mùa xuân vĩnh cửu" độc đáo với nhiệt độ 18–21 °C, kiến trúc thời kỳ thuộc địa Pháp và thiên nhiên xung quanh với các thác nước, hồ nước và rừng thông. Cho đến tháng 7 năm 2025, nơi đây là tỉnh lỵ của tỉnh Lâm Đồng.
content: |
<page-hero data-from="#1e3a5f" data-to="#0f172a" data-accent="#3b82f6">
<page-hero-crumbs>
[Trang chủ](/) / [Địa điểm](/city) / Đà Lạt
</page-hero-crumbs>
Started translating all pages. A helper has been added to the update itself, which cleans up non-existent links (sometimes AI makes them up) and does this via an MDX parser. Along the way, this mechanism checks for the correctness of HTML tags in the code. The most common error is an incorrect closing tag (opens one tag and closes another). Markup violations result in an error, and such data is not saved. The error rate was approximately 4% of the cards.

That's fine, though. 1,000 cards were processed in 2 hours, and it cost less than 9 dollars. That comes out to roughly 1 ruble per card. I'll run the process again. Already filled cards are skipped.

Next.js i18n Domain Routing in Local Development: Why I Had to Run the Dev Server on Port 80
When setting up the local environment for Next.js, I ran into a rather frustrating limitation of the built-in domain-based i18n routing.
The task seems simple at first glance: the application uses different domains for different locales, and you want to replicate the same setup locally. For example:
const nextConfig: NextConfig = {
i18n: {
locales: LOCALE_CODES,
defaultLocale: 'en',
localeDetection: false,
domains: [
{
domain: 'vietnamguru-v3.localhost',
defaultLocale: 'ru',
locales: ['ru'],
http: true,
},
],
},
}
Next.js officially supports this configuration. The http: true field exists partly for local testing of locale domains over HTTP instead of HTTPS.
The trouble starts with the port.
You Cannot Properly Specify a Dev Port in i18n.domains
A standard Next.js dev server runs on 3000:
http://vietnamguru-v3.localhost:3000
It would be logical to write:
domains: [
{
domain: 'vietnamguru-v3.localhost:3000',
defaultLocale: 'ru',
locales: ['ru'],
http: true,
},
]
However, i18n.domains in Next.js is designed specifically for domains, not arbitrary origins.
The current DomainLocale type looks like this:
export interface DomainLocale {
defaultLocale: string
domain: string
http?: true
locales?: readonly string[]
}
It does not have a port, an origin, or any kind of devPort.
This in itself wouldn't be so bad if Next used the domain solely for locale detection. But domain routing also affects link generation.
<Link href="/place"> Unexpectedly Becomes an Absolute Link
In the application, there is a completely ordinary link:
<Link href="/place">
Что посетить
</Link>
Without domain-based i18n, you would expect HTML roughly like this:
<a href="/place">Что посетить</a>
And the browser naturally opens:
http://vietnamguru-v3.localhost:3000/place
Meaning the current origin, including the port, is preserved automatically.
However, with domain routing enabled, Next.js knows that a specific locale belongs to a specific domain. Therefore, it can generate an absolute locale-domain URL for the Link.
As a result, you get:
<a href="http://vietnamguru-v3.localhost/place">
Что посетить
</a>
And this is where :3000 is lost.
This is especially frustrating because there is no absolute URL in the source JSX:
<Link href="/place">
The Next.js routing layer itself makes it absolute.
This leads to a paradoxical situation:
Current page:
http://vietnamguru-v3.localhost:3000/foo
JSX:
<Link href="/place">
Generated href:
http://vietnamguru-v3.localhost/place
The browser quite reasonably perceives the latter URL as HTTP on the standard port 80.
You Cannot Simply Tell Next.js: "Leave Internal Links Relative"
This is perhaps the main limitation.
In the domain i18n configuration, there is no setting like:
relativeLinks: true
or:
absoluteLocaleLinks: false
There is no way to specify:
port: 3000
and there is no separate dev-origin:
origin: 'http://vietnamguru-v3.localhost:3000'
In other words, the configuration model essentially assumes that the locale domain is available on the standard port of the corresponding protocol.
For production, this is completely normal:
https://example.com
https://example.fr
For local development:
http://example.localhost:3000
— is already a problem.
Why http: true Does Not Solve the Problem
The field name initially gives hope:
{
domain: 'vietnamguru-v3.localhost',
http: true,
}
But it only handles choosing the scheme:
https://
or:
http://
Meaning Next gets enough information to construct:
http://vietnamguru-v3.localhost/place
But information that the local server is on 3000 simply does not exist in this model.
The Reverse Proxy Approach
Architecturally, the cleanest solution is to place a local reverse proxy in front of Next:
http://vietnamguru-v3.localhost
|
v
localhost:3000
For example, via nginx, Caddy, or another local proxy.
Then Next continues to generate:
http://vietnamguru-v3.localhost/place
and this URL actually works.
However, for my current dev environment, this is extra infrastructure just to bypass a framework limitation.
Therefore, a temporary solution turned out to be simpler: running Next.js itself directly on port 80.
Running Next.js on Port 80
The launch itself is elemental:
PORT=80 npm run dev
After that:
http://vietnamguru-v3.localhost
truly is the address of the dev server, and the absolute links generated by Next.js start working correctly.
But the next problem arises.
A Regular User Cannot Listen on Port 80
On Linux, ports below 1024 are traditionally privileged ports.
Therefore, a regular:
PORT=80 npm run dev
may end with a permission error when Node.js attempts to bind to port 80.
The first obvious thought:
sudo npm run dev
But this is a bad option in itself, and in my case, it's also practically unworkable: Node/npm are not installed globally in the system environment.
For example, if Node is managed by a user version manager, the npm visible to the current shell does not necessarily exist in the sudo environment.
You get a classic situation:
npm run dev
works, but:
sudo npm run dev
— does not, or launches an entirely different Node environment.
And running the entire dev server as root just for the ability to open a single port is still undesirable.
CAP_NET_BIND_SERVICE Instead of Running Node as Root
In Linux, the CAP_NET_BIND_SERVICE capability exists for this purpose.
It allows a specific executable to open privileged network ports without running the entire process as root.
For the current Node executable:
which node
you can grant the capability:
sudo setcap 'cap_net_bind_service=+ep' $(which node)
After that, Node can continue to be run as a regular user:
PORT=80 npm run dev
and the process will be able to listen on port 80.
As a result, the local schema becomes:
vietnamguru-v3.localhost
|
| :80
v
Next.js dev
while Next.js domain routing generates:
http://vietnamguru-v3.localhost/place
which now matches the actual address of the application.
Why This Is Still a Workaround
Granting CAP_NET_BIND_SERVICE to node itself is not an ideal universal solution.
The capability is assigned to the Node.js executable, not a specific Next.js project. Consequently, any process launched via that specific Node binary from this environment gets the ability to bind to privileged ports.
Furthermore, if Node is installed via a version manager and the Node version is switched or reinstalled, the path to the executable may change. The capability would then need to be reassigned to the new binary.
You can check current capabilities like this, for example:
getcap $(which node)
Expected result:
/path/to/node cap_net_bind_service=ep
If necessary, the capability can be removed:
sudo setcap -r $(which node)
Therefore, this is specifically a convenient local workaround, not a setting that should be thoughtlessly deployed to all development environments.
Conclusion
The problem turned out not to be DNS, /etc/hosts, React, or browser behavior.
It arises from a combination of several Next.js domain-based i18n features:
i18n.domainsdescribes the hostname but does not provide a separate port setting.http: truelets you choose HTTP instead of HTTPS, but does not specify a dev port.- With domain routing, Next.js can transform a regular
<Link href="/...">into an absolute link to the locale domain. - There is no separate toggle in the configuration to keep such internal links relative.
- Therefore, the standard Next dev port
3000does not mix well with local emulation of domain-based locale routing.
In my case, the temporary solution turned out to be:
sudo setcap 'cap_net_bind_service=+ep' $(which node)
PORT=80 npm run dev
After that, the local hostname can be used without an explicit port:
http://vietnamguru-v3.localhost
and the absolute Next.js locale-domain links match the actual origin.
It works. But it has quite a few infrastructural consequences for something that at the application level looks simply like:
<Link href="/place">
Sources
The official Next.js Pages Router documentation confirms built-in domain routing support, the domains structure, and the purpose of http: true specifically for local HTTP testing.
The current DomainLocale type in vercel/next.js contains domain, defaultLocale, locales, and http, but lacks a separate port setting.
In the Link documentation, standard internal transitions are still specified using relative pathnames (/, /about, /blog/...), meaning you don't need to write the absolute locale-domain URL in the application's JSX.
Added 8 more languages. Updating one card (these 8 languages as well) costs 5 cents.
Interesting fact: although I launched a completely brand new domain and published it just today — https://vietnamguru.travel, AI bots devoured it almost instantly and started vacuuming the new site.


Started translation into another 8 languages. Processed 742 pages in 9.5 hours.

It cost $32.

Hypothesis: The Open Web Will Become a Competitive Advantage Again
Over the past fifteen years or so, the development of web infrastructure has moved toward increasingly restricting machine access.
The reasons were entirely rational. Bots generated load, scraped content, scanned for vulnerabilities, engaged in spam, copied databases, harvested prices, and created fake accounts. In response, websites gradually accumulated CDN, WAF, rate limits, JavaScript challenges, fingerprinting, CAPTCHA, anti-scraping, and behavioral analysis.
As a result, a paradox of the modern internet has emerged:
We created the World Wide Web for the free linking and dissemination of information, and then spent twenty years making that information as inconvenient as possible for automated reading.
For a Web where the primary consumer of a page was a human with a browser, this made sense.
I assume that with the advent of AI, this balance is beginning to shift.
The Bot Is Ceasing to Be Merely a Parasite
In the old economy of a public website, there was a fairly straightforward division:
Human → good visitorSearch crawler → tolerated because it brings humansOther bot → bad visitor
The latter had practically no economic value.
It fetched the page, consumed CPU and bandwidth, and bought nothing.
Therefore, the natural engineering strategy is:
do not let them in.
But AI creates a completely new class of machine consumer.
An AI crawler might read a site not to show its owner a stolen copy of the page, but to subsequently answer a human:
Where can I go on a day trip from Da Lat?
What is the difference between a porter and a stout?
How do you properly use a Finnish sauna?
And if a significant portion of human search indeed shifts from a list of links to a dialogue with AI, a fundamental shift occurs:
the bot becomes an intermediary between the publisher and the human.
This results in:
Old Web:Publisher ↓Google ↓SERP ↓Human ↓WebsiteAI Web:Publisher ↓Machine ↓understanding / synthesis ↓Human
And then an AI bot's request can no longer be automatically considered useless traffic.
Perhaps this is the top of a new user acquisition funnel.
Hence, the Paradox
The industry has spent huge amounts of money building infrastructure perfectly tailored for protecting information from machines, precisely at the moment when machines are becoming one of the primary ways of consuming information.
Moreover, the best and most commercially successful sites are often the most heavily protected.
Cloudflare, WAF, bot protection, dynamic rendering, authorization walls, rate limiting, JavaScript challenges.
In the old model, this is an advantage.
In the new model for a public information resource, part of this infrastructure potentially turns into a distribution handicap.
The most absurd situation might look like this:
A company has the best content in the industry, but the AI knows the competitor better because the competitor's site is easier to read.
Not because the competitor optimized keywords better.
Not because they have more backlinks.
But because their knowledge is physically accessible to a machine.
Second Paradox: Websites Have Learned to Deliver Cheap Information at Great Expense
There is another issue that I consider significant.
Modern web development has been optimized for human sessions for many years.
A single person opens a page, reads it for ten seconds, and clicks the next one.
Therefore, nobody is particularly bothered that fetching a single page triggers:
SSR↓application server↓5 API calls↓15 database queries↓personalization↓analytics↓third-party services↓render
A human is physically slow.
An AI crawler is not.
It is capable of telling the server:
GET page 1GET page 2GET page 3GET page 4GET page 5...
several times a second and keeping it up for hours.
And unexpectedly, it turns out that an architecture that served 1,000 humans brilliantly struggles to serve a single very inquisitive robot.
Therefore, simply saying:
"Fine, tomorrow we'll turn off Cloudflare protection and allow AI crawlers"
might turn out to be impossible.
Over years of a closed Web, many systems have lost the economic ability to be open.
My Bet Is the Opposite
For public knowledge/content projects, I consciously consider mass machine reading to be desirable behavior.
Therefore, the architecture must proceed from the assumption:
My site might be read not by thousands of humans, but by millions of machine requests. And that is a good thing.
Consequently, the marginal cost of serving public knowledge must tend toward zero.
If a crawler wants to read a thousand pages, let it read.
If several independent AI systems want to simultaneously download ten language versions of an encyclopedia, wonderful.
This is not a DDoS, as long as the behavior remains reasonable and the infrastructure can handle it.
This is distribution.
VietnamGuru — The First Experiment
On August 14, 2026, I published a new international domain vietnamguru.travel.
The domain is new.
At the same time, I consciously made it as simple as possible for machine discovery:
- standard indexable URLs;
- server-rendered HTML;
- proper
<a href>tags; - sitemap;
- canonical;
- hreflang;
- open language versions;
- connection to the old
vietnamguru.ru; - no artificial obstacles for normal crawlers.
And almost immediately after publication, various AI crawlers began exploring the new domain.
Some make multiple requests per second and systematically traverse related pages and language versions.
I am not trying to stop this behavior.
On the contrary, I see it as the first observable confirmation that the new distribution channel actually exists.
At the same time, the entire project runs on completely standard infrastructure: a small DigitalOcean server with 4 CPUs and 8 GB of RAM remains far from its performance limits under such crawling.
This is also part of the experiment.
My bet is not only that machines must be allowed to read.
It is also that:
a public knowledge resource must be cheap enough to maintain that it is economically viable to let machines read it aggressively.
But Crawling Proves Nothing by Itself
This is a fundamental caveat.
Today I am only observing:
Discovery.
I still have to test the following stages:
Discovery ↓Crawling ↓Understanding ↓Retrieval ↓Citation ↓Recommendation
The mere fact that GPTBot, PerplexityBot, or any other crawler arrives does not mean the site will gain an audience.
Therefore, the experiment must continue.
The next interesting question:
How soon after launching a brand new domain will independent AI systems be able to correctly answer questions using information from it?
An even stronger test:
When will they start using it for non-branded queries?
Not:
What is VietnamGuru?
but:
What waterfalls near Da Lat are worth visiting?
And finally, the most interesting level:
When will AI independently consider a resource useful enough to recommend it or use it as evidence among other sources?
If the Hypothesis Is Confirmed
Then the very concept of public website optimization will change.
In the previous generation, there was SEO:
help the search engine find the page so it brings a human to it.
In the next generation, a more fundamental task may emerge:
help the machine retrieve, understand, verify, and link your knowledge so it can use it when solving a human's problem.
This is no longer quite SEO.
And not even necessarily GEO/AEO in today's marketing sense.
This is machine accessibility as a property of an information system.
Then the competitive characteristics become:
accessibilitystructurabilityconnectednesssemantic clarityspeedstable URLscheap mass readingprovenancerefreshness
Meaning that many extremely boring engineering properties suddenly become properties of distribution.
And Here Another Paradox Appears
In the old Web, a site's value was partly measured by the number of people it managed to force to come to the site.
AI can destroy this metric.
A human may never open vietnamguru.travel.
They will ask their agent:
Is it worth going to Da Lat in August?
And the agent will read VietnamGuru, compare it with the weather, reviews, transport schedules, and five other sources, and give the human an answer.
From the perspective of Google Analytics:
0 visitors.
From the perspective of real impact:
VietnamGuru participated in the decision-making.
This results in a rather amusing situation:
the successful information site of the future could potentially become more influential while the share of people directly visiting its pages decreases.
And then many of today's Web metrics start measuring the wrong thing.
A More General Bet
My hypothesis is therefore not about VietnamGuru or specific AI crawlers.
It is this:
As we transition from the human-browsed Web to the AI-mediated Web, the ability of a public information system to be freely, massively, and cheaply read by machines will become a competitive advantage.
Today, a significant part of the industry out of inertia treats bot traffic as an expense or a threat.
I assume that for public knowledge resources, part of this traffic will become a distribution channel.
Therefore, a window of opportunity opens.
While other content owners are asking:
How do I ban AI from taking my content?
I want to test the opposite question:
What happens if you make your knowledge corpus one of the most convenient places for an AI wanting to understand your subject matter?
Maybe nothing.
Maybe AI platforms will build completely different mechanisms for acquiring knowledge.
Maybe publishers will indeed close up and a licensing economy will emerge.
Maybe today's crawlers will disappear altogether a year from now.
This is an experiment, not an established fact.
But my bet for August 2026 is simple:
If machines are becoming humanity's new interface to the internet, fighting every machine just because it is a machine is perhaps one of the last habits of the departing Web.
And I am consciously betting on the opposite:
For public knowledge, openness will once again become an advantage.
I finally rolled out the multilingual website on the new domain https://vietnamguru.travel/
The Russian version remains at https://vietnamguru.ru
Surprisingly, Google has already indexed a couple of pages in less than a day.

In less than a day, the following statistics were gathered (grouped by user agent):
