Browse Cruises

All of our guides were created by personally testing out the cruises in all cities.
A restaurant with a view of the eiffel tower

Paris Dinner
Cruises

A group of people sitting at tables in front of the eiffel tower

Paris River
Cruises

A boat traveling down a river next to a bridge

Amsterdam
Canal Cruises

A british flag flying in the wind next to a bridge

London Thames
River Cruises

Cruise Comparisons

The eiffel tower lit up at night

Bateaux Mouches
vs.
Bateaux Parisiens

(function() { // Hosted ratings.json path: const RATINGS_JSON_URL = "https://raw.githubusercontent.com/adamdimitrov/cruisedetective/refs/heads/main/ratings.json"; function cleanUrl(url) { if (!url) return ""; try { const u = new URL(url); return (u.origin + u.pathname).replace(/\/$/, "").toLowerCase(); } catch(e) { return url.split('?')[0].replace(/\/$/, "").toLowerCase(); } } // Helper to recursively find and update the dateModified field inside a JSON-LD object function updateSchemaDates(obj, dateStr) { let updated = false; if (typeof obj === 'object' && obj !== null) { if ('dateModified' in obj) { obj['dateModified'] = dateStr; updated = true; } // Recursively search child dictionaries and arrays for (let key in obj) { if (updateSchemaDates(obj[key], dateStr)) { updated = true; } } } return updated; } // Helper to format raw duration text (e.g. "1h" -> "1 hour", "1.5h" -> "1.5 hours") function formatDuration(text) { const clean = text.replace("⏳ Duration:", "").replace("⏳", "").replace("Duration:", "").trim().toLowerCase(); if (clean === "1h" || clean === "1 hour" || clean === "60 min" || clean === "60 minutes" || clean === "60 min.") { return "1 hour"; } if (clean === "1.5h" || clean === "1.5 hours" || clean === "90 min" || clean === "90 minutes" || clean === "90 min.") { return "1.5 hours"; } if (clean === "2h" || clean === "2 hours" || clean === "120 min") { return "2 hours"; } return clean; } // Helper to dynamically update the price value inside a text block (e.g. "25€ / 1 hour", "10-32€") function updatePriceText(oldText, newPrice) { // Clean double € typos if present const cleanedText = oldText.replace(/€€/g, "€").trim(); // Regex to find a number preceding a '-' or '€' (e.g. "10-32€" or "25€") const match = cleanedText.match(/(\d+)(?:-(\d+))?\s*€/); if (match) { const oldMin = match[1]; const oldMax = match[2]; if (oldMax) { // Keep max price in a range, update min price return cleanedText.replace(`${oldMin}-${oldMax}€`, `${newPrice}-${oldMax}€`) .replace(`${oldMin}-${oldMax} €`, `${newPrice}-${oldMax} €`); } else { // Update single price return cleanedText.replace(`${oldMin}€`, `${newPrice}€`) .replace(`${oldMin} €`, `${newPrice} €`); } } return newPrice + "€"; } async function initRatingsSync() { console.log("Initializing Cruise Detective Ratings, Price & Duration Sync..."); try { const response = await fetch(`${RATINGS_JSON_URL}?t=${new Date().getTime()}`); if (!response.ok) { throw new Error(`Failed to load ratings.json: ${response.statusText}`); } const ratingsDb = await response.json(); console.log("Successfully loaded ratings database."); // 1. Update the visual Last Updated date and Google Schema dateModified const timestamp = ratingsDb["_timestamp"]; if (timestamp) { const syncDate = new Date(timestamp); const isoDateStr = syncDate.toISOString().split('T')[0]; // Format: YYYY-MM-DD // Format date visually (e.