All of our guides were created by personally testing out the cruises in all cities.
(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.