Skip to content

Commit

Permalink
feat(react): update SEO-friendly routing example (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Jun 27, 2019
1 parent 440110e commit 95661db
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 171 deletions.
2 changes: 1 addition & 1 deletion React InstantSearch/routing-seo-friendly/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
40 changes: 22 additions & 18 deletions React InstantSearch/routing-seo-friendly/public/index.html
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />

<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png">

<!--
<!--
Do not use @7 in production, use a complete version like x.x.x, see website for latest version:
https://community.algolia.com/react-instantsearch/Getting_started.html#load-the-algolia-theme
-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/algolia-min.css">

<title>routing-basic</title>
</head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/algolia-min.css"
/>

<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<title>SEO-friendly routing</title>
</head>

<div id="root"></div>
</body>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>

<div id="root"></div>
</body>
</html>
4 changes: 2 additions & 2 deletions React InstantSearch/routing-seo-friendly/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"short_name": "routing-basic",
"name": "routing-basic Sample",
"short_name": "SEO routing",
"name": "SEO-friendly routing",
"icons": [
{
"src": "favicon.png",
Expand Down
4 changes: 4 additions & 0 deletions React InstantSearch/routing-seo-friendly/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ em {
flex: 1;
}

.search-panel__filters > div {
margin-bottom: 2rem;
}

.search-panel__results {
flex: 3;
}
Expand Down
210 changes: 132 additions & 78 deletions React InstantSearch/routing-seo-friendly/src/App.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 }) => (
<div>
<Highlight attribute="name" hit={hit} />
</div>
);

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 (
<div className="container">
<InstantSearch
searchClient={searchClient}
indexName="instant_search"
searchState={this.state.searchState}
onSearchStateChange={this.onSearchStateChange}
createURL={createURL}
>
<div className="search-panel">
<div className="search-panel__filters">
return (
<div className="container">
<InstantSearch
searchClient={searchClient}
indexName="instant_search"
searchState={searchState}
onSearchStateChange={onSearchStateChange}
createURL={createURL}
>
<div className="search-panel">
<div className="search-panel__filters">
<ClearRefinements />

<Panel header="Category">
<Menu attribute="categories" />
</Panel>

<Panel header="Brands">
<RefinementList attribute="brand" />
</div>
</Panel>
</div>

<div className="search-panel__results">
<SearchBox className="searchbox" placeholder="Search" />
<Hits hitComponent={Hit} />
<div className="search-panel__results">
<SearchBox className="searchbox" placeholder="Search" />

<div className="pagination">
<Pagination />
</div>
<Hits hitComponent={Hit} />

<div className="pagination">
<Pagination />
</div>
</div>
</InstantSearch>
</div>
);
}
}

function Hit(props) {
return (
<div>
<Highlight attribute="name" hit={props.hit} />
</div>
</InstantSearch>
</div>
);
}

Hit.propTypes = {
hit: PropTypes.object.isRequired,
};

App.propTypes = {
Expand Down
Loading

0 comments on commit 95661db

Please sign in to comment.