Structured data for a small hotel site: which schema actually earns its place
A hotel site accumulates structured data the way it accumulates plugins: one gets added because a
theme ships it, another because a booking widget injects its own block, a third because someone
once read that schema helps rankings and pasted a generic template without adapting it. The result
is rarely wrong in a way that throws an error. It is wrong in a way that describes a hotel that does
not quite match the one a guest is looking at.
Hotel, not just LocalBusiness
LocalBusiness validates and renders without complaint on a hotel page, which is exactly the
problem: it is the wrong type, and nothing forces anyone to notice. Hotel is a more specific type
under the same schema.org tree, and it carries fields a generic business listing does not:
checkinTime, checkoutTime, amenityFeature, starRating when the property actually holds a
rating from a real authority, petsAllowed. A validator that only checks for valid JSON will pass
either type. Only a human deciding which type actually describes a hotel with rooms, check-in
hours and amenities will catch the mismatch.
{
"@context": "https://schema.org",
"@type": "Hotel",
"name": "Example Riad",
"checkinTime": "14:00",
"checkoutTime": "12:00",
"amenityFeature": [
{ "@type": "LocationFeatureSpecification", "name": "Wifi", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Pool", "value": true }
],
"address": {
"@type": "PostalAddress",
"addressLocality": "Marrakech",
"addressCountry": "MA"
}
}
Rooms are a separate entity, not a paragraph
A room described only in prose inside the hotel's description field gives a crawler nothing to
extract. Room (or HotelRoom, depending on how detailed the site needs to be) is its own type,
nested or linked under the hotel, and it is where occupancy, bed configuration and price range
belong. A site with six room types deserves six room entities, not one paragraph that lists them
comma-separated. The nesting also matters for maintenance: a room that gets renamed or retired
should not require editing a block of hand-written prose buried in a JSON string.
Price ranges, not fabricated numbers
priceRange is tempting to fill in with a plausible-looking string and forget. The trouble is that
guests, and increasingly AI answer engines summarising a page, treat that field as current. A
priceRange left over from a rate that changed two seasons ago is worse than no priceRange at
all, because it sets an expectation the booking engine will contradict at checkout. If pricing
changes with season or availability and nobody owns updating this field, it is more honest to leave
it out than to ship a number the site itself does not stand behind.
The widget conflict nobody checks for
A booking engine embedded as a third-party widget frequently injects its own Hotel or Product
schema into the page, separate from whatever the site's own template emits. Two blocks describing
the same property, with different check-in times or a different address format, do not cancel each
other out; a parser reads both and has no way to know which one to trust. This is worth an actual
check, not an assumption: view the rendered page source after the widget loads, search for every
application/ld+json block on the page, and confirm there is exactly one describing the hotel
itself. If there are two, one of them needs to be suppressed, usually by a configuration flag in the
booking widget rather than by fighting the template.
Where this actually pays off
None of this changes how the site looks to a visitor. It changes what shows up as a rich result in
search, and increasingly what an AI system quotes when a traveler asks it to compare a few
properties instead of scrolling a search results page. On a direct-booking hotel site built with this in mind, the schema is generated
from the same room and rate data the booking engine already reads, rather than typed once by hand
and left to drift the next time a room is renamed or a rate structure changes.
Testing it like any other output
The same discipline that applies to any structured data applies here: validate the shape before it
ships, not after a rich result silently disappears. A short script that fetches the rendered hotel
page, extracts every ld+json block, and asserts there is exactly one Hotel entity with a
checkinTime, an address and at least one amenityFeature, catches a broken block on the next
deploy instead of three months later, when someone finally notices the star rating vanished from
search results and has no idea why.
A small hotel site does not need every schema type schema.org offers. It needs the handful that
actually describe what a guest is trying to confirm before booking: is there a room available for
these dates, what time can they check in, and is the property what the photos suggest it is.
Everything else is noise a crawler has to parse through to find the part that matters.
Nooralto builds websites and runs search optimisation for businesses, from Agadir and Paris.
Top comments (0)