fix(SW-2933): Making the hotels/city listing render correct for SEO purposes
Approved-by: Matilda Landström
This commit is contained in:
@@ -15,7 +15,9 @@ export default function CityListingSkeleton() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className={styles.cityList}>
|
<ul className={styles.cityList}>
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<CityListingItemSkeleton key={index} />
|
<li key={index}>
|
||||||
|
<CityListingItemSkeleton />
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||||
|
|
||||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
|
|
||||||
import CityListingItem from "./CityListingItem"
|
import CityListingItem from "./CityListingItem"
|
||||||
@@ -35,15 +36,17 @@ export default function CityListing() {
|
|||||||
) : (
|
) : (
|
||||||
<section className={styles.container} ref={scrollRef}>
|
<section className={styles.container} ref={scrollRef}>
|
||||||
<div className={styles.listHeader}>
|
<div className={styles.listHeader}>
|
||||||
<Subtitle type="two">
|
<Typography variant="Title/Subtitle/md">
|
||||||
{intl.formatMessage(
|
<h2>
|
||||||
{
|
{intl.formatMessage(
|
||||||
defaultMessage:
|
{
|
||||||
"{count, plural, one {# destination} other {# destinations}}",
|
defaultMessage:
|
||||||
},
|
"{count, plural, one {# destination} other {# destinations}}",
|
||||||
{ count: activeCities.length }
|
},
|
||||||
)}
|
{ count: activeCities.length }
|
||||||
</Subtitle>
|
)}
|
||||||
|
</h2>
|
||||||
|
</Typography>
|
||||||
<DestinationFilterAndSort listType="city" />
|
<DestinationFilterAndSort listType="city" />
|
||||||
</div>
|
</div>
|
||||||
{activeCities.length === 0 ? (
|
{activeCities.length === 0 ? (
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ export default async function DestinationCityPage({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<header className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{images?.length ? (
|
{images?.length ? (
|
||||||
<TopImages images={images} destinationName={city.name} />
|
<TopImages images={images} destinationName={city.name} />
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</div>
|
||||||
<main className={styles.mainContent}>
|
<main className={styles.mainContent}>
|
||||||
<HotelListing />
|
<HotelListing />
|
||||||
{blocks && <Blocks blocks={blocks} />}
|
{blocks && <Blocks blocks={blocks} />}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default async function DestinationCountryPage({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<header className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -70,7 +70,7 @@ export default async function DestinationCountryPage({
|
|||||||
destinationName={translatedCountry}
|
destinationName={translatedCountry}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</div>
|
||||||
<main className={styles.mainContent}>
|
<main className={styles.mainContent}>
|
||||||
<CityListing />
|
<CityListing />
|
||||||
{blocks && <Blocks blocks={blocks} />}
|
{blocks && <Blocks blocks={blocks} />}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export default function HotelListingSkeleton() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className={styles.hotelList}>
|
<ul className={styles.hotelList}>
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<HotelListingItemSkeleton key={index} />
|
<li key={index}>
|
||||||
|
<HotelListingItemSkeleton />
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
|||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
|
|
||||||
import HotelListingItem from "./HotelListingItem"
|
import HotelListingItem from "./HotelListingItem"
|
||||||
@@ -48,14 +48,17 @@ export default function HotelListing() {
|
|||||||
) : (
|
) : (
|
||||||
<section className={styles.container} ref={scrollRef}>
|
<section className={styles.container} ref={scrollRef}>
|
||||||
<div className={styles.listHeader}>
|
<div className={styles.listHeader}>
|
||||||
<Subtitle type="two">
|
<Typography variant="Title/Subtitle/md">
|
||||||
{intl.formatMessage(
|
<h2>
|
||||||
{
|
{intl.formatMessage(
|
||||||
defaultMessage: "{count, plural, one {# hotel} other {# hotels}}",
|
{
|
||||||
},
|
defaultMessage:
|
||||||
{ count: activeHotels.length }
|
"{count, plural, one {# hotel} other {# hotels}}",
|
||||||
)}
|
},
|
||||||
</Subtitle>
|
{ count: activeHotels.length }
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</Typography>
|
||||||
<div className={styles.cta}>
|
<div className={styles.cta}>
|
||||||
{mapUrl && (
|
{mapUrl && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
|
|
||||||
import { createDestinationDataStore } from "@/stores/destination-data"
|
import { createDestinationDataStore } from "@/stores/destination-data"
|
||||||
@@ -19,6 +19,7 @@ export default function DestinationDataProvider({
|
|||||||
}: DestinationDataProviderProps) {
|
}: DestinationDataProviderProps) {
|
||||||
const storeRef = useRef<DestinationDataStore>(undefined)
|
const storeRef = useRef<DestinationDataStore>(undefined)
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
storeRef.current = createDestinationDataStore({
|
storeRef.current = createDestinationDataStore({
|
||||||
@@ -26,6 +27,7 @@ export default function DestinationDataProvider({
|
|||||||
allHotels,
|
allHotels,
|
||||||
pathname,
|
pathname,
|
||||||
sortItems,
|
sortItems,
|
||||||
|
searchParams,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,38 @@ export function createDestinationDataStore({
|
|||||||
allHotels,
|
allHotels,
|
||||||
pathname,
|
pathname,
|
||||||
sortItems,
|
sortItems,
|
||||||
|
searchParams,
|
||||||
}: InitialState) {
|
}: InitialState) {
|
||||||
const defaultSort =
|
const defaultSort =
|
||||||
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
||||||
|
|
||||||
const allFilters = getFiltersFromHotels(allHotels)
|
const allFilters = getFiltersFromHotels(allHotels)
|
||||||
const allFilterSlugs = Object.values(allFilters).flatMap((filter: Filter[]) =>
|
const allFilterSlugs = Object.values(allFilters).flatMap((filter: Filter[]) =>
|
||||||
filter.map((f) => f.slug)
|
filter.map((f) => f.slug)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const activeFilters: string[] = []
|
||||||
|
const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
|
||||||
|
pathname,
|
||||||
|
allFilterSlugs
|
||||||
|
)
|
||||||
|
if (basePathnameWithoutFilters !== pathname) {
|
||||||
|
const pathParts = pathname.split("/")
|
||||||
|
const lastPathPart = pathParts[pathParts.length - 1]
|
||||||
|
activeFilters.push(lastPathPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeSort = defaultSort
|
||||||
|
if (searchParams) {
|
||||||
|
const sortParam = searchParams.get("sort")
|
||||||
|
if (sortParam && isValidSortOption(sortParam, sortItems)) {
|
||||||
|
activeSort = sortParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filteredHotels = getFilteredHotels(allHotels, activeFilters)
|
||||||
|
const activeHotels = getSortedHotels(filteredHotels, activeSort)
|
||||||
|
const filteredCities = getFilteredCities(filteredHotels, allCities)
|
||||||
|
const activeCities = getSortedCities(filteredCities, activeSort)
|
||||||
|
|
||||||
return create<DestinationDataState>((set) => ({
|
return create<DestinationDataState>((set) => ({
|
||||||
actions: {
|
actions: {
|
||||||
updateActiveFiltersAndSort(filters, sort) {
|
updateActiveFiltersAndSort(filters, sort) {
|
||||||
@@ -144,24 +167,21 @@ export function createDestinationDataStore({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
allHotels,
|
allHotels,
|
||||||
activeHotels: allHotels,
|
activeHotels: activeHotels,
|
||||||
pendingHotelCount: allHotels.length,
|
pendingHotelCount: activeHotels.length,
|
||||||
allCities,
|
allCities,
|
||||||
activeCities: allCities,
|
activeCities: activeCities,
|
||||||
pendingCityCount: allCities.length,
|
pendingCityCount: activeCities.length,
|
||||||
activeSort: defaultSort,
|
activeSort,
|
||||||
pendingSort: defaultSort,
|
pendingSort: activeSort,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
activeFilters: [],
|
activeFilters,
|
||||||
pendingFilters: [],
|
pendingFilters: activeFilters,
|
||||||
allFilters,
|
allFilters,
|
||||||
allFilterSlugs,
|
allFilterSlugs,
|
||||||
basePathnameWithoutFilters: getBasePathNameWithoutFilters(
|
basePathnameWithoutFilters,
|
||||||
pathname,
|
|
||||||
allFilterSlugs
|
|
||||||
),
|
|
||||||
sortItems,
|
sortItems,
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ReadonlyURLSearchParams } from "next/navigation"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CategorizedFilters,
|
CategorizedFilters,
|
||||||
SortItem,
|
SortItem,
|
||||||
@@ -44,4 +46,5 @@ export interface DestinationDataState {
|
|||||||
export interface InitialState
|
export interface InitialState
|
||||||
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> {
|
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> {
|
||||||
pathname: string
|
pathname: string
|
||||||
|
searchParams: ReadonlyURLSearchParams
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user