From d1cb5d9f349d73a978b882c48b4b577b1f45a19e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 23 Apr 2026 17:18:02 -1000 Subject: [PATCH 1/6] Fix author archive parity and pagination --- package.json | 1 + scripts/build-author-post-paths.mjs | 186 +++++ src/components/PaginationNav.astro | 86 +++ src/data/author-post-paths.json | 824 +++++++++++++++------- src/lib/authorArchives.ts | 121 ++++ src/pages/[...slug].astro | 12 +- src/pages/author/[slug].astro | 108 ++- src/pages/author/[slug]/page/[page].astro | 63 ++ src/styles/global.css | 40 ++ 9 files changed, 1118 insertions(+), 323 deletions(-) create mode 100644 scripts/build-author-post-paths.mjs create mode 100644 src/components/PaginationNav.astro create mode 100644 src/lib/authorArchives.ts create mode 100644 src/pages/author/[slug]/page/[page].astro diff --git a/package.json b/package.json index dd99b55..4dae2dd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "migrate:download:rest": "node scripts/export-wp-rest.mjs", "migrate:convert:wxr": "node scripts/convert-wxr-to-content.mjs", "migrate:convert:rest": "node scripts/convert-rest-to-content.mjs", + "migrate:authors:map": "node scripts/build-author-post-paths.mjs", "migrate:related:legacy": "node scripts/extract-legacy-related-posts.mjs", "migrate:media:manifest": "node scripts/build-media-manifest.mjs", "migrate:media:manifest:site": "node scripts/build-wp-content-manifest.mjs", diff --git a/scripts/build-author-post-paths.mjs b/scripts/build-author-post-paths.mjs new file mode 100644 index 0000000..0c09946 --- /dev/null +++ b/scripts/build-author-post-paths.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import matter from 'gray-matter'; + +const args = parseArgs(process.argv.slice(2)); +const outPath = path.resolve(args.out ?? 'src/data/author-post-paths.json'); +const postsDir = path.resolve(args.postsDir ?? 'src/content/posts'); +const wxrDir = path.resolve(args.wxrDir ?? 'data/raw'); + +const AUTHOR_ALIASES = { + Tiffany: 'tiffany', + 'alan@rentmoreweeks.com': 'alan', + 'Our Discount Desk': 'our-discount-desk', + 'Our Travel Reporter': 'our-travel-reporter', +}; + +const ROUTE_ALIASES = new Map([ + ['/where-am-i-24-2/', '/where-am-i-24/'], +]); + +const localPosts = await loadLocalPosts(postsDir); +const authorPathMap = await buildAuthorPathMap(wxrDir, localPosts); + +await fs.mkdir(path.dirname(outPath), { recursive: true }); +await fs.writeFile(outPath, `${JSON.stringify(authorPathMap, null, 2)}\n`); + +console.log(`Author post paths written: ${outPath}`); +for (const [slug, routes] of Object.entries(authorPathMap)) { + console.log(`- ${slug}: ${routes.length}`); +} + +function parseArgs(argv) { + const out = {}; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (!arg.startsWith('--')) continue; + const [key, inlineValue] = arg.split('='); + const name = key.slice(2); + if (inlineValue !== undefined) { + out[name] = inlineValue; + continue; + } + const next = argv[i + 1]; + if (!next || next.startsWith('--')) { + out[name] = true; + } else { + out[name] = next; + i += 1; + } + } + return out; +} + +async function loadLocalPosts(rootDir) { + const files = (await fs.readdir(rootDir)) + .filter((entry) => entry.endsWith('.md')) + .map((entry) => path.join(rootDir, entry)); + + const byWordpressId = new Map(); + const byRoute = new Map(); + + for (const filePath of files) { + const raw = await fs.readFile(filePath, 'utf8'); + const { data } = matter(raw); + if (data.status !== 'publish' || data.draft === true) continue; + + const wordpressId = String(data.wordpressId ?? '').trim(); + const route = normalizeRoutePath(data.path); + const dateValue = toTimestamp(data.date); + if (!route || !dateValue) continue; + + const record = { + wordpressId, + route, + date: dateValue, + }; + + if (wordpressId) byWordpressId.set(wordpressId, record); + byRoute.set(route, record); + } + + return { byWordpressId, byRoute }; +} + +async function buildAuthorPathMap(wxrDir, localPosts) { + const files = (await fs.readdir(wxrDir)) + .filter((entry) => /^wordpress-export-posts-\d{4}\.xml$/.test(entry)) + .map((entry) => path.join(wxrDir, entry)) + .sort(); + + const pathsBySlug = new Map(Object.values(AUTHOR_ALIASES).map((slug) => [slug, new Map()])); + + for (const filePath of files) { + const raw = await fs.readFile(filePath, 'utf8'); + for (const item of iterateItems(raw)) { + const slug = AUTHOR_ALIASES[item.creator]; + if (!slug) continue; + if (item.postType !== 'post' || item.status !== 'publish') continue; + + const sourceRoute = normalizeRoutePath(item.link); + const preferred = localPosts.byWordpressId.get(item.wordpressId); + const fallback = localPosts.byRoute.get(sourceRoute); + const aliasTarget = ROUTE_ALIASES.get(sourceRoute) ?? ''; + const aliasResolved = aliasTarget ? localPosts.byRoute.get(aliasTarget) : null; + const resolved = preferred ?? fallback ?? aliasResolved; + if (!resolved) continue; + + const recordedRoute = + preferred || fallback || !aliasResolved ? resolved.route : sourceRoute; + + pathsBySlug.get(slug).set(recordedRoute, resolved.date); + } + } + + return Object.fromEntries( + [...pathsBySlug.entries()].map(([slug, routes]) => { + const orderedRoutes = [...routes.entries()] + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([route]) => route); + return [slug, orderedRoutes]; + }) + ); +} + +function *iterateItems(xml) { + const itemRegex = /([\s\S]*?)<\/item>/g; + for (const match of xml.matchAll(itemRegex)) { + const item = match[1]; + yield { + creator: decodeXml(extractCdata(item, 'dc:creator')), + status: extractCdata(item, 'wp:status'), + postType: extractCdata(item, 'wp:post_type'), + wordpressId: extractTag(item, 'wp:post_id'), + link: decodeXml(extractTag(item, 'link')), + }; + } +} + +function extractCdata(source, tagName) { + const match = source.match(new RegExp(`<${escapeRegExp(tagName)}><\\/${escapeRegExp(tagName)}>`, 'i')); + return match ? match[1].trim() : ''; +} + +function extractTag(source, tagName) { + const match = source.match(new RegExp(`<${escapeRegExp(tagName)}>([\\s\\S]*?)<\\/${escapeRegExp(tagName)}>`, 'i')); + return match ? match[1].trim() : ''; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function decodeXml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(/–/g, '–') + .replace(/’/g, '’') + .replace(/“/g, '“') + .replace(/”/g, '”') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +function normalizeRoutePath(value) { + const raw = String(value || '') + .replace(/https?:\/\/blog\.hichee\.com/i, '') + .replace(/%ef%bf%bc/gi, '') + .replace(/\uFFFC/g, '') + .trim(); + + if (!raw) return ''; + + const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`; + return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; +} + +function toTimestamp(value) { + if (value instanceof Date) return value.getTime(); + const parsed = Date.parse(String(value || '')); + return Number.isFinite(parsed) ? parsed : 0; +} diff --git a/src/components/PaginationNav.astro b/src/components/PaginationNav.astro new file mode 100644 index 0000000..5c38f47 --- /dev/null +++ b/src/components/PaginationNav.astro @@ -0,0 +1,86 @@ +--- +interface Props { + basePath: string; + currentPage: number; + totalPages: number; +} + +const { basePath, currentPage, totalPages } = Astro.props; +const items = totalPages > 1 ? buildItems(currentPage, totalPages) : []; +const previousHref = totalPages > 1 && currentPage > 1 ? pageHref(basePath, currentPage - 1) : null; +const nextHref = totalPages > 1 && currentPage < totalPages ? pageHref(basePath, currentPage + 1) : null; + +function buildItems(current: number, total: number) { + const pages = new Set([1, total, current - 1, current, current + 1]); + if (current <= 3) { + pages.add(2); + pages.add(3); + } + if (current >= total - 2) { + pages.add(total - 1); + pages.add(total - 2); + } + + const ordered = [...pages] + .filter((page) => page >= 1 && page <= total) + .sort((a, b) => a - b); + + const items = []; + let previous = 0; + for (const page of ordered) { + if (previous && page - previous > 1) { + items.push({ type: 'ellipsis', key: `ellipsis-${previous}-${page}` }); + } + items.push({ type: 'page', key: `page-${page}`, page }); + previous = page; + } + return items; +} + +function pageHref(rootPath: string, page: number) { + if (page <= 1) return rootPath; + return `${rootPath}page/${page}/`; +} +--- + +{ + totalPages > 1 && ( + + ) +} diff --git a/src/data/author-post-paths.json b/src/data/author-post-paths.json index 6a50a50..1f0bd96 100644 --- a/src/data/author-post-paths.json +++ b/src/data/author-post-paths.json @@ -1,286 +1,596 @@ { - "alan": [ - "/20-free-tools-that-make-your-hospitality-website-rock/", - "/5-steps-to-clean-up-and-organize-your-vacation-rental-website/", - "/7-short-term-rental-myths-debunked/", - "/airbnb-alternatives-2024/", - "/airbnb-alternatives-airbnb-v-booking-com-decoding-the-best-deals/", - "/airbnb-alternatives-airbnb-v-vrbo-a-guide-for-savvy-travelers/", - "/airbnb-alternatives-hipcamp-guide-discovering-the-best-hidden-gems/", - "/airbnb-bookings-hack-get-20-more-bookings-starting-today/", - "/airbnb-ceo-vows-to-tackle-runaway-cleaning-fees/", - "/airbnb-gift-cards-all-you-need-to-know-about-these-perfect-gifts/", - "/airbnb-hosts-is-it-time-to-review-your-views-on-reviews/", - "/airbnb-or-vrbo-where-should-i-list-my-property/", - "/airbnb-rental-arbitrage-and-how-to-succeed-at-it/", - "/airbnbs-10-fold-increase-in-host-cancellation-fees-could-benefit-guests/", - "/airbnbs-latest-update-sparks-outrage-could-this-be-the-end-for-many-hosts/", - "/amazing-disney-themed-vacation-rental-in-kissimmee-and-how-to-book-it-for-less/", - "/amazon-travel-essentials-you-need-in-2022-my-top-10-travel-products/", - "/best-airbnb-smart-home-devices-part-3-moneymaking-wifi/", - "/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022%ef%bf%bc/", - "/build-a-backyard-pickleball-court-for-more-bookings/", - "/check-out-this-wonderful-treehouse-and-book-it-for-less/", - "/does-social-media-drive-direct-bookings-for-your-vacation-rental/", - "/dont-get-scammed-on-airbnb-find-out-the-ugly-truth-and-save-big-money/", - "/dont-give-up-the-daydream-no-1/", - "/dont-give-up-the-daydream-no-2/", - "/dont-give-up-the-daydream-no-3/", - "/dont-give-up-the-daydream-no-4/", - "/dont-give-up-the-daydream/", - "/fair-use/", - "/free-airbnb-welcome-book-template/", - "/free-short-term-rental-agreement-templates-word-pdf/", - "/gatlinburg-4-nights-save-192/", - "/getting-more-direct-bookings-the-value-of-repeat-guests/", - "/holiday-let-emails-messages-to-send-to-guests/", - "/hot-tub-photography-for-airbnb-hosts/", - "/how-to-furnish-your-airbnb-like-a-pro-and-a-few-shopping-secrets/", - "/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo%ef%bf%bc/", - "/labor-day-weekend-2022-vacation-rental-market-data-forecast/", - "/lowest-price-vacation-rentals-magic-search-without-witchcraft/", - "/new-york-city-3-nights-save-474/", - "/pack-up-and-go-with-these-15-travel-hacks-and-more-diy-ideas-by-crafty-panda/", - "/pet-friendly-airbnb-guide/", - "/smart-home-devices-for-your-airbnb-a-complete-guide/", - "/smart-home-monitoring-for-your-airbnb-a-complete-guide-pt-2/", - "/the-10-best-things-to-do-in-miami-a-video-guide/", - "/the-coming-collapse-of-airbnb-short-term-rentals/", - "/the-meaning-behind-hichee/", - "/the-pros-and-cons-of-booking-direct-vacation-rental-secrets/", - "/the-short-term-rental-hosts-guide-on-how-to-survive-the-recession/", - "/the-ultimate-guide-to-selecting-the-perfect-travel-insurance-for-your-2023-adventure/", - "/today-is-book-direct-day-but-does-it-really-work/", - "/top-10-travel-tips-tricks-from-a-flight-attendant/", - "/travel-hack-airbnb-5-times-the-price-i-save-4100-in-1-minute/", - "/travel-quotes-101/", - "/travel-quotes-102/", - "/travel-quotes-103/", - "/travel-quotes-104/", - "/travel-quotes-105/", - "/travel-quotes-106/", - "/travel-quotes-107/", - "/travel-quotes-108/", - "/travel-quotes-109/", - "/vacation-rental-damage-deposits-all-you-need-to-know-including-tips-from-hosts/", - "/vacation-rental-deals-st-barts-4-guests-july-3rd-july-16th/", - "/vacation-rental-price-comparison-watch-as-i-save-5836-on-this-airbnb-in-under-1-minute/", - "/vacation-rental-website-builders-what-are-your-best-options/", - "/vacation-rentals-best-price-and-100-peace-of-mind-every-time/", - "/vacation-rentals-book-direct-with-the-owner-and-save-heres-how/", - "/vacation-rentals-cancellation-policies-compared-all-you-need-to-know/", - "/vrbo-and-airbnb-youll-never-guess-what-vrbo-are-doing/", - "/want-to-really-get-away-heres-some-expert-advice-for-digital-nomad-accommodations/", - "/were-all-in-the-airbnb-business-now-part-2/", - "/where-am-i-1/", - "/where-am-i-2/", - "/where-am-i-3/", - "/why-youll-be-750-times-better-off-running-an-airbnb-than-a-vacation-rental-or-holiday-let/", - "/youll-lose-thousands-hosting-on-airbnb-if-you-do-this%ef%bf%bc/" - ], - "our-discount-desk": [ - "/35-secrets-to-save-money-on-your-next-vacation/", - "/arizona-4-nights-save-108/", - "/arizona-7-nights-save-295/", - "/california-7-nights-save-1915/", - "/cancun-7-nights-save-3927/", - "/catalonia-6-nights-save-121/", - "/costa-rica-7-nights-save-110/", - "/florida-5-nights-save-1861/", - "/indiana-5-nights-save-661/", - "/north-carolina-4-nights-save-41/", - "/quebec-3-nights-save-217/", - "/saint-lucia-7-nights-save-484/" - ], - "our-travel-reporter": [ - "/airbnb-cabins-get-close-to-nature-in-these-hidden-gems/", - "/airbnb-treehouses/", - "/property-spotlight-a-delightful-a-frame-cabin-best-price-revealed/", - "/property-spotlight-stunning-beach-house-and-how-to-book-it-for-less/", - "/see-how-to-save-over-100-a-night-on-this-wonderful-treehouse-in-georgia/", - "/top-7-best-vacation-rental-channel-managers-in-2022/", - "/top-9-website-builders-property-managers-use-for-their-vacation-rental/" - ], "tiffany": [ - "/9-most-expensive-airbnb-luxe-properties-in-the-us/", - "/9-of-the-best-frozen-themed-airbnbs-in-the-us-love-is-an-open-door/", - "/9-of-the-best-gites-in-france/", - "/9-of-the-most-expensive-airbnb-getaways-in-europe/", - "/a-peek-inside-the-most-instagrammable-airbnb-in-miami/", - "/a-romantic-airbnb-in-california-relationshipgoals/", + "/pet-friendly-airbnbs/", + "/top-eerie-escapes-to-stay-for-halloween/", + "/hocking-hills-airbnbs-with-hot-tubs/", + "/oklahoma-airbnbs-with-hot-tubs/", + "/the-redditors-guide-to-airbnb-hosting-best-communities-for-advice-and-support/", + "/best-airbnbs-destin/", + "/best-marco-island-airbnbs/", + "/best-airbnbs-fort-lauderdale/", + "/best-chrome-extensions-for-travelers/", + "/barbie-airbnb-in-california/", + "/anna-maria-island-rental-near-holmes-beach/", + "/airbnb-key-west/", + "/estes-park-cabins-with-hot-tubs/", + "/price-check-this-orlando-airbnb-with-pool-and-save-money/", + "/barbie-themed-airbnbs/", "/a-romantic-airbnb-with-hot-tub/", - "/a-trendy-miami-airbnb-with-panoramic-morning-views/", - "/a-wizards-retreat-discovering-the-magic-of-a-harry-potter-themed-airbnb/", - "/adult-themed-airbnb-in-florida-do-you-dare-go-down-the-rabbit-hole/", - "/adult-themed-airbnbs-10-steamy-escapes-for-the-grown-ups/", "/airbnb-anna-maria-island/", - "/airbnb-beach-house-retreat-a-sun-kissed-escape/", - "/airbnb-condo-in-miami-experience-the-high-life/", + "/romantic-getaways/", "/airbnb-florida/", - "/airbnb-jacksonville/", - "/airbnb-key-west/", - "/airbnb-miami/", + "/airbnb-condo-in-miami-experience-the-high-life/", + "/miami-airbnb-with-pool-dive-into-your-dream-vacation/", + "/waterfront-airbnb-tampa-fl-family-friendly-find-with-a-view/", + "/romantic-airbnb-new-mexico-glamping-at-el-mistico-ranch/", + "/orlando-fl-airbnb-your-ticket-to-paradise-in-the-heart-of-orlando/", + "/a-peek-inside-the-most-instagrammable-airbnb-in-miami/", "/airbnb-orlando-near-disney-a-luxe-shipping-container/", - "/airbnb-orlando/", + "/tampa-airbnb-with-hot-tub-treat-yourself-to-a-romantic-break/", + "/a-trendy-miami-airbnb-with-panoramic-morning-views/", + "/airbnb-jacksonville/", + "/a-romantic-airbnb-in-california-relationshipgoals/", "/airbnb-tampa/", - "/airbnb-wedding-venue-in-alabama-a-secret-gem-for-your-special-day/", - "/airbnb-wedding-venue-in-costa-rica-discover-your-dream-destination/", - "/airbnb-wedding-venue-in-montana-a-luxurious-hideaway-for-your-i-dos/", + "/best-secluded-romantic-airbnb-in-california/", + "/airbnb-orlando/", + "/airbnb-beach-house-retreat-a-sun-kissed-escape/", + "/romantic-airbnbs/", + "/pet-friendly-airbnb-a-furry-friendly-find-for-your-next-adventure/", + "/disney-themed-airbnb-for-your-little-superheros/", + "/pet-friendly-airbnb-in-north-carolina-where-tails-wag-and-hearts-warm/", + "/airbnb-miami/", + "/adult-themed-airbnb-in-florida-do-you-dare-go-down-the-rabbit-hole/", + "/pet-friendly-airbnb-in-montana-with-paw-some-perks/", + "/disney-themed-airbnb-in-kissimmee-florida-with-millennium-falcon-bed/", + "/gold-medal-accommodations-your-ultimate-guide-to-where-to-stay-during-the-paris-olympics-2024/", + "/disney-themed-airbnb-in-kissimmee-where-dreams-come-true/", + "/an-airbnb-treehouse-adventure-in-georgia-unwind-in-natures-embrace/", + "/an-airbnb-treehouse-experience-ultimate-getaway-in-the-sky/", + "/disney-themed-airbnb-in-florida-a-magical-stay-for-disney-fans/", + "/airbnb-wedding-venue-in-washington-a-romantic-retreat-for-your-big-day/", + "/adult-themed-airbnbs-10-steamy-escapes-for-the-grown-ups/", "/airbnb-wedding-venue-in-new-mexico-an-iconic-spot-for-saying-yes/", + "/tie-the-knot-at-this-fancy-airbnb-wedding-venue-in-florida/", + "/airbnb-wedding-venue-in-montana-a-luxurious-hideaway-for-your-i-dos/", + "/airbnb-wedding-venue-in-alabama-a-secret-gem-for-your-special-day/", "/airbnb-wedding-venue-in-oregon-a-breathtaking-setting-for-lovebirds/", - "/airbnb-wedding-venue-in-washington-a-romantic-retreat-for-your-big-day/", + "/unique-airbnb-wedding-venue-to-tie-the-knot/", + "/airbnb-wedding-venue-in-costa-rica-discover-your-dream-destination/", + "/harry-potter-airbnb-in-massachusetts/", + "/harry-potter-airbnb-fantasy-where-wizardry-and-comfort-unite/", + "/harry-potter-airbnb-journey-rent-harry-potters-childhood-home/", + "/harry-potter-airbnb-dream-book-your-stay-now/", + "/harry-potter-airbnb-adventure-stay-in-a-wizarding-world-wonderland/", + "/harry-potter-airbnb-magic-experience-the-ultimate-wizards-retreat/", "/airbnbs-with-pickleball-courts-9-of-the-best-game-on/", - "/an-airbnb-treehouse-adventure-in-georgia-unwind-in-natures-embrace/", - "/an-airbnb-treehouse-experience-ultimate-getaway-in-the-sky/", - "/anna-maria-island-rental-near-holmes-beach/", - "/barbie-airbnb-in-california/", - "/barbie-themed-airbnbs/", - "/barcelona-catalonia-7-nights-save-780/", - "/barcelona-catalunya-8-nights-save-176/", - "/bellagio-lombardy-3-nights-save-795/", - "/best-airbnbs-destin/", - "/best-airbnbs-fort-lauderdale/", - "/best-chrome-extensions-for-travelers/", - "/best-marco-island-airbnbs/", - "/best-secluded-romantic-airbnb-in-california/", + "/muggle-to-magician-stay-in-a-harry-potter-themed-airbnb/", + "/the-spellbinding-stay-experience-a-harry-potter-airbnb-adventure/", + "/a-wizards-retreat-discovering-the-magic-of-a-harry-potter-themed-airbnb/", + "/linkedin-groups-for-airbnb-hosts-the-ultimate-network-for-success/", + "/the-best-travel-accessories-for-2024-a-must-have-guide-for-globetrotters/", + "/property-spotlight-unveiling-the-ultimate-castle-rental-experience/", + "/unique-airbnb-stays/", + "/property-spotlight-amazing-glamping-rental/", + "/property-spotlight-a-cool-tiny-house-with-big-savings/", "/best-travel-apps-for-stress-free-journeys-in-2024/", "/creating-airbnb-floor-plans-the-best-apps-for-the-job/", - "/disney-themed-airbnb-for-your-little-superheros/", - "/disney-themed-airbnb-in-florida-a-magical-stay-for-disney-fans/", - "/disney-themed-airbnb-in-kissimmee-florida-with-millennium-falcon-bed/", - "/disney-themed-airbnb-in-kissimmee-where-dreams-come-true/", - "/dont-give-up-the-daydream-no-23-2/", - "/dont-give-up-the-daydream-no-23/", - "/dont-give-up-the-daydream-no-24/", - "/dont-give-up-the-daydream-no-25/", - "/dubai-united-arab-emirates-5-nights-save-196/", - "/dubai-united-arab-emirates-6-nights-save-235/", - "/dublin-ireland-8-nights-save-161/", - "/estes-park-cabins-with-hot-tubs/", + "/passport-to-inspiration-a-deep-dive-into-the-best-travel-blogs-on-the-web/", "/facebook-groups-for-airbnb-hosts-the-ultimate-list/", - "/firenze-toscana-3-nights-save-428/", - "/firenze-toscana-7-nights-save-1037/", - "/firenze-toscana-8-nights-save-410/", - "/fuengirola-spain-7-nights-save-121/", - "/gold-medal-accommodations-your-ultimate-guide-to-where-to-stay-during-the-paris-olympics-2024/", + "/9-of-the-most-expensive-airbnb-getaways-in-europe/", + "/the-best-airbnb-wedding-venues-in-the-us/", + "/the-most-expensive-airbnbs-in-the-caribbean/", + "/kissimmee-florida-7-nights-save-537/", "/grosenbrode-schleswig-holstein-6-nights-save-120/", + "/roma-citta-metropolitana-di-roma-6-nights-save-120/", + "/reykjavik-iceland-6-nights-save-248/", + "/zarbo-di-mare-sicilia-7-nights-save-259/", + "/wilen-sarnen-obwalden-7-nights-save-324/", + "/mailhoc-tarn-6-nights-save-991/", + "/vogar-iceland-6-nights-save-172/", + "/valtournenche-valle-daosta-8-nights-save-155/", + "/valenca-do-douro-viseu-3-nights-save-63/", + "/venice-veneto-italy-9-nights-save-483/", + "/9-of-the-best-gites-in-france/", + "/twickenham-england-6-nights-save-462/", + "/truckee-california-10-nights-save-7117/", + "/roma-citta-metropolitana-di-roma-6-nights-save-374/", + "/rome-lazio-3-nights-save-127/", + "/sussex-inlet-australia-5-nights-save-169/", + "/swampscott-massachusetts-4-nights-save-211/", + "/torcigliano-tuscany-7-nights-save-1294/", + "/tawonga-south-australia-5-nights-save-185/", + "/tambon-kammala-phuket-province-8-nights-save-714/", + "/taormina-sicilia-4-nights-save-268/", + "/tenerife-spain-7-nights-save-675/", + "/surfers-paradise-australia-7-nights-save-108/", + "/south-kuta-bali-9-nights-save-1612/", + "/saint-martin-wallis-14-nights-save-1483/", + "/split-central-dalmatia-5-nights-save-116/", + "/soller-balearic-islands-7-nights-save-637/", + "/sesimbra-setubal-7-nights-save-66/", + "/shute-harbour-queensland-14-nights-save-132/", + "/sainte-maxime-provence-alpes-cote-dazur-6-nights-save-450/", + "/rome-lazio-4-nights-save-318/", + "/fuengirola-spain-7-nights-save-121/", + "/roma-lazio-6-nights-save-431/", + "/firenze-toscana-3-nights-save-428/", + "/dubai-united-arab-emirates-5-nights-save-196/", + "/dubai-united-arab-emirates-6-nights-save-235/", + "/ko-samui-district-surat-thani-8-nights-save-84/", + "/kihei-hawaii-8-nights-save-182/", "/hamilton-island-australia-10-nights-save-81/", - "/hardwicke-bay-south-australia-8-nights-save-56/", - "/harry-potter-airbnb-adventure-stay-in-a-wizarding-world-wonderland/", - "/harry-potter-airbnb-dream-book-your-stay-now/", - "/harry-potter-airbnb-fantasy-where-wizardry-and-comfort-unite/", - "/harry-potter-airbnb-in-massachusetts/", - "/harry-potter-airbnb-journey-rent-harry-potters-childhood-home/", - "/harry-potter-airbnb-magic-experience-the-ultimate-wizards-retreat/", - "/harry-potter-themed-airbnbs/", "/headford-county-galway-5-nights-save-508/", - "/hocking-hills-airbnbs-with-hot-tubs/", - "/hua-hin-prachuap-khiri-khan-7-nights-save-95/", "/ionian-islands-peloponnese-6-nights-save-382/", - "/kamakura-kanagawa-japan-8-nights-save-158/", - "/kapalua-hawaii-7-nights-save-471/", - "/kathisma-beach-greece-7-nights-save-182/", "/kihei-hawaii-5-nights-save-64/", - "/kihei-hawaii-8-nights-save-182/", - "/kissimmee-florida-7-nights-save-537/", - "/kissimmee-florida-8-nights-save-583/", - "/ko-samui-district-surat-thani-8-nights-save-84/", + "/hua-hin-prachuap-khiri-khan-7-nights-save-95/", + "/lucolena-florence-7-nights-save-486/", + "/paris-ile-de-france-7-nights-save-141/", + "/london-england-7-nights-save-64/", + "/poggibonsi-toscana-7-nights-save-239/", + "/oia-aegean-5-nights-save-1611/", + "/megalochori-aegean-5-nights-save-183/", + "/phuket-thailand-8-nights-save-380/", + "/malay-western-visayas-5-nights-save-120/", + "/manarola-liguria-9-nights-save-683/", + "/london-england-9-nights-save-343/", + "/kyoto-city-kyoto-7-nights-save-493/", "/ko-samui-thailand-8-nights-save-270/", "/koh-samui-tambon-bophut-surat-thani-6-nights-save-530/", - "/kuta-bali-4-nights-save-8722/", + "/london-england-2-nights-save-129/", + "/9-of-the-best-frozen-themed-airbnbs-in-the-us-love-is-an-open-door/", + "/dont-give-up-the-daydream-no-25/", + "/port-douglas-australia-9-nights-save-1287/", + "/praiano-campania-7-nights-save-86/", + "/perledo-lecco-5-nights-save-96/", + "/where-am-i-29/", "/kuta-bali-7-nights-save-2235/", - "/kyoto-city-kyoto-7-nights-save-493/", - "/les-houches-auvergne-rhone-alpes-8-nights-save-433/", - "/linkedin-groups-for-airbnb-hosts-the-ultimate-network-for-success/", + "/9-most-expensive-airbnb-luxe-properties-in-the-us/", + "/kathisma-beach-greece-7-nights-save-182/", + "/kissimmee-florida-8-nights-save-583/", + "/travel-quotes-123/", + "/kapalua-hawaii-7-nights-save-471/", + "/dont-give-up-the-daydream-no-24/", + "/paris-ile-de-france-8-nights-save-282/", "/lombardia-italy-7-nights-save-2507/", - "/london-england-2-nights-save-129/", - "/london-england-7-nights-save-64/", - "/london-england-9-nights-save-343/", + "/kuta-bali-4-nights-save-8722/", + "/where-am-i-28/", + "/les-houches-auvergne-rhone-alpes-8-nights-save-433/", + "/montreuil-aux-lions-nord-pas-de-calais-picardie-6-nights-save-108/", + "/paris-ile-de-france-5-nights-save-366/", + "/travel-quotes-122-3/", + "/suratthani-koh-samui-9-nights-save-154/", + "/barcelona-catalonia-7-nights-save-780/", + "/dont-give-up-the-daydream-no-23-2/", + "/barcelona-catalunya-8-nights-save-176/", + "/bellagio-lombardy-3-nights-save-795/", + "/where-am-i-26/", + "/harry-potter-themed-airbnbs/", + "/dont-give-up-the-daydream-no-23/", + "/medak-croatia-7-nights-save-335/", + "/north-side-grand-cayman-7-nights-save-792/", "/lucolena-florence-7-nights-save-423/", - "/lucolena-florence-7-nights-save-486/", - "/madrid-community-of-madrid-5-nights-save-1218/", "/madrid-community-of-madrid-8-nights-save-358/", - "/mailhoc-tarn-6-nights-save-991/", - "/makawao-hawaii-5-nights-save-54/", - "/malay-western-visayas-5-nights-save-120/", - "/manarola-liguria-9-nights-save-683/", + "/kamakura-kanagawa-japan-8-nights-save-158/", + "/firenze-toscana-8-nights-save-410/", + "/dublin-ireland-8-nights-save-161/", + "/madrid-community-of-madrid-5-nights-save-1218/", "/marrakesh-morocco-7-nights-save-367/", - "/medak-croatia-7-nights-save-335/", - "/megalochori-aegean-5-nights-save-183/", - "/miami-airbnb-with-pool-dive-into-your-dream-vacation/", + "/makawao-hawaii-5-nights-save-54/", "/monterotondo-marittimo-toscana-10-nights-save-78/", - "/montreuil-aux-lions-nord-pas-de-calais-picardie-6-nights-save-108/", - "/muggle-to-magician-stay-in-a-harry-potter-themed-airbnb/", - "/north-side-grand-cayman-7-nights-save-792/", "/occitanie-france-7-nights-save-68/", - "/oia-aegean-5-nights-save-1611/", - "/oklahoma-airbnbs-with-hot-tubs/", - "/orlando-fl-airbnb-your-ticket-to-paradise-in-the-heart-of-orlando/", - "/paris-ile-de-france-5-nights-save-366/", - "/paris-ile-de-france-7-nights-save-141/", - "/paris-ile-de-france-8-nights-save-282/", - "/passport-to-inspiration-a-deep-dive-into-the-best-travel-blogs-on-the-web/", - "/perledo-lecco-5-nights-save-96/", - "/pet-friendly-airbnb-a-furry-friendly-find-for-your-next-adventure/", - "/pet-friendly-airbnb-in-montana-with-paw-some-perks/", - "/pet-friendly-airbnb-in-north-carolina-where-tails-wag-and-hearts-warm/", - "/pet-friendly-airbnbs/", - "/phuket-thailand-8-nights-save-380/", - "/poggibonsi-toscana-7-nights-save-239/", - "/port-douglas-australia-9-nights-save-1287/", - "/praiano-campania-7-nights-save-86/", - "/price-check-this-orlando-airbnb-with-pool-and-save-money/", - "/property-spotlight-a-cool-tiny-house-with-big-savings/", - "/property-spotlight-amazing-glamping-rental/", - "/property-spotlight-unveiling-the-ultimate-castle-rental-experience/", - "/reykjavik-iceland-6-nights-save-248/", - "/roma-citta-metropolitana-di-roma-6-nights-save-120/", - "/roma-citta-metropolitana-di-roma-6-nights-save-374/", - "/roma-lazio-6-nights-save-431/", - "/romantic-airbnb-new-mexico-glamping-at-el-mistico-ranch/", - "/romantic-airbnbs/", - "/romantic-getaways/", - "/rome-lazio-3-nights-save-127/", - "/rome-lazio-4-nights-save-318/", - "/saint-martin-wallis-14-nights-save-1483/", - "/sainte-maxime-provence-alpes-cote-dazur-6-nights-save-450/", - "/sesimbra-setubal-7-nights-save-66/", - "/shute-harbour-queensland-14-nights-save-132/", - "/soller-balearic-islands-7-nights-save-637/", - "/south-kuta-bali-9-nights-save-1612/", - "/split-central-dalmatia-5-nights-save-116/", - "/suratthani-koh-samui-9-nights-save-154/", - "/surfers-paradise-australia-7-nights-save-108/", - "/sussex-inlet-australia-5-nights-save-169/", - "/swampscott-massachusetts-4-nights-save-211/", - "/tambon-kammala-phuket-province-8-nights-save-714/", - "/tampa-airbnb-with-hot-tub-treat-yourself-to-a-romantic-break/", - "/taormina-sicilia-4-nights-save-268/", - "/tawonga-south-australia-5-nights-save-185/", - "/tenerife-spain-7-nights-save-675/", - "/the-best-airbnb-wedding-venues-in-the-us/", - "/the-best-travel-accessories-for-2024-a-must-have-guide-for-globetrotters/", - "/the-most-expensive-airbnbs-in-the-caribbean/", - "/the-redditors-guide-to-airbnb-hosting-best-communities-for-advice-and-support/", - "/the-spellbinding-stay-experience-a-harry-potter-airbnb-adventure/", - "/tie-the-knot-at-this-fancy-airbnb-wedding-venue-in-florida/", - "/top-eerie-escapes-to-stay-for-halloween/", - "/torcigliano-tuscany-7-nights-save-1294/", - "/travel-quotes-122-3/", - "/travel-quotes-123/", - "/truckee-california-10-nights-save-7117/", - "/twickenham-england-6-nights-save-462/", - "/unique-airbnb-stays/", - "/unique-airbnb-wedding-venue-to-tie-the-knot/", - "/valenca-do-douro-viseu-3-nights-save-63/", - "/valtournenche-valle-daosta-8-nights-save-155/", - "/venice-veneto-italy-9-nights-save-483/", - "/vogar-iceland-6-nights-save-172/", - "/waterfront-airbnb-tampa-fl-family-friendly-find-with-a-view/", - "/where-am-i-26/", - "/where-am-i-28/", - "/where-am-i-29/", - "/wilen-sarnen-obwalden-7-nights-save-324/", - "/zarbo-di-mare-sicilia-7-nights-save-259/" + "/firenze-toscana-7-nights-save-1037/", + "/hardwicke-bay-south-australia-8-nights-save-56/", + "/cahuita-limon-province-8-nights-save-117/", + "/catalunya-spain-8-nights-save-412/", + "/cul-de-sac-saint-martin-france-7-nights-save-617/", + "/conwy-wales-8-nights-save-607/", + "/booroobin-queensland-7-nights-save-385/", + "/bacalar-quintana-roo-6-nights-save-159-2/", + "/abcoude-amsterdam-8-nights-save-252/", + "/dont-give-up-the-daydream-no-20/", + "/glenbrook-nevada-6-nights-save-105/", + "/yufu-oita-japan-5-nights-save-144/", + "/kappeln-schleswig-holstein-8-nights-save-105/", + "/where-am-i-27/", + "/venice-veneto-7-nights-save-460/", + "/seminyak-bali-8-nights-save-194/", + "/skala-greece-7-nights-save-104/", + "/travel-quotes-122-2/", + "/roma-lazio-5-nights-save-60/", + "/dont-give-up-the-daydream-no-21/", + "/mount-martha-victoria-10-nights-save-50/", + "/lipari-sicilia-5-nights-save-256/", + "/where-am-i-25-2/", + "/ann-maria-florida-7-nights-save-254/", + "/ashford-washington-8-nights-save-84/", + "/bacalar-quintana-roo-6-nights-save-159/", + "/travel-quotes-122/", + "/cuzco-peru-5-nights-save-389/", + "/reggello-toscana-12-nights-save-178/", + "/paris-ile-de-france-4-nights-save-378/", + "/marina-del-cantone-campania-11-nights-save-117/", + "/where-am-i-25/", + "/state-of-santa-catarina-brazil-5-nights-save-1007/", + "/sant-josep-de-sa-talaia-pm-7-nights-save-474/", + "/roma-lazio-4-nights-save-712/", + "/travel-quotes-121/", + "/trapiche-malaga-provinz-5-nights-save-733/", + "/dont-give-up-the-daydream-no-19/", + "/selfoss-iceland-6-nights-save-1242/", + "/firenze-toscana-5-nights-save-140/", + "/kemenuh-bali-6-nights-save-543/", + "/choeng-mon-plai-laem-koh-samui-9-nights-save-55/", + "/chiba-japan-7-nights-save-1110/", + "/newquay-cornwall-england-3-nights-save-332/", + "/travel-quotes-119/", + "/fredericksburg-texas-7-nights-save-73/", + "/dont-give-up-the-daydream-no-18/", + "/quarteira-algarve-7-nights-save-787/", + "/mendocino-california-8-nights-save-137/", + "/edinburgh-scotland-6-nights-save-93/", + "/where-am-i-23-2/", + "/madeira-portugal-7-nights-save-1725/", + "/gregory-town-the-bahamas-4-nights-save-87/", + "/firenze-toscana-8-nights-save-1010/", + "/canggu-bali-10-nights-save-118/", + "/bellemare-plage-flacq-6-nights-save-65/", + "/dont-give-up-the-daydream-no-17/", + "/hernando-beach-florida-7-nights-save-270/", + "/garden-city-utah-9-nights-save-1151/", + "/florence-italy-5-nights-save-301/", + "/edinburgh-scotland-2-nights-save-140/", + "/celukan-bawang-bali-8-nights-save-898/", + "/travel-quotes-118-2/", + "/callao-salvaje-canarias-spain-7-nights-save-872/", + "/dont-give-up-the-daydream-no-16/", + "/firenze-toscana-7-nights-save-773/", + "/firenze-toscana-3-nights-save-67/", + "/etna-pennsylvania-10-nights-save-98/", + "/can-besso-jesus-balearic-islands-7-nights-save-1630/", + "/bifrost-west-iceland-6-nights-save-369/", + "/bellagio-lombardia-5-nights-save-335/", + "/travel-quotes-118/", + "/athens-attica-5-nights-save-144/", + "/dont-give-up-the-daydream-no-9/", + "/oia-santorini-7-nights-save-116/", + "/amsterdam-north-holland-5-nights-save-371/", + "/paris-departement-de-paris-6-nights-save-59/", + "/where-am-i-24-2/", + "/where-am-i-24/", + "/positano-campania-6-nights-save-146/", + "/roma-lazio-7-nights-save-178/", + "/travel-quotes-117-2/", + "/sorrento-campania-6-nights-save-69/", + "/jewett-new-york-10-nights-save-101/", + "/dont-give-up-the-daydream-no-14/", + "/lahaina-hawaii-5-nights-save-951/", + "/airbnb-beach-house-adventure-seaside-dreams-await/", + "/9-of-the-coolest-tiny-house-rentals-you-can-find-in-the-us-and-where-to-rent-them-for-less/", + "/lauterbrunnen-bern-9-nights-save-77/", + "/9-best-farmhouse-airbnbs-to-rent-in-the-us-for-less/", + "/where-am-i-23/", + "/airbnb-treehouse-in-montana-ski-hike-and-relax/", + "/logan-ohio-8-nights-save-684/", + "/merovigli-aegean-5-nights-save-1300/", + "/disney-themed-airbnb-in-reunion-for-the-princess-in-your-life/", + "/praiano-campania-7-nights-save-433/", + "/travel-quotes-117/", + "/miramar-beach-florida-6-nights-save-347/", + "/new-orleans-louisiana-8-nights-save-765/", + "/dont-give-up-the-daydream-no-13/", + "/panama-city-beach-florida-8-nights-save-350/", + "/park-city-utah-8-nights-save-856/", + "/9-best-chocolate-box-cottages-you-can-rent-in-the-uk/", + "/savannah-georgia-7-nights-save-4691/", + "/9-most-unique-rentals-you-can-find-in-the-us/", + "/sea-ranch-california-6-nights-save-112/", + "/9-of-the-best-glamping-rentals-you-can-find-in-the-us/", + "/sedona-arizona-7-nights-save-141/", + "/sevierville-tennessee-7-nights-save-138/", + "/the-sea-ranch-california-7-nights-save-171/", + "/thompsonville-michigan-5-nights-save-122/", + "/williams-arizona-8-nights-save-214/", + "/ventura-california-8-nights-save-2216/", + "/waikoloa-hawaii-7-nights-save-182/", + "/kihei-hawaii-6-nights-save-426/", + "/glendale-arizona-9-nights-save-1469/", + "/telluride-colorado-6-nights-save-285/", + "/stowe-vermont-5-nights-save-139/", + "/steinsland-hordaland-7-nights-save-682/", + "/st-leonards-on-sea-england-7-nights-save-5035/", + "/sedona-arizona-11-nights-save-102/", + "/pasadena-california-7-nights-save-85/", + "/the-ultimate-list-of-vacation-rental-events-that-you-should-definitely-go-to-2023/", + "/lahaina-hawaii-8-nights-save-137/", + "/happy-new-year/", + "/orange-beach-alabama-7-nights-save-194/", + "/lahaina-hawaii-6-nights-save-745/", + "/north-port-florida-7-nights-save-256/", + "/naples-new-york-10-nights-save-77/", + "/napili-hawaii-6-nights-save-219/", + "/lahaina-hawaii-5-nights-save-728/", + "/travel-quotes-116/", + "/kihei-hawaii-10-nights-save-2120/", + "/keystone-colorado-7-nights-save-1670/", + "/key-west-florida-5-nights-save-336/", + "/merry-christmas/", + "/where-am-i-22/", + "/kailua-kona-hawaii-9-nights-save-88/", + "/glen-ellen-california-10-nights-save-117/", + "/travel-quotes-115/", + "/galveston-texas-7-nights-save-196/", + "/gatlinburg-tennessee-5-nights-save-194/", + "/franklin-tennessee-7-nights-save-142/", + "/where-am-i-21/", + "/fort-walton-beach-florida-7-nights-save-352/", + "/fernandina-beach-florida-10-nights-save-1092/", + "/dillon-colorado-4-nights-save-372/", + "/travel-quotes-114/", + "/blowing-rock-north-carolina-6-nights-save-263/", + "/franklin-tennessee-14-nights-save-219/", + "/blowing-rock-north-carolina-8-nights-save-315/", + "/breckenridge-colorado-8-nights-save-1198/", + "/where-am-i-20/", + "/destin-florida-7-nights-save-1281/", + "/columbus-indiana-8-nights-save-286/", + "/clermont-florida-7-nights-save-278/", + "/travel-quotes-113/", + "/coconut-grove-miami-7-nights-save-593/", + "/broken-bow-oklahoma-5-nights-save-108/", + "/charlottesville-virginia-5-nights-save-281/", + "/avon-colorado-8-nights-save-2143/", + "/where-am-i-19/", + "/albrightsville-pennsylvania-8-nights-save-673/", + "/carolina-beach-north-carolina-8-nights-save-330/", + "/travel-quotes-112/", + "/bald-head-island-north-carolina-7-nights-save-523/", + "/st-andrews-scotland-8-nights-save-1433/", + "/schmitten-salzburg-6-nights-save-365/", + "/where-am-i-18/", + "/san-diego-california-7-nights-save-372/", + "/nisaki-greece-10-nights-save-73/", + "/mckinleyville-california-6-nights-save-259/", + "/travel-quotes-111/", + "/lauterbrunnen-bern-7-nights-save-202/", + "/lagos-portugal-6-nights-save-2849/", + "/koloa-hawaii-14-nights-save-73/", + "/daphne-alabama-6-nights-save-91/", + "/where-am-i-17/", + "/broken-bow-oklahoma-7-nights-save-113/", + "/avon-colorado-7-nights-save-193/", + "/amalfi-campania-7-nights-save-887/", + "/travel-quotes-110/", + "/monmouth-wales-5-nights-save-111/", + "/kendal-cumbria-14-nights-save-69/", + "/gwynedd-cymru-8-nights-save-238/", + "/where-am-i-16/", + "/dollar-clackmannanshire-7-nights-save-135/", + "/atrani-campania-6-nights-save-164/", + "/aberfoyle-scotland-5-nights-save-445/", + "/treminis-auvergne-rhone-alpes-8-nights-save-97/", + "/valais-switzerland-7-nights-save-185/", + "/west-yellowstone-montana-6-nights-save-138/", + "/where-am-i-15/", + "/mountain-village-colorado-7-nights-save-998/", + "/dubrovnik-neretva-county-croatia-7-nights-save-135/", + "/cooke-city-silver-gate-montana-8-nights-save-237/", + "/columbia-falls-montana-9-nights-save-2062/", + "/occitanie-france-7-nights-save-218/", + "/dont-give-up-the-daydream-no-12/", + "/panama-city-beach-florida-7-nights-save-100/", + "/south-tyrol-italy-7-nights-save-340/", + "/where-am-i-14/", + "/selat-bali-7-nights-save-236/", + "/kyle-texas-6-nights-save-3600/", + "/caylus-france-9-nights-save-135/", + "/nordland-norway-7-nights-save-329/", + "/dont-give-up-the-daydream-no-11/", + "/where-am-i-13/", + "/sausalito-california-7-nights-save-2087/", + "/valais-switzerland-7-nights-save-13637/", + "/paris-7-nights-save-2097/", + "/dont-give-up-the-daydream-no-10/", + "/palm-springs-7-nights-save-1051/", + "/where-am-i-12/", + "/hot-springs-arkansas-10-nights-save-67/", + "/miami-5-nights-save-416/", + "/hilton-head-island-south-carolina-6-nights-save-985/", + "/clermont-florida-3-nights-save-654/", + "/bethany-beach-delaware-7-nights-save-940/", + "/noyers-sur-cher-6-nights-save-132/", + "/nashville-7-nights-save-292/", + "/los-angeles-6-nights-save-423/", + "/cape-town-6-nights-save-522/", + "/amsterdam-9-nights-save-514/", + "/hawaii-7-nights-save-500/", + "/where-am-i-10/", + "/tennessee-5-nights-save-377/", + "/miami-10-nights-save-801/", + "/georgia-5-nights-save-602/", + "/costa-rica-7-nights-save-984/", + "/california-7-nights-save-143/", + "/texas-6-nights-save-101/", + "/queenstown-4-nights-save-800/", + "/where-am-i-9/", + "/asturias-9-nights-save-111/", + "/faro-7-nights-save-3652/", + "/fernhill-5-nights-save-72/", + "/new-south-wales-5-nights-save-1997/", + "/breckenridge-6-nights-save-105/", + "/albion-5-nights-save-257/", + "/nayarit-7-nights-save-86/", + "/chicago-6-nights-save-1207/", + "/lahaina-5-nights-save-295/", + "/saint-augustine-10-nights-save-173/", + "/miami-6-nights-save-399/", + "/baie-saint-paul-10-nights-save-103/", + "/valle-real-6-nights-save-745/", + "/sicilia-7-nights-save-524/", + "/alicante-6-nights-save-173/", + "/barvaux-sur-ourthe-6-nights-save-86/", + "/clackmannanshire-5-nights-save-222/", + "/abcoude-5-nights-save-520/", + "/sicilia-6-nights-save-127/", + "/ljubljana-10-nights-save-159/", + "/ionian-islands-6-nights-save-225/", + "/madrid-7-nights-save-203/", + "/baden-wurttemberg-10-nights-save-180/", + "/sopley-8-nights-save-209/", + "/granada-10-nights-save-115/", + "/province-of-brindisi-7-nights-save-2435/", + "/sardegna-6-nights-save-110/", + "/paleokastritsa-7-nights-save-284/", + "/noizay-9-nights-save-101/", + "/arezzo-5-nights-save-145/", + "/corfu-7-nights-save-596/", + "/darzkowo-6-nights-save-113/", + "/fayston-4-nights-save-147/", + "/marina-6-nights-save-3138/", + "/kostanje-11-nights-save-1184/", + "/rome-7-nights-save-233/", + "/east-hampton-6-nights-save-135/", + "/yallingup-7-nights-save-181/", + "/elevated-plains-8-nights-save-105/", + "/roma-8-nights-save-117/", + "/dauphin-island-9-nights-save-88/", + "/bayard-10-nights-save-299/", + "/tennessee-6-nights-save-90/", + "/the-ultimate-list-of-vacation-rental-events-that-you-should-definitely-go-to/", + "/new-zealand-6-nights-save-189/", + "/croatia-7-nights-save-174/", + "/colorado-10-nights-save-518/", + "/california-7-nights-save-3327/", + "/looking-for-an-airbnb-beach-house-these-are-jaw-dropping/", + "/dont-give-up-the-daydream-no-8/", + "/mexico-7-nights-save-293/", + "/tennessee-7-nights-save-439/", + "/where-am-i-8/", + "/california-5-nights-save-59/", + "/tennessee-5-nights-save-87/", + "/valencian-community-5-nights-save-118/", + "/michigan-7-nights-save-4131/", + "/utah-10-nights-save-117/", + "/9-magical-disney-themed-rentals-you-can-find-in-the-us-and-where-to-rent-them-for-less/", + "/dont-give-up-the-daydream-no-7/", + "/tennessee-7-nights-save-107/", + "/mexico-6-nights-save-222/", + "/where-am-i-7/", + "/little-cayman-7-nights-save-343/", + "/malibu-7-nights-save-868/", + "/tennessee-10-nights-save-73/", + "/michigan-7-nights-save-230/", + "/california-7-nights-save-1915-2/", + "/dont-give-up-the-daydream-no-6/", + "/florida-7-nights-save-276/", + "/michigan-7-nights-save-267/", + "/tennessee-5-nights-save-170/", + "/where-am-i-6/", + "/joshua-tree-5-nights-save-130/", + "/dont-give-up-the-daydream-no-5/", + "/where-am-i-5/", + "/where-am-i-4/", + "/airbnb-castles-9-budget-friendly-fairytales-in-the-u-s/" + ], + "alan": [ + "/hot-tub-photography-for-airbnb-hosts/", + "/build-a-backyard-pickleball-court-for-more-bookings/", + "/the-10-best-things-to-do-in-miami-a-video-guide/", + "/airbnb-bookings-hack-get-20-more-bookings-starting-today/", + "/today-is-book-direct-day-but-does-it-really-work/", + "/airbnb-alternatives-hipcamp-guide-discovering-the-best-hidden-gems/", + "/airbnb-alternatives-airbnb-v-booking-com-decoding-the-best-deals/", + "/airbnb-alternatives-airbnb-v-vrbo-a-guide-for-savvy-travelers/", + "/pet-friendly-airbnb-guide/", + "/airbnbs-latest-update-sparks-outrage-could-this-be-the-end-for-many-hosts/", + "/airbnb-hosts-is-it-time-to-review-your-views-on-reviews/", + "/vrbo-and-airbnb-youll-never-guess-what-vrbo-are-doing/", + "/were-all-in-the-airbnb-business-now-part-2/", + "/why-youll-be-750-times-better-off-running-an-airbnb-than-a-vacation-rental-or-holiday-let/", + "/vacation-rental-website-builders-what-are-your-best-options/", + "/airbnb-alternatives-2024/", + "/airbnb-gift-cards-all-you-need-to-know-about-these-perfect-gifts/", + "/smart-home-devices-for-your-airbnb-a-complete-guide/", + "/smart-home-monitoring-for-your-airbnb-a-complete-guide-pt-2/", + "/best-airbnb-smart-home-devices-part-3-moneymaking-wifi/", + "/want-to-really-get-away-heres-some-expert-advice-for-digital-nomad-accommodations/", + "/the-pros-and-cons-of-booking-direct-vacation-rental-secrets/", + "/vacation-rentals-cancellation-policies-compared-all-you-need-to-know/", + "/vacation-rental-damage-deposits-all-you-need-to-know-including-tips-from-hosts/", + "/the-ultimate-guide-to-selecting-the-perfect-travel-insurance-for-your-2023-adventure/", + "/how-to-furnish-your-airbnb-like-a-pro-and-a-few-shopping-secrets/", + "/20-free-tools-that-make-your-hospitality-website-rock/", + "/holiday-let-emails-messages-to-send-to-guests/", + "/free-short-term-rental-agreement-templates-word-pdf/", + "/free-airbnb-welcome-book-template/", + "/airbnb-rental-arbitrage-and-how-to-succeed-at-it/", + "/the-short-term-rental-hosts-guide-on-how-to-survive-the-recession/", + "/getting-more-direct-bookings-the-value-of-repeat-guests/", + "/airbnb-ceo-vows-to-tackle-runaway-cleaning-fees/", + "/travel-quotes-109/", + "/travel-quotes-108/", + "/the-coming-collapse-of-airbnb-short-term-rentals/", + "/travel-quotes-107/", + "/travel-quotes-106/", + "/amazing-disney-themed-vacation-rental-in-kissimmee-and-how-to-book-it-for-less/", + "/travel-hack-airbnb-5-times-the-price-i-save-4100-in-1-minute/", + "/check-out-this-wonderful-treehouse-and-book-it-for-less/", + "/travel-quotes-105/", + "/does-social-media-drive-direct-bookings-for-your-vacation-rental/", + "/5-steps-to-clean-up-and-organize-your-vacation-rental-website/", + "/labor-day-weekend-2022-vacation-rental-market-data-forecast/", + "/lowest-price-vacation-rentals-magic-search-without-witchcraft/", + "/7-short-term-rental-myths-debunked/", + "/vacation-rentals-book-direct-with-the-owner-and-save-heres-how/", + "/travel-quotes-104/", + "/vacation-rentals-best-price-and-100-peace-of-mind-every-time/", + "/dont-get-scammed-on-airbnb-find-out-the-ugly-truth-and-save-big-money/", + "/dont-give-up-the-daydream-no-4/", + "/airbnbs-10-fold-increase-in-host-cancellation-fees-could-benefit-guests/", + "/fair-use/", + "/the-meaning-behind-hichee/", + "/where-am-i-3/", + "/where-am-i-2/", + "/where-am-i-1/", + "/dont-give-up-the-daydream-no-3/", + "/travel-quotes-103/", + "/vacation-rental-price-comparison-watch-as-i-save-5836-on-this-airbnb-in-under-1-minute/", + "/pack-up-and-go-with-these-15-travel-hacks-and-more-diy-ideas-by-crafty-panda/", + "/travel-quotes-102/", + "/dont-give-up-the-daydream-no-2/", + "/dont-give-up-the-daydream-no-1/", + "/travel-quotes-101/", + "/amazon-travel-essentials-you-need-in-2022-my-top-10-travel-products/", + "/top-10-travel-tips-tricks-from-a-flight-attendant/", + "/best-cheap-flights-websites-nobody-is-talking-about-how-to-find-cheap-flights-2022/", + "/how-to-get-longer-bookings-and-monthly-bookings-on-airbnb-and-vrbo/", + "/airbnb-or-vrbo-where-should-i-list-my-property/", + "/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/", + "/gatlinburg-4-nights-save-192/", + "/new-york-city-3-nights-save-474/", + "/vacation-rental-deals-st-barts-4-guests-july-3rd-july-16th/", + "/dont-give-up-the-daydream/" + ], + "our-discount-desk": [ + "/north-carolina-4-nights-save-41/", + "/indiana-5-nights-save-661/", + "/catalonia-6-nights-save-121/", + "/saint-lucia-7-nights-save-484/", + "/california-7-nights-save-1915/", + "/florida-5-nights-save-1861/", + "/quebec-3-nights-save-217/", + "/arizona-4-nights-save-108/", + "/arizona-7-nights-save-295/", + "/costa-rica-7-nights-save-110/", + "/cancun-7-nights-save-3927/", + "/35-secrets-to-save-money-on-your-next-vacation/" + ], + "our-travel-reporter": [ + "/top-9-website-builders-property-managers-use-for-their-vacation-rental/", + "/top-7-best-vacation-rental-channel-managers-in-2022/", + "/airbnb-cabins-get-close-to-nature-in-these-hidden-gems/", + "/see-how-to-save-over-100-a-night-on-this-wonderful-treehouse-in-georgia/", + "/property-spotlight-stunning-beach-house-and-how-to-book-it-for-less/", + "/property-spotlight-a-delightful-a-frame-cabin-best-price-revealed/", + "/airbnb-treehouses/" ] } diff --git a/src/lib/authorArchives.ts b/src/lib/authorArchives.ts new file mode 100644 index 0000000..cb3d6b6 --- /dev/null +++ b/src/lib/authorArchives.ts @@ -0,0 +1,121 @@ +import { getCollection, type CollectionEntry } from 'astro:content'; +import authorPostPaths from '../data/author-post-paths.json'; + +export const AUTHOR_PAGE_SIZE = 12; +const ROUTE_ALIASES = new Map([ + ['/where-am-i-24-2/', '/where-am-i-24/'], +]); + +type AuthorProfile = { + name: string; + bio: string; +}; + +export type AuthorArchivePost = Pick, 'id' | 'data'>; + +type AuthorArchiveEntry = { + slug: string; + profile: AuthorProfile; + posts: AuthorArchivePost[]; + totalPages: number; +}; + +export const authorProfiles: Record = { + alan: { + name: 'Alan Egan', + bio: 'Alan has been working in the vacation rental sector since 2004, when he first created a listing site for his property management company. He has been helping short-term rental owners and managers to stand out in an over-saturated marketplace for over 12 years and has written thousands of articles in that time. He has written books on vacation rental photography and was the first in the industry to create online marketing courses for hosts. He has given keynote presentations across various subjects at The Vacation Rental World Summit, VRMA, VRMintel, Host, and The Book Direct Summit.' + }, + tiffany: { + name: 'Tiffany Martin', + bio: "Tiffany Martin, a 33-year-old travel content contributor based in Manila, Philippines, brings the world's beauty to your screen through her writing, narrating the best places in the globe with a charm that resonates. Aside from being a travel writer and an all-around digital nomad, she's also a wifey, a mom to two girls, and a licensed educator. During her downtime, she loves traveling, cooking, and playing with her energetic Dachshund and cuddly Golden Malinois." + }, + 'our-discount-desk': { + name: 'Our Discount Desk', + bio: 'Posts and travel deals curated by the HiChee team.' + }, + 'our-travel-reporter': { + name: 'Our Travel Reporter', + bio: 'Travel reporting and destination coverage from the HiChee team.' + } +}; + +export async function getAuthorArchiveEntries(): Promise { + const posts = (await getCollection('posts')).filter( + (entry) => !entry.data.draft && entry.data.status === 'publish' + ); + + const postsByPath = new Map( + posts.map((post) => [normalizeRoutePath(post.data.path), post] as const) + ); + + return Object.entries(authorPostPaths).map(([slug, paths]) => { + const authorPosts = (paths as string[]) + .map((entryPath) => resolveArchivePost(entryPath, postsByPath)) + .filter((post): post is AuthorArchivePost => Boolean(post)) + .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); + + return { + slug, + profile: getAuthorProfile(slug), + posts: authorPosts, + totalPages: Math.max(1, Math.ceil(authorPosts.length / AUTHOR_PAGE_SIZE)) + }; + }); +} + +export function getAuthorProfile(slug: string): AuthorProfile { + return authorProfiles[slug] ?? { + name: titleCaseSlug(slug), + bio: `Posts written by ${titleCaseSlug(slug)}.` + }; +} + +export function buildAuthorPagePath(slug: string, page: number): string { + if (page <= 1) return `/author/${slug}/`; + return `/author/${slug}/page/${page}/`; +} + +export function paginateAuthorPosts(posts: AuthorArchivePost[], page: number) { + const currentPage = Math.max(1, page); + const start = (currentPage - 1) * AUTHOR_PAGE_SIZE; + return posts.slice(start, start + AUTHOR_PAGE_SIZE); +} + +function resolveArchivePost( + route: string, + postsByPath: Map> +): AuthorArchivePost | null { + const normalizedRoute = normalizeRoutePath(route); + const direct = postsByPath.get(normalizedRoute); + if (direct) return direct; + + const canonicalRoute = ROUTE_ALIASES.get(normalizedRoute); + if (!canonicalRoute) return null; + + const canonicalPost = postsByPath.get(canonicalRoute); + if (!canonicalPost) return null; + + return { + ...canonicalPost, + id: `${canonicalPost.id}::${normalizedRoute}`, + data: { + ...canonicalPost.data, + path: normalizedRoute, + }, + }; +} + +function normalizeRoutePath(inputPath: string) { + const raw = String(inputPath || '').replace(/%ef%bf%bc/gi, '').replace(/\uFFFC/g, ''); + if (!raw) return '/'; + const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`; + return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; +} + +function titleCaseSlug(slug: string): string { + return slug + .split(/[-_]/g) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 41a3f28..660b3dd 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -7,6 +7,15 @@ import legacyRelatedPostsByRouteRaw from '../data/legacy-related-posts.json'; import authorPostPaths from '../data/author-post-paths.json'; import { normalizeWpUploadUrl, rewriteWpUploadsInHtml } from '../lib/media'; +function canonicalizeAliasPath(route: string): string { + switch (route) { + case '/where-am-i-24-2/': + return '/where-am-i-24/'; + default: + return route; + } +} + export async function getStaticPaths() { const allEntries = [ ...(await getCollection('posts')).filter((entry) => !entry.data.draft && entry.data.status === 'publish'), @@ -565,7 +574,8 @@ function normalizeRoutePath(inputPath: string): string { const raw = String(inputPath || '').replace(/%ef%bf%bc/gi, '').replace(/\uFFFC/g, ''); if (!raw) return '/'; const withStart = raw.startsWith('/') ? raw : `/${raw}`; - return withStart.endsWith('/') ? withStart : `${withStart}/`; + const normalized = withStart.endsWith('/') ? withStart : `${withStart}/`; + return canonicalizeAliasPath(normalized); } function pickRelatedPosts< diff --git a/src/pages/author/[slug].astro b/src/pages/author/[slug].astro index d3d88b0..b5c6308 100644 --- a/src/pages/author/[slug].astro +++ b/src/pages/author/[slug].astro @@ -1,90 +1,68 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import PostCard from '../../components/PostCard.astro'; -import authorPostPaths from '../../data/author-post-paths.json'; - -type AuthorProfile = { - name: string; - bio: string; -}; - -function titleCaseSlug(slug: string): string { - return slug - .split(/[-_]/g) - .filter(Boolean) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); -} - -const authorProfiles: Record = { - alan: { - name: 'Alan Egan', - bio: 'Alan has been working in the vacation rental sector since 2004, when he first created a listing site for his property management company. He has been helping short-term rental owners and managers to stand out in an over-saturated marketplace for over 12 years and has written thousands of articles in that time. He has written books on vacation rental photography and was the first in the industry to create online marketing courses for hosts. He has given keynote presentations across various subjects at The Vacation Rental World Summit, VRMA, VRMintel, Host, and The Book Direct Summit.' - }, - tiffany: { - name: 'Tiffany Martin', - bio: "Tiffany Martin, a 33-year-old travel content contributor based in Manila, Philippines, brings the world's beauty to your screen through her writing, narrating the best places in the globe with a charm that resonates. Aside from being a travel writer and an all-around digital nomad, she's also a wifey, a mom to two girls, and a licensed educator. During her downtime, she loves traveling, cooking, and playing with her energetic Dachshund and cuddly Golden Malinois." - }, - 'our-discount-desk': { - name: 'Our Discount Desk', - bio: 'Posts and travel deals curated by the HiChee team.' - }, - 'our-travel-reporter': { - name: 'Our Travel Reporter', - bio: 'Travel reporting and destination coverage from the HiChee team.' - } -}; +import PaginationNav from '../../components/PaginationNav.astro'; +import { + AUTHOR_PAGE_SIZE, + buildAuthorPagePath, + getAuthorArchiveEntries, +} from '../../lib/authorArchives'; export async function getStaticPaths() { - const posts = (await getCollection('posts')).filter( - (entry) => !entry.data.draft && entry.data.status === 'publish' - ); - const normalizePath = (inputPath: string) => { - const raw = String(inputPath || '').replace(/%ef%bf%bc/gi, '').replace(/\uFFFC/g, ''); - if (!raw) return '/'; - const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`; - return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; - }; - const postsByPath = new Map(posts.map((post) => [normalizePath(post.data.path), post] as const)); - - return Object.entries(authorPostPaths).map(([slug, paths]) => { - const authorPosts = (paths as string[]) - .map((path) => postsByPath.get(normalizePath(path))) - .filter((post): post is (typeof posts)[number] => Boolean(post)) - .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); - + const archives = await getAuthorArchiveEntries(); + return archives.map(({ slug, profile, posts, totalPages }) => { return { params: { slug }, - props: { posts: authorPosts } + props: { + slug, + profile, + posts: posts.slice(0, AUTHOR_PAGE_SIZE), + totalPosts: posts.length, + currentPage: 1, + totalPages + } }; }); } -const slug = Astro.params.slug as string; -const posts = [...((Astro.props.posts ?? []) as any[])].sort( - (a, b) => b.data.date.getTime() - a.data.date.getTime() -); - -const profile = authorProfiles[slug] ?? { - name: titleCaseSlug(slug), - bio: `Posts written by ${titleCaseSlug(slug)}.` +const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.props as { + slug: string; + profile: { name: string; bio: string }; + posts: any[]; + totalPosts: number; + currentPage: number; + totalPages: number; }; + +const title = + currentPage > 1 + ? `${profile.name}, Author at The HiChee Blog and Travel Guide - Page ${currentPage}` + : `${profile.name}, Author at The HiChee Blog and Travel Guide`; + +const description = + currentPage > 1 + ? `Author archive for ${profile.name}, page ${currentPage}. ${profile.bio}` + : `Author archive for ${profile.name}. ${profile.bio}`; ---

Author

-

Archives for {profile.name}

+

Author: {profile.name}

{profile.bio}

-

{posts.length} posts

+

+ {totalPosts} posts + {totalPages > 1 && ` • Page ${currentPage} of ${totalPages}`} +

{posts.map((post) => )}
+ +
diff --git a/src/pages/author/[slug]/page/[page].astro b/src/pages/author/[slug]/page/[page].astro new file mode 100644 index 0000000..ef2295c --- /dev/null +++ b/src/pages/author/[slug]/page/[page].astro @@ -0,0 +1,63 @@ +--- +import BaseLayout from '../../../../layouts/BaseLayout.astro'; +import PaginationNav from '../../../../components/PaginationNav.astro'; +import PostCard from '../../../../components/PostCard.astro'; +import { + buildAuthorPagePath, + getAuthorArchiveEntries, + paginateAuthorPosts, +} from '../../../../lib/authorArchives'; + +export async function getStaticPaths() { + const archives = await getAuthorArchiveEntries(); + const paths = []; + + for (const { slug, profile, posts, totalPages } of archives) { + for (let page = 2; page <= totalPages; page += 1) { + paths.push({ + params: { slug, page: String(page) }, + props: { + slug, + profile, + posts: paginateAuthorPosts(posts, page), + totalPosts: posts.length, + currentPage: page, + totalPages + } + }); + } + } + + return paths; +} + +const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.props as { + slug: string; + profile: { name: string; bio: string }; + posts: any[]; + totalPosts: number; + currentPage: number; + totalPages: number; +}; +--- + + +
+

Author

+

Author: {profile.name}

+

{profile.bio}

+

+ {totalPosts} posts • Page {currentPage} of {totalPages} +

+
+ +
+ {posts.map((post) => )} +
+ + +
diff --git a/src/styles/global.css b/src/styles/global.css index 5f06034..129f577 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -201,6 +201,46 @@ a:hover { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } +.pagination-nav { + margin: 1.5rem 0 0; +} + +.pagination-nav__list { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.page-numbers { + min-width: 2.4rem; + padding: 0.6rem 0.95rem; + border: 1px solid var(--line); + border-radius: 999px; + background: #fff; + color: var(--text); + font-weight: 600; + text-align: center; +} + +a.page-numbers:hover { + border-color: var(--link); +} + +.page-numbers.current { + border-color: var(--link); + background: var(--link); + color: #fff; +} + +.page-numbers--ellipsis { + border: 0; + background: transparent; + color: var(--muted); + min-width: auto; + padding-inline: 0.2rem; +} + .post-card { border: 1px solid var(--line); border-radius: 8px; From 509a6b49ef11d1a0b332caf839d58f3e6346e31b Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 23 Apr 2026 17:28:05 -1000 Subject: [PATCH 2/6] Match author archive titles and home redirect --- data/redirects.csv | 1 + data/redirects.extra.csv | 1 + public/_redirects | 1 + src/pages/author/[slug]/page/[page].astro | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/data/redirects.csv b/data/redirects.csv index 084145e..e39f54f 100644 --- a/data/redirects.csv +++ b/data/redirects.csv @@ -9,5 +9,6 @@ from,to /looking-for-a-pet-friendly-airbnb-heres-your-ultimate-guide/,/pet-friendly-airbnb-guide/ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/,/pet-friendly-airbnbs/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%ef%bf%bc/,/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ +/the-hichee-blog/,/ /hichee-blog-for-hosts/page/2,/hichee-blog-for-hosts/ /hichee-blog-for-hosts/page/2/,/hichee-blog-for-hosts/ diff --git a/data/redirects.extra.csv b/data/redirects.extra.csv index ff61df2..92b7522 100644 --- a/data/redirects.extra.csv +++ b/data/redirects.extra.csv @@ -6,6 +6,7 @@ from,to /harry-potter-themed-airbnbs-our-top-9-for-a-magical-stay/,/harry-potter-themed-airbnbs/ /looking-for-a-pet-friendly-airbnb-heres-your-ultimate-guide/,/pet-friendly-airbnb-guide/ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/,/pet-friendly-airbnbs/ +/the-hichee-blog/,/ /career/,https://hichee.com/careers /hosts/,https://hichee.com/hosts /listing-verification/,https://hichee.com/listing-verification diff --git a/public/_redirects b/public/_redirects index c6bfec2..6734844 100644 --- a/public/_redirects +++ b/public/_redirects @@ -13,6 +13,7 @@ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/ /pet-friendly-airbnbs/ 301 /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%*/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ 301 /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%ef%bf%bc/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ 301 +/the-hichee-blog/ / 301 /career https://hichee.com/careers 301 /career/ https://hichee.com/careers 301 /hosts https://hichee.com/hosts 301 diff --git a/src/pages/author/[slug]/page/[page].astro b/src/pages/author/[slug]/page/[page].astro index ef2295c..ac05495 100644 --- a/src/pages/author/[slug]/page/[page].astro +++ b/src/pages/author/[slug]/page/[page].astro @@ -42,7 +42,7 @@ const { slug, profile, posts, totalPosts, currentPage, totalPages } = Astro.prop --- From 7879fd2fcd20e5cf12362729545e08f63bc7acb6 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 23 Apr 2026 17:31:59 -1000 Subject: [PATCH 3/6] Add legacy blog alias redirects --- data/redirects.csv | 5 +++++ data/redirects.extra.csv | 5 +++++ public/_redirects | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/data/redirects.csv b/data/redirects.csv index e39f54f..50d6cb0 100644 --- a/data/redirects.csv +++ b/data/redirects.csv @@ -9,6 +9,11 @@ from,to /looking-for-a-pet-friendly-airbnb-heres-your-ultimate-guide/,/pet-friendly-airbnb-guide/ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/,/pet-friendly-airbnbs/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%ef%bf%bc/,/youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ +/at-ease-rentals-with-anthony-gantt-underrepresented-hosts-series/,/at-ease-rentals-with-anthony-gantt-under-represented-hosts-series/ +/blog-post-guest-review-scores-touchstay/,/how-to-use-communication-to-elevate-your-guest-review-scores/ +/short-term-rental-market-analysis/,/mastering-the-short-term-rental-game-a-comprehensive-market-analysis-guide/ +/collecting-a-vacation-rental-security-deposit/,/the-importance-of-collecting-a-vacation-rental-security-deposit/ +/blog-posts-airbnb-winter-release-2022/,/your-guide-to-airbnbs-2022-winter-release/ /the-hichee-blog/,/ /hichee-blog-for-hosts/page/2,/hichee-blog-for-hosts/ /hichee-blog-for-hosts/page/2/,/hichee-blog-for-hosts/ diff --git a/data/redirects.extra.csv b/data/redirects.extra.csv index 92b7522..72bae1e 100644 --- a/data/redirects.extra.csv +++ b/data/redirects.extra.csv @@ -6,6 +6,11 @@ from,to /harry-potter-themed-airbnbs-our-top-9-for-a-magical-stay/,/harry-potter-themed-airbnbs/ /looking-for-a-pet-friendly-airbnb-heres-your-ultimate-guide/,/pet-friendly-airbnb-guide/ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/,/pet-friendly-airbnbs/ +/at-ease-rentals-with-anthony-gantt-underrepresented-hosts-series/,/at-ease-rentals-with-anthony-gantt-under-represented-hosts-series/ +/blog-post-guest-review-scores-touchstay/,/how-to-use-communication-to-elevate-your-guest-review-scores/ +/short-term-rental-market-analysis/,/mastering-the-short-term-rental-game-a-comprehensive-market-analysis-guide/ +/collecting-a-vacation-rental-security-deposit/,/the-importance-of-collecting-a-vacation-rental-security-deposit/ +/blog-posts-airbnb-winter-release-2022/,/your-guide-to-airbnbs-2022-winter-release/ /the-hichee-blog/,/ /career/,https://hichee.com/careers /hosts/,https://hichee.com/hosts diff --git a/public/_redirects b/public/_redirects index 6734844..57a353e 100644 --- a/public/_redirects +++ b/public/_redirects @@ -13,6 +13,11 @@ /pet-friendly-airbnbs-sniff-out-the-best-spots-for-a-pawsome-vacation/ /pet-friendly-airbnbs/ 301 /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%*/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ 301 /youll-lose-thousands-hosting-on-airbnb-if-you-do-this%ef%bf%bc/ /youll-lose-thousands-hosting-on-airbnb-if-you-do-this/ 301 +/at-ease-rentals-with-anthony-gantt-underrepresented-hosts-series/ /at-ease-rentals-with-anthony-gantt-under-represented-hosts-series/ 301 +/blog-post-guest-review-scores-touchstay/ /how-to-use-communication-to-elevate-your-guest-review-scores/ 301 +/short-term-rental-market-analysis/ /mastering-the-short-term-rental-game-a-comprehensive-market-analysis-guide/ 301 +/collecting-a-vacation-rental-security-deposit/ /the-importance-of-collecting-a-vacation-rental-security-deposit/ 301 +/blog-posts-airbnb-winter-release-2022/ /your-guide-to-airbnbs-2022-winter-release/ 301 /the-hichee-blog/ / 301 /career https://hichee.com/careers 301 /career/ https://hichee.com/careers 301 From ee733686f431bd7bcc2d386715389b725cc3ca1e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 23 Apr 2026 21:27:00 -1000 Subject: [PATCH 4/6] Harden author path WXR tag parsing --- scripts/build-author-post-paths.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/build-author-post-paths.mjs b/scripts/build-author-post-paths.mjs index 0c09946..3225da2 100644 --- a/scripts/build-author-post-paths.mjs +++ b/scripts/build-author-post-paths.mjs @@ -107,6 +107,8 @@ async function buildAuthorPathMap(wxrDir, localPosts) { const resolved = preferred ?? fallback ?? aliasResolved; if (!resolved) continue; + // Preserve the exported alias path when the WXR item only resolves via ROUTE_ALIASES + // so author archive pagination can match WordPress while reusing the canonical local post. const recordedRoute = preferred || fallback || !aliasResolved ? resolved.route : sourceRoute; @@ -140,7 +142,8 @@ function *iterateItems(xml) { function extractCdata(source, tagName) { const match = source.match(new RegExp(`<${escapeRegExp(tagName)}><\\/${escapeRegExp(tagName)}>`, 'i')); - return match ? match[1].trim() : ''; + if (match) return match[1].trim(); + return extractTag(source, tagName); } function extractTag(source, tagName) { From 4380cc0b2dc5253b1027e2e135c7928e12b2a92b Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 23 Apr 2026 21:29:53 -1000 Subject: [PATCH 5/6] Centralize route aliases for author archives --- scripts/build-author-post-paths.mjs | 6 +-- src/components/PaginationNav.astro | 21 +++++++-- src/data/route-aliases.json | 6 +++ src/lib/authorArchives.ts | 29 +++++------- src/lib/routePaths.ts | 32 ++++++++++++++ src/pages/[...slug].astro | 54 ++--------------------- src/pages/author/[slug].astro | 10 ++++- src/pages/author/[slug]/page/[page].astro | 10 ++++- 8 files changed, 89 insertions(+), 79 deletions(-) create mode 100644 src/data/route-aliases.json create mode 100644 src/lib/routePaths.ts diff --git a/scripts/build-author-post-paths.mjs b/scripts/build-author-post-paths.mjs index 3225da2..bf8c38b 100644 --- a/scripts/build-author-post-paths.mjs +++ b/scripts/build-author-post-paths.mjs @@ -8,6 +8,8 @@ const args = parseArgs(process.argv.slice(2)); const outPath = path.resolve(args.out ?? 'src/data/author-post-paths.json'); const postsDir = path.resolve(args.postsDir ?? 'src/content/posts'); const wxrDir = path.resolve(args.wxrDir ?? 'data/raw'); +const routeAliasesPath = new URL('../src/data/route-aliases.json', import.meta.url); +const routeAliases = JSON.parse(await fs.readFile(routeAliasesPath, 'utf8')); const AUTHOR_ALIASES = { Tiffany: 'tiffany', @@ -16,9 +18,7 @@ const AUTHOR_ALIASES = { 'Our Travel Reporter': 'our-travel-reporter', }; -const ROUTE_ALIASES = new Map([ - ['/where-am-i-24-2/', '/where-am-i-24/'], -]); +const ROUTE_ALIASES = new Map(Object.entries(routeAliases)); const localPosts = await loadLocalPosts(postsDir); const authorPathMap = await buildAuthorPathMap(wxrDir, localPosts); diff --git a/src/components/PaginationNav.astro b/src/components/PaginationNav.astro index 5c38f47..997de23 100644 --- a/src/components/PaginationNav.astro +++ b/src/components/PaginationNav.astro @@ -3,12 +3,15 @@ interface Props { basePath: string; currentPage: number; totalPages: number; + ariaLabel?: string; } -const { basePath, currentPage, totalPages } = Astro.props; +const { basePath, currentPage, totalPages, ariaLabel = 'Pagination' } = Astro.props; const items = totalPages > 1 ? buildItems(currentPage, totalPages) : []; const previousHref = totalPages > 1 && currentPage > 1 ? pageHref(basePath, currentPage - 1) : null; const nextHref = totalPages > 1 && currentPage < totalPages ? pageHref(basePath, currentPage + 1) : null; +const previousPage = currentPage - 1; +const nextPage = currentPage + 1; function buildItems(current: number, total: number) { const pages = new Set([1, total, current - 1, current, current + 1]); @@ -45,11 +48,16 @@ function pageHref(rootPath: string, page: number) { { totalPages > 1 && ( -