Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pretty URL slugs to listing pages #233

Merged
merged 3 commits into from May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/public-reference/cypress/integration/navigation.ts
Expand Up @@ -13,11 +13,26 @@ describe("Navigating around the site", function() {
cy.contains("Rent affordable housing")
})

it("Loads a listing page directly", function() {
it("Loads a listing page directly by id", function() {
cy.visit("http://localhost:3000/listing/Uvbk5qurpB2WI9V6WnNdH")

// Check that the listing page sidebar apply section text is present on the page
cy.contains("Get a Paper Application")

// Check that the URL got re-written with a URL slug
cy.location("pathname").should(
"eq",
"/listing/Uvbk5qurpB2WI9V6WnNdH/archer_studios_98_archer_street_san_jose_ca"
)
})

it("Loads a listing page directly with a full url", function() {
cy.visit(
"http://localhost:3000/listing/Uvbk5qurpB2WI9V6WnNdH/archer_studios_98_archer_street_san_jose_ca"
)

// Check that the listing page sidebar apply section text is present on the page
cy.contains("Get a Paper Application")
})

it("Loads a non-listing-related page directly", function() {
Expand Down
7 changes: 6 additions & 1 deletion apps/public-reference/next.config.js
Expand Up @@ -59,9 +59,14 @@ module.exports = withCSS(
const listingPaths = listings.reduce(
(listingPaths, listing) =>
Object.assign({}, listingPaths, {
[`/listing/${listing.id}`]: {
[`/listing/${listing.id}/${listing.urlSlug}`]: {
page: "/listing",
query: { id: listing.id }
},
// Create a redirect so that the base ID redirects to the ID with URL slug
[`/listing/${listing.id}`]: {
page: "/redirect",
query: { to: `/listing/${listing.id}/${listing.urlSlug}` }
}
}),
{}
Expand Down
17 changes: 17 additions & 0 deletions apps/public-reference/pages/redirect.tsx
@@ -0,0 +1,17 @@
import React from "react"
import { NextPage } from "next"
import Head from "next/head"

interface RedirectProps {
to: string
}

const Redirect: NextPage<RedirectProps> = ({ to }) => (
<Head>
<meta http-equiv="refresh" content={`0; url=${to}`} />
</Head>
)

Redirect.getInitialProps = ({ query: { to } }) => Promise.resolve({ to: to as string })

export default Redirect
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -23,8 +23,8 @@
"devDependencies": {
"@storybook/react": "^5.3.14",
"@types/jest": "^25.1.4",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"@typescript-eslint/parser": "^2.29.0",
"concurrently": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
Expand Down
28 changes: 28 additions & 0 deletions services/listings/__tests__/lib/url_helper.test.ts
@@ -0,0 +1,28 @@
import { formatUrlSlug, listingUrlSlug } from "../../src/lib/url_helper"
import { Listing } from "@bloom-housing/core"
import triton from "../../listings/triton.json"

describe("formatUrlSlug", () => {
test("reformats strings properly", () => {
expect(formatUrlSlug("snake_case")).toEqual("snake_case")
expect(formatUrlSlug("SnakeCase")).toEqual("snake_case")
expect(formatUrlSlug("Mix of spaces_and-hyphens")).toEqual("mix_of_spaces_and_hyphens")
expect(formatUrlSlug("Lots@of&weird spaces&^&!@^*&AND OTHER_CHARS")).toEqual(
"lots_of_weird_spaces_and_other_chars"
)
})

test("with an empty string", () => {
expect(formatUrlSlug("")).toEqual("")
})
})

describe("listingUrlSlug", () => {
// Force cast to listing - should we add a dependency to `listingsLoader` instead?
const listing = (triton as unknown) as Listing

test("Generates a URL slug for a Listing", () => {
const slug = listingUrlSlug(listing)
expect(slug).toEqual("the_triton_55_triton_park_lane_foster_city_ca")
})
})
2 changes: 2 additions & 0 deletions services/listings/src/index.ts
Expand Up @@ -5,6 +5,7 @@ import jp from "jsonpath"
import { Listing } from "@bloom-housing/core"
import listingsLoader from "./lib/listings_loader"
import { transformUnits } from "./lib/unit_transformations"
import { listingUrlSlug } from "./lib/url_helper"
import { amiCharts } from "./lib/ami_charts"

dotenv.config({ path: ".env" })
Expand All @@ -29,6 +30,7 @@ app.use(async ctx => {
// Transform all the listings
listings.forEach(listing => {
listing.unitsSummarized = transformUnits(listing.units, amiCharts)
listing.urlSlug = listingUrlSlug(listing)
})

const data = {
Expand Down
30 changes: 30 additions & 0 deletions services/listings/src/lib/url_helper.ts
@@ -0,0 +1,30 @@
/**
* Formats the input string as a URL slug.
* This includes the following transformations:
* - All lowercase
* - Remove special characters
* - snake_case
* @param input
*/
import { Listing } from "@bloom-housing/core"

export const formatUrlSlug = (input: string): string => {
return (
(
(input || "")
// Divide into words based on upper case letters followed by lower case letters
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]+|[0-9]+/g) || []
)

.join("_")
.toLowerCase()
)
}

export const listingUrlSlug = (listing: Listing): string => {
const {
name,
buildingAddress: { city, street, state }
} = listing
return formatUrlSlug([name, street, city, state].join(" "))
}
1 change: 1 addition & 0 deletions shared/core/src/listings.ts
Expand Up @@ -58,6 +58,7 @@ export interface Listing {
unitsAvailable: number
unitAmenities: string
unitsSummarized?: UnitsSummarized
urlSlug?: string
waitlistCurrentSize: number
waitlistMaxSize: number
yearBuilt: number
Expand Down
Expand Up @@ -56,7 +56,10 @@ const ListingsList = (props: ListingsProps) => {
/>
)}
</div>
<LinkButton href={`listing/id=${listing.id}`} as={`/listing/${listing.id}`}>
<LinkButton
href={`listing/id=${listing.id}`}
as={`/listing/${listing.id}/${listing.urlSlug}`}
>
{t("label.seeDetails")}
</LinkButton>
</div>
Expand Down
40 changes: 20 additions & 20 deletions yarn.lock
Expand Up @@ -3131,40 +3131,40 @@
dependencies:
"@types/yargs-parser" "*"

"@typescript-eslint/eslint-plugin@^2.25.0":
version "2.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.25.0.tgz#0b60917332f20dcff54d0eb9be2a9e9f4c9fbd02"
integrity sha512-W2YyMtjmlrOjtXc+FtTelVs9OhuR6OlYc4XKIslJ8PUJOqgYYAPRJhAqkYRQo3G4sjvG8jSodsNycEn4W2gHUw==
"@typescript-eslint/eslint-plugin@^2.29.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.30.0.tgz#312a37e80542a764d96e8ad88a105316cdcd7b05"
integrity sha512-PGejii0qIZ9Q40RB2jIHyUpRWs1GJuHP1pkoCiaeicfwO9z7Fx03NQzupuyzAmv+q9/gFNHu7lo1ByMXe8PNyg==
dependencies:
"@typescript-eslint/experimental-utils" "2.25.0"
"@typescript-eslint/experimental-utils" "2.30.0"
functional-red-black-tree "^1.0.1"
regexpp "^3.0.0"
tsutils "^3.17.1"

"@typescript-eslint/experimental-utils@2.25.0":
version "2.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz#13691c4fe368bd377b1e5b1e4ad660b220bf7714"
integrity sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==
"@typescript-eslint/experimental-utils@2.30.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.30.0.tgz#9845e868c01f3aed66472c561d4b6bac44809dd0"
integrity sha512-L3/tS9t+hAHksy8xuorhOzhdefN0ERPDWmR9CclsIGOUqGKy6tqc/P+SoXeJRye5gazkuPO0cK9MQRnolykzkA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/typescript-estree" "2.25.0"
"@typescript-eslint/typescript-estree" "2.30.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"

"@typescript-eslint/parser@^2.25.0":
version "2.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.25.0.tgz#abfb3d999084824d9a756d9b9c0f36fba03adb76"
integrity sha512-mccBLaBSpNVgp191CP5W+8U1crTyXsRziWliCqzj02kpxdjKMvFHGJbK33NroquH3zB/gZ8H511HEsJBa2fNEg==
"@typescript-eslint/parser@^2.29.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.30.0.tgz#7681c305a6f4341ae2579f5e3a75846c29eee9ce"
integrity sha512-9kDOxzp0K85UnpmPJqUzdWaCNorYYgk1yZmf4IKzpeTlSAclnFsrLjfwD9mQExctLoLoGAUXq1co+fbr+3HeFw==
dependencies:
"@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "2.25.0"
"@typescript-eslint/typescript-estree" "2.25.0"
"@typescript-eslint/experimental-utils" "2.30.0"
"@typescript-eslint/typescript-estree" "2.30.0"
eslint-visitor-keys "^1.1.0"

"@typescript-eslint/typescript-estree@2.25.0":
version "2.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz#b790497556734b7476fa7dd3fa539955a5c79e2c"
integrity sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==
"@typescript-eslint/typescript-estree@2.30.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.30.0.tgz#1b8e848b55144270255ffbfe4c63291f8f766615"
integrity sha512-nI5WOechrA0qAhnr+DzqwmqHsx7Ulr/+0H7bWCcClDhhWkSyZR5BmTvnBEyONwJCTWHfc5PAQExX24VD26IAVw==
dependencies:
debug "^4.1.1"
eslint-visitor-keys "^1.1.0"
Expand Down