Portfolio

Corporate Website: BALTIC PALACE

Baltic Palace is a unique spot on the map of the Polish coastline, combining modernity with cozy interior designs and a distinctive architectural form. Locat...

#Websites
Corporate Website: BALTIC PALACE

#Baltic-palace.com, technology for an exceptional location on the Baltic Sea

Every independent hotel website answers one commercial question before it answers any design question: will a guest who already knows the property finish the booking here, or will they drift back to a listings portal and hand the commission to someone else. Baltic Palace has forty-four rooms and apartments, which puts it firmly in the range where that question decides the year. A chain property absorbs portal commission as a cost of distribution. A boutique property on the Polish coast feels it directly.

The building sits in Pobierowo, in the West Pomeranian dune belt, roughly a hundred metres from the beach entrance, and was designed by the MTA Architekci studio. The ground floor holds the entrance hall, a bar, a restaurant, treatment rooms and a spa zone with two pools and saunas, and the property also runs conference facilities. That mix matters more than it first appears, because it means the site serves at least three readers who share nothing: a couple planning a weekend, a family booking two weeks in August, and a training coordinator who needs to know how many people fit in a room.

The project ran in 2017 and took about six weeks. The layout and the placement of elements came from the client, so my part was turning that into templates, a content model and responsive behaviour, not deciding how it should look.

#Objective of baltic-palace.com and its audience

The brief was not to invent demand. Brand traffic already existed; the site had to stop leaking it. In practice that means the booking path has to survive the things people actually do, which are not the things a demo does. They open two tabs and compare dates. They press the browser back button halfway through a form. They start on a phone on a train with intermittent signal and finish on a laptop three days later. A booking flow that only works when nothing unusual happens is a booking flow that works in a demo.

The conference and group audience reads the same pages with completely different eyes. They are looking for constraints, not atmosphere: room capacity in each seating arrangement, whether catering can happen in the restaurant, how far the nearest station is. Those readers convert on a specification, and a specification buried in a paragraph of prose is a specification nobody finds.

Then there is seasonality, which shaped the technical decisions more than anything else. Traffic to a Baltic coast property is not a line, it is a set of narrow spikes: January planning, the long May weekend, then the summer peak. Between them the site serves a few dozen visits a day. Infrastructure sized for the peak is money burned for most of the year, and infrastructure sized for the average collapses in precisely the week the property earns its margin.

#Technical features of baltic-palace.com

The frontend uses Tailwind CSS and media queries with WCAG 2.1 in mind. Accessibility here is not a compliance exercise. A large share of guests are older, and a booking form read on a phone in direct sunlight needs contrast that elegant light grey on white simply does not provide. For the same reason, the form fields carry visible labels rather than placeholders that vanish at the first keystroke.

Galleries and the presentation of the building are the heaviest part of the site. Architectural and interior photography is primary content here, not decoration, so compressing it to the point where the texture of a wall becomes a smudge removes the property’s main argument. The building is shown as a three-dimensional model in Three.js, and image galleries load dynamically through GraphQL with srcset handling. GraphQL earns its place for a specific reason: a conventional endpoint returns every field of every image, including the ones a list view never uses, and a single request for a dozen objects with full metadata can outweigh the thumbnails themselves. A query that declares it needs three fields moves that cost from the network into the database, where it is cheaper.

Booking runs through a dedicated module integrated with the Stripe API, with server-side validation and storage in PostgreSQL using AES-256 encryption. Server-side validation is not redundant next to browser validation. The form accepts a date range, and a date range is the easiest field in any booking system to break: a back button, two tabs open at once, a date that was free when the page loaded and taken by the time the form was submitted. Each of those looks like a perfectly valid form from the client side.

The informational section about Pobierowo and the property is written for the searches people actually make in this area, with change notifications accelerated through the Google Indexing API. Backups go automatically to Amazon S3 with cross-region replication, versioning and Zstandard compression. Versioning matters more than the backup itself, because the failure that actually happens to hotels is not a dead server. It is a price list overwritten with the wrong file two days before the May holiday.

Performance rests on Varnish for server-side caching and Cloudflare for media optimisation, with WebP and preconnect for key resources over HTTP/3. Location is handled by a Mapbox GL JS module with GeoJSON data and tiling. On a seaside property, the map answers a question nobody asks out loud: how far is the sea really, given that every property within a kilometre describes itself as beachfront.

There are no result metrics on this page, and that is deliberate. Occupancy, booking volume and revenue belong to the property, not to its contractor’s case study, and none of those numbers could be attributed to a source here. What can be stated honestly, and what actually transfers to the next project, is the reasoning behind the technical decisions.

The spa zone and the conference facilities behave differently from rooms inside the content model, and that difference is easy to get wrong. A room has availability and a price, so it is a record. The spa has no availability in that sense; it has treatments, opening hours and seasonal restrictions. A conference room has a capacity that changes with the seating arrangement, and it is that number, not the photograph, that triggers an enquiry. Forcing all three into a single content type produces fields that stay empty two thirds of the time and an editorial panel nobody can navigate a year later.

Seasonality has a second consequence that is less obvious than load. Offer content ages in steps rather than gradually. Next year’s rates, session dates, the restaurant menu and spa package scopes all change at once, usually in autumn, and somebody at the property then has to update a dozen places in one sitting. If those values live hardcoded in page copy, one page always keeps the old price until spring. The content model therefore separates description from parameter, so a parameter changes in one place.

#Technical challenges and solutions

Gallery weight surfaced first in testing. A large number of high-resolution images delayed the first meaningful paint. Redis handles query caching and Fastly serves as a second CDN layer for parallel media delivery. Splitting media onto its own path makes sense because galleries and HTML documents have completely different invalidation profiles: offer copy changes every few weeks, photography from a shoot essentially never. Keeping both under one cache policy means either reloading images too often or refreshing text too rarely.

The three-dimensional model was slowing down mobile browsers, which for a property whose traffic leans heavily mobile was a real problem rather than a theoretical one. Polygon reduction and Draco texture compression solved the transfer side. The trade-off is worth naming plainly: every such reduction removes detail, and detail is the entire reason the model exists. The line sits where the building still reads as this specific building rather than a generic block.

The booking system stalled under traffic spikes. The cause is structural. A reservation has to check availability, hold the dates, take payment and send confirmation, and the user waits for the last of those while caring only about the first. Separating them with RabbitMQ means the hold and the response happen immediately, while confirmation and downstream synchronisation move to a queue. Rate limiting at the Nginx level protects the same endpoint from double clicks and from the bots that scan availability all through the season.

Stale cache was the last of these and the most expensive commercially. An offer change nobody can see is a phone call from an annoyed guest. Varnish with webhook-driven purge, plus Edge Side Includes for dynamic sections, solves it without giving up caching everywhere else. ESI addresses a specific tension: a room page is ninety percent content that does not change for months and a few percent price and availability that change daily. Without splitting those layers, the cache lifetime for the whole document has to be set by its shortest-lived fragment, which is the same as switching it off.

#Technologies used

Yoast SEO handles metadata, XML sitemaps and search engine notifications. UpdraftPlus runs backups to Amazon S3 with cross-region replication and AES-256 encryption. Cloudflare provides the CDN layer with Argo Smart Routing, Brotli compression and protection against volumetric traffic. Redis holds the object cache with sharding and persistence for queries and sessions. Varnish runs page caching on custom VCL with grace mode and ESI support.

Grace mode deserves its own sentence, because it is the most underrated setting in this configuration. It lets the cache serve a slightly stale page when the backend is not answering, instead of showing an error. For a hotel in peak season, the difference between a page from two minutes ago and an error message is the difference between a booking and no booking.

Lighthouse runs automatically in the CI/CD pipeline on GitHub Actions, so a performance regression appears at the change rather than in a complaint. RabbitMQ queues booking work and confirmation emails with retries. Fastly adds a second media distribution path with geographic optimisation. Mapbox GL JS draws the map with tiling. GraphQL handles gallery and content loading with query batching, and Git together with the pipeline keeps change history in a state where one thing can be rolled back rather than a whole week of work.

It is worth saying what this list does not tell you. It does not tell you how the same stack behaves on a different property. Three caching layers across forty-four rooms with sharp seasonality are justified by exactly this traffic shape. On a property with steady year-round demand the same configuration would be overhead whose maintenance cost outweighs its benefit.

#Management and technical support

The site needs continuous monitoring. System and plugin updates go through a staging environment with a full copy, not through production with optimism. The difference shows up most clearly in the booking module: an update tested on an empty installation always passes, because there is nothing to break, while the same update against a production copy immediately reveals that the plugin changed the date format in existing records.

Cloudflare, Redis and Fastly carry performance under load; Varnish and RabbitMQ carry the stability of dynamic processes. SQL queries run through compound indexes, because an availability search always filters on date and room type together, and an index on either column alone does nothing for that query. Cache is purged surgically on content updates rather than wholesale, because a full purge before a long weekend means the first several hundred visits land on a cold cache.

There is one more tension that recurs in every accommodation project and is better named before the build than after it. A property’s own site and a booking portal describe the same room in two different languages. The portal enforces a description format, a fixed amenity list and its own cropping rules, so it looks identical for every property in town. The owned site is the only place where the things the portal’s form cannot carry can be shown: the shape of a terrace, the view from one specific floor, the way afternoon light enters the restaurant. A site that copies the portal’s structure gives up its only advantage and is left with a worse availability search.

The site can be extended with a property management system integration at the reception end, a seasonal promotions module, or a guest review section. Each of those is a separate scope, quoted after analysis rather than added along the way.

Article FAQ

Frequently asked questions

Practical answers to apply the topic in real execution.

SEO-readyGEO-readyAEO-ready4 Q&A
What scope did the BALTIC PALACE project cover?#
BALTIC PALACE sits in the Websites category and was first delivered in 2017. The entities behind it are GraphQL, Redis, PostgreSQL, Cloudflare and RabbitMQ.
How did delivery run for BALTIC PALACE?#
The build ran about six weeks and went live in 2017. It sits on GraphQL, Redis, PostgreSQL, Cloudflare and RabbitMQ. The layout came from the client. I built the templates and the content model against it, then checked the paths that carry traffic on a copy of production rather than on an empty install.
What was the hardest technical part of BALTIC PALACE?#
Performance under real traffic and cache behaviour. BALTIC PALACE needed staging close to production.
What part of BALTIC PALACE could be reused on another build?#
GraphQL, Redis, PostgreSQL, Cloudflare and RabbitMQ is the part that carries over; that layer looks much the same on the next build. What does not carry over is this project's content model and its integrations, written against one client's data for a Websites brief. A second build starts from a scope review, and the quote follows it.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss