Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 070 / 100

SEO for travel sites with dynamic pricing for production

What I learned building a travel site on the Amadeus API.

Expona Admin8 min read

Travel platforms live at the intersection of two worlds: static content that search engines love, and real-time pricing data that changes every few seconds. When I built a flight and route search experience on the Amadeus API, I learned that SEO for travel sites with dynamic pricing isn't about hiding prices or generating fake pages—it's about showing search engines a real, crawlable structure while letting JavaScript handle the constantly shifting fares. This guide shares what worked, what broke our indexing, and the patterns that kept us ranking without compromising data freshness.

Static shells, dynamic prices — Route pages are indexable; live prices load fresh

The first instinct when building on an API like Amadeus is to make everything dynamic. You fetch flight data on page load, render it, and call it a day. But from an SEO standpoint, that's a trap. Search engines need something concrete to crawl and cache. The solution is a two-layer architecture: static shell pages for every major route, and client-side price hydration.

Here's what I mean in practice. A route page like /flights/colombo-to-dubai exists as a real, server-rendered HTML file with:

  • Route metadata (departure/arrival cities, distance, average flight time)
  • Structured data (JSON-LD schema)
  • A placeholder for pricing information
  • Useful editorial content (best times to fly, airline options, typical connection patterns)

Then, once the page loads in a browser, a small client-side script fires an API call to Amadeus and populates the live price asynchronously. Googlebot sees the static shell and indexes it. Real users see the current fare.

This approach has a second benefit: resilience. If your API is down, the page still renders. The price section might be empty, but the route is still discoverable and the page doesn't 404.

In Next.js with static generation, it looks something like this:

html
// pages/flights/[departure]-to-[arrival].js
export async function getStaticProps({ params }) {
  // Generate the static shell with route info
  const routeData = {
    departure: params.departure,
    arrival: params.arrival,
    distance: calculateDistance(params.departure, params.arrival),
    averageFlightTime: getAverageFlightTime(params.departure, params.arrival),
  };

  return {
    props: { routeData },
    revalidate: 86400, // ISR: revalidate once per day
  };
}

export default function FlightRoutePage({ routeData }) {
  const [prices, setPrices] = useState(null);

  useEffect(() => {
    // Fetch live prices only in the browser
    fetch(`/api/flights/prices?from=${routeData.departure}&to=${routeData.arrival}`)
      .then(res => res.json())
      .then(data => setPrices(data));
  }, [routeData]);

  return (
    <div>
      <h1>Flights from {routeData.departure} to {routeData.arrival}</h1>
      <p>Average flight time: {routeData.averageFlightTime} hours</p>
      {prices ? <PriceSection prices={prices} /> : <p>Loading current fares...</p>}
    </div>
  );
}

The key is that Googlebot will see the static content and move on before JavaScript runs. You'll have indexable route pages, and users will get real-time pricing.

Route landing pages — Colombo to Dubai deserves a real page with useful information

Every route with search volume should have a dedicated landing page. This isn't a generated page in the sense of "we template this the same way for 50,000 routes." It's a real, intentional page with information that a traveler actually cares about.

When we launched our travel platform, we had routes like Colombo to Dubai, Colombo to Singapore, and dozens more. I initially thought: "Let's just generate a template page for each route and let SEO handle it." That was naive. Colombo to Dubai is a high-volume route with real user demand. It deserves content.

That page should include:

  • Typical airlines on that route (SriLankan, Emirates, Air India, etc.)
  • Best days and times to fly for lower fares
  • Visa and entry requirements for the destination
  • Ground transportation options from Dubai airport
  • Seasonal pricing trends
  • User reviews or common travel tips

This content gives the page E-E-A-T signals (Experience, Expertise, Authoritativeness, Trustworthiness). It also gives users a reason to land on your site instead of going straight to Skyscanner or Google Flights.

In our case, we paired programmatic generation with human editorial review. Every route page got a template structure, but high-priority routes (top 50 by search volume) had custom copy written by someone who actually knew that route. We used structured data to mark up that content:

html
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "FlightRoute",
  "departure": {
    "@type": "Airport",
    "iataCode": "CMB",
    "name": "Colombo Bandaranayake International Airport"
  },
  "arrival": {
    "@type": "Airport",
    "iataCode": "DXB",
    "name": "Dubai International Airport"
  },
  "distance": {
    "@type": "Distance",
    "value": "2500",
    "unitCode": "KMT"
  },
  "averageFlightTime": {
    "@type": "Duration",
    "duration": "PT4H30M"
  },
  "airlineOptions": ["SriLankan Airlines", "Emirates", "Air India"],
  "bestTimeToBook": "3-6 weeks in advance"
}
</script>

This structured data helps search engines understand what your page is about and can lead to rich snippets in search results.

Do not index search results — Generated search pages create infinite duplicates

This is where I saw the most damage. On our initial build, I made every search result page indexable. A user searches for "flights from Colombo to any city on Monday under $500," and we generate a URL like /search?departure=CMB&arrival=*&date=2024-01-15&maxPrice=500. Then we didn't block that URL from crawling.

What happened? Google crawled millions of unique combinations. A single user session could generate 50+ unique URLs. Within weeks, we had billions of crawl requests eating our quota and no real ranking benefit. Our crawl budget was being wasted on parameter variations that had zero external links and minimal search intent.

The solution is simple: block all search result pages from indexing. Use a robots.txt rule or add noindex tags to any page that's the result of a user query or filter.

html
# robots.txt
User-agent: *
Disallow: /search
Disallow: /results
Disallow: /flights?*

# Or in your Next.js page:
<Head>
  <meta name="robots" content="noindex, follow" />
</Head>

The SEO value comes from your route landing pages (Colombo to Dubai), not from dynamic search results. A user might land on the route page, run a search from there, and then book. That journey is fine—the search results page just shouldn't be indexed.

As a bonus, this massively reduced our crawl overhead. We went from millions of indexed URLs to thousands. Our crawl budget recovered, and we started ranking better on the pages that mattered.

Price markup honesty — Show 'from' prices only if they are genuinely bookable

Travel sites love to slap a "from $99" price on a route. Users click, and the cheapest option is sold out or from a carrier with 23-hour layovers. That's not just bad UX—it's a trust issue that affects SEO indirectly. Users bounce, session duration drops, and you look like a spam site to Google.

More importantly, price markup has semantic weight. If you mark up a price in JSON-LD schema, you're asserting that it's real, bookable, and current. Misleading schema can get you flagged.

Our approach: only show a "from" price if it meets three criteria:

  1. The price is available right now (not historical or projected).
  2. It's bookable within the next 48 hours.
  3. It's from a carrier with a reasonable itinerary (no more than 2 stops, total duration under 16 hours for Colombo-Dubai).

If none of those conditions are met, we show a price range or a "prices vary" message instead.

Here's a practical example. We pull live pricing from Amadeus:

html
async function getDisplayPrice(departure, arrival, date) {
  const prices = await amadeus.shopping.flightOffersSearch.get({
    originLocationCode: departure,
    destinationLocationCode: arrival,
    departureDate: date,
    adults: 1,
  });

  // Filter for genuinely bookable options
  const bookableOffers = prices.data
    .filter(offer => offer.instantTicketingRequired === false) // Can be booked without urgency
    .filter(offer => offer.validatingAirlineCodes && offer.validatingAirlineCodes.length > 0)
    .filter(offer => {
      const totalStops = offer.itineraries[0].segments.length - 1;
      const totalDuration = calculateTotalDuration(offer.itineraries[0]);
      return totalStops <= 2 && totalDuration <= 16 * 60; // 16 hours in minutes
    });

  if (bookableOffers.length === 0) {
    return { display: 'Prices vary', detailed: null };
  }

  const cheapest = bookableOffers[0];
  return {
    display: `from ${cheapest.price.total} ${cheapest.price.currency}`,
    detailed: cheapest,
    lastUpdated: new Date(),
  };
}

This approach builds trust. Users aren't surprised when they land on your booking page. And Google sees consistency between your landing page claims and actual availability.

Common mistakes

  • Indexing all parameter variations: Treating every search filter as a unique page instead of disallowing search result pages. This inflates your indexed URL count without SEO value.
  • Relying entirely on client-side rendering: If you render route pages via JavaScript without a static shell, Googlebot sees little content and the page won't rank well. Use SSR or static generation for the core content.
  • Showing outdated prices in markup: Updating the HTML price every 5 minutes but marking it up as current when it's actually 2 hours old. This creates a trust gap with both users and search engines.
  • Ignoring meta descriptions for route pages: Every route page should have a unique, enticing meta description. This doesn't just help CTR—it signals to Google that the page is intentional, not auto-generated spam.
  • Forgetting breadcrumb schema: Travel sites benefit hugely from breadcrumb markup. It helps Google understand your site structure and can lead to breadcrumb rich snippets in search results.

Key takeaways

  • Use static shells (server-rendered or statically generated route pages) with client-side price hydration. This makes pages indexable while keeping prices fresh.
  • Create real landing pages for high-volume routes with useful editorial content, not just templated pages. Invest in E-E-A-T signals.
  • Block all search result and filter pages from indexing using robots.txt or noindex tags. Preserve your crawl budget for route pages and editorial content.
  • Show "from" prices only when they are genuinely bookable right now. This builds trust with both users and search engines, and keeps your schema markup honest.
  • Use structured data (JSON-LD) to mark up route information, airlines, flight times, and prices. This helps search engines understand your content and can lead to rich snippets.
  • Monitor your crawl stats and indexed URL count. A healthy travel site should have thousands of indexed pages, not millions. If your count is climbing rapidly, review your noindex rules.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles