diff --git a/React InstantSearch/routing-seo-friendly/package.json b/React InstantSearch/routing-seo-friendly/package.json index 42f3fb7a21..a409d58450 100644 --- a/React InstantSearch/routing-seo-friendly/package.json +++ b/React InstantSearch/routing-seo-friendly/package.json @@ -14,7 +14,7 @@ "react": "16.8.6", "react-dom": "16.8.6", "react-instantsearch-dom": "5.7.0", - "react-router-dom": "5.0.0", + "react-router-dom": "5.0.1", "react-scripts": "1.1.5" }, "devDependencies": { diff --git a/React InstantSearch/routing-seo-friendly/public/index.html b/React InstantSearch/routing-seo-friendly/public/index.html index 2ca7611215..6848e6e788 100644 --- a/React InstantSearch/routing-seo-friendly/public/index.html +++ b/React InstantSearch/routing-seo-friendly/public/index.html @@ -1,29 +1,33 @@ + + + + - - - - + + - - - - - - - routing-basic - + - - + SEO-friendly routing + -
- + + +
+ diff --git a/React InstantSearch/routing-seo-friendly/public/manifest.json b/React InstantSearch/routing-seo-friendly/public/manifest.json index cfaf188094..8fdf145ef7 100644 --- a/React InstantSearch/routing-seo-friendly/public/manifest.json +++ b/React InstantSearch/routing-seo-friendly/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "routing-basic", - "name": "routing-basic Sample", + "short_name": "SEO routing", + "name": "SEO-friendly routing", "icons": [ { "src": "favicon.png", diff --git a/React InstantSearch/routing-seo-friendly/src/App.css b/React InstantSearch/routing-seo-friendly/src/App.css index 925ccd85ca..b0cdf69624 100644 --- a/React InstantSearch/routing-seo-friendly/src/App.css +++ b/React InstantSearch/routing-seo-friendly/src/App.css @@ -46,6 +46,10 @@ em { flex: 1; } +.search-panel__filters > div { + margin-bottom: 2rem; +} + .search-panel__results { flex: 3; } diff --git a/React InstantSearch/routing-seo-friendly/src/App.js b/React InstantSearch/routing-seo-friendly/src/App.js index e4cd96d283..4334ad94ae 100644 --- a/React InstantSearch/routing-seo-friendly/src/App.js +++ b/React InstantSearch/routing-seo-friendly/src/App.js @@ -1,11 +1,14 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { InstantSearch, Hits, SearchBox, + Menu, RefinementList, Pagination, Highlight, + Panel, + ClearRefinements, } from 'react-instantsearch-dom'; import algoliasearch from 'algoliasearch'; import qs from 'qs'; @@ -18,118 +21,169 @@ const searchClient = algoliasearch( '6be0576ff61c053d5f9a3225e2a90f76' ); -const createURL = state => { - const brands = - state.refinementList && - state.refinementList.brand && - state.refinementList.brand.join('~'); +const encodedCategories = { + Cameras: 'Cameras & Camcorders', + Cars: 'Car Electronics & GPS', + Phones: 'Cell Phones', + TV: 'TV & Home Theater', +}; + +const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => { + const newKey = encodedCategories[key]; + const newValue = key; + + return { + ...acc, + [newKey]: newValue, + }; +}, {}); + +// Returns a slug from the category name. +// Spaces are replaced by "+" to make +// the URL easier to read and other +// characters are encoded. +function getCategorySlug(name) { + const encodedName = decodedCategories[name] || name; + + return encodedName + .split(' ') + .map(encodeURIComponent) + .join('+'); +} + +// Returns a name from the category slug. +// The "+" are replaced by spaces and other +// characters are decoded. +function getCategoryName(slug) { + const decodedSlug = encodedCategories[slug] || slug; + return decodedSlug + .split('+') + .map(decodeURIComponent) + .join(' '); +} + +const createURL = state => { const isDefaultRoute = !state.query && state.page === 1 && - state.refinementList.brand && - state.refinementList.brand.length === 0; + (state.refinementList && state.refinementList.brand.length === 0) && + (state.menu && !state.menu.categories); if (isDefaultRoute) { return ''; } - const queryParams = qs.stringify({ - q: state.query, - p: state.page, + const categoryPath = state.menu.categories + ? `${getCategorySlug(state.menu.categories)}/` + : ''; + const queryParameters = {}; + + if (state.query) { + queryParameters.query = encodeURIComponent(state.query); + } + if (state.page !== 1) { + queryParameters.page = state.page; + } + if (state.refinementList.brand) { + queryParameters.brands = state.refinementList.brand.map(encodeURIComponent); + } + + const queryString = qs.stringify(queryParameters, { + addQueryPrefix: true, + arrayFormat: 'repeat', }); - return `/search${brands ? `/brands/${brands}` : ''}?${queryParams}`; + return `/search/${categoryPath}${queryString}`; }; -const searchStateToUrl = (props, searchState) => +const searchStateToUrl = searchState => searchState ? createURL(searchState) : ''; const urlToSearchState = location => { - const [, brand] = location.pathname.split('/brands/'); - const routeState = qs.parse(location.search.slice(1)); - - const searchState = { - query: routeState.q, + const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/); + const category = getCategoryName( + (pathnameMatches && pathnameMatches[1]) || '' + ); + const { query = '', page = 1, brands = [] } = qs.parse( + location.search.slice(1) + ); + // `qs` does not return an array when there's a single value. + const allBrands = Array.isArray(brands) ? brands : [brands].filter(Boolean); + + return { + query: decodeURIComponent(query), + page, + menu: { + categories: decodeURIComponent(category), + }, refinementList: { - brand: (brand && brand.split('~')) || [], + brand: allBrands.map(decodeURIComponent), }, - page: routeState.p || 1, }; - - return searchState; }; -class App extends Component { - state = { - searchState: urlToSearchState(this.props.location), - lastLocation: this.props.location, - }; +const Hit = ({ hit }) => ( +
+ +
+); - static getDerivedStateFromProps(props, state) { - if (props.location !== state.lastLocation) { - return { - searchState: urlToSearchState(props.location), - lastLocation: props.location, - }; - } +Hit.propTypes = { + hit: PropTypes.object.isRequired, +}; - return null; - } +const App = ({ location, history }) => { + const [searchState, setSearchState] = useState(urlToSearchState(location)); + const [debouncedSetState, setDebouncedSetState] = useState(null); - onSearchStateChange = searchState => { - clearTimeout(this.debouncedSetState); + const onSearchStateChange = updatedSearchState => { + clearTimeout(debouncedSetState); - this.debouncedSetState = setTimeout(() => { - this.props.history.push( - searchStateToUrl(this.props, searchState), - searchState - ); - }, DEBOUNCE_TIME); + setDebouncedSetState( + setTimeout(() => { + history.push(searchStateToUrl(updatedSearchState), updatedSearchState); + }, DEBOUNCE_TIME) + ); - this.setState({ searchState }); + setSearchState(updatedSearchState); }; - render() { - return ( -
- -
-
+ return ( +
+ +
+
+ + + + + + + -
+ +
-
- - +
+ -
- -
+ + +
+
- -
- ); - } -} - -function Hit(props) { - return ( -
- +
+
); -} - -Hit.propTypes = { - hit: PropTypes.object.isRequired, }; App.propTypes = { diff --git a/React InstantSearch/routing-seo-friendly/yarn.lock b/React InstantSearch/routing-seo-friendly/yarn.lock index a3caf863b3..85c8ba646a 100644 --- a/React InstantSearch/routing-seo-friendly/yarn.lock +++ b/React InstantSearch/routing-seo-friendly/yarn.lock @@ -9,6 +9,13 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.4.0": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" + integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== + dependencies: + regenerator-runtime "^0.13.2" + abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1719,10 +1726,6 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - core-js@^2.4.0, core-js@^2.5.0: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -1777,14 +1780,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-context@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3" - integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag== - dependencies: - fbjs "^0.8.0" - gud "^1.0.0" - cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2232,12 +2227,6 @@ encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - dependencies: - iconv-lite "~0.4.13" - enhanced-resolve@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" @@ -2882,18 +2871,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.0: - version "0.8.17" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3517,7 +3494,7 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: @@ -3879,7 +3856,7 @@ is-root@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-1.0.0.tgz#07b6c233bc394cd9d02ba15c966bd6660d6342d5" -is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3942,13 +3919,6 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -4734,6 +4704,15 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +mini-create-react-context@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189" + integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw== + dependencies: + "@babel/runtime" "^7.4.0" + gud "^1.0.0" + tiny-warning "^1.0.2" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -4861,13 +4840,6 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - node-forge@0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" @@ -5723,12 +5695,6 @@ promise@8.0.1: dependencies: asap "~2.0.3" -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - dependencies: - asap "~2.0.3" - prop-types@15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -5937,29 +5903,29 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== -react-router-dom@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.0.tgz#542a9b86af269a37f0b87218c4c25ea8dcf0c073" - integrity sha512-wSpja5g9kh5dIteZT3tUoggjnsa+TPFHSMrpHXMpFsaHhQkm/JNVGh2jiF9Dkh4+duj4MKCkwO6H08u6inZYgQ== +react-router-dom@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" + integrity sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.0.0" + react-router "5.0.1" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.0.tgz#349863f769ffc2fa10ee7331a4296e86bc12879d" - integrity sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA== +react-router@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f" + integrity sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg== dependencies: "@babel/runtime" "^7.1.2" - create-react-context "^0.2.2" history "^4.9.0" hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" + mini-create-react-context "^0.3.0" path-to-regexp "^1.7.0" prop-types "^15.6.2" react-is "^16.6.0" @@ -6137,6 +6103,11 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" @@ -6519,7 +6490,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5: +setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -7006,7 +6977,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a" integrity sha512-ytQx8T4DL8PjlX53yYzcIC0WhIZbpR0p1qcYjw2pHu3w6UtgWwFJQ/02cnhOnBBhlFx/edUIfcagCaQSe3KMWg== -tiny-warning@^1.0.0: +tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q== @@ -7111,10 +7082,6 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -ua-parser-js@^0.7.18: - version "0.7.18" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" - uglify-js@3.4.x, uglify-js@^3.0.13: version "3.4.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.5.tgz#650889c0766cf0f6fd5346cea09cd212f544be69" @@ -7462,10 +7429,6 @@ whatwg-fetch@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" -whatwg-fetch@>=0.10.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - whatwg-url@^4.3.0: version "4.8.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"