Skip to content

Commit

Permalink
Add pretty URL slugs to listing pages (#233)
Browse files Browse the repository at this point in the history
* Added url helper to listing service to make url slugs from listing
* Minor version bump on typescript-eslint plugin to get fixes from typescript-eslint/typescript-eslint#1736
* Added routing with url slug for listings + a redirect from the id path
  • Loading branch information
bencpeters committed May 6, 2020
1 parent 9e06447 commit 6d4a204
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 25 deletions.
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

0 comments on commit 6d4a204

Please sign in to comment.