algolia / docsearch

:blue_book: The easiest way to add search to your documentation.
https://docsearch.algolia.com
MIT License
3.87k stars 374 forks source link

Using Docsearch index with instantsearch.js #457

Closed bildungsroman closed 5 years ago

bildungsroman commented 5 years ago

Do you want to request a feature or report a bug?

Feature request (or explanation of how to implement if this exists).

What is the current behavior?

I have a documentation site built using docusaurus.io, and am using Docsearch as my search in the navbar - this is working fine. However, I'd like to implement Algolia instantsearch on the landing page of the site as well. The code is working fine with an old index set up with instantsearch, but when I plug in the API key and ID of the new docsearch index into the instantsearch config function, I get a stream of JSON objects that I can't make sense of.

This is the template I'm trying to use (which worked on my old index on a jekyll site):

  const hitTemplate = function(hit) {
    console.log(hit);
  let url = hit.url;
  const title = hit.lvl1;
  const content = hit._highlightResult.matchedWords;

  return `
    <div class="post-item">
      <a class="post-link" href="${url}">
        <h4>${title}</h4>
      </a>
      <div class="search-article-description">${content}</div>
      <a href="${url}" class="read-more">Read More &raquo;</a>
    </div>
  `;

That results in a blank <div>.

This is what my results look like when no template is applied:

screen shot 2018-09-21 at 12 56 27 pm

What is the expected behavior?

Being able to display results in a coherent way, as when using a standard Algolia index.

Haroenv commented 5 years ago

A DocSearch hit looks like this:

{
  "hierarchy": {
    "lvl2": null,
    "lvl3": null,
    "lvl0": "Glossary of React Terms",
    "lvl1": "Single-page Application",
    "lvl6": null,
    "lvl4": null,
    "lvl5": null
  },
  "url": "https://reactjs.org/docs/glossary.html#single-page-application",
  "content": null,
  "anchor": "single-page-application",
  "objectID": "3940650362",
  "_highlightResult": {
    "hierarchy": {
      "lvl0": {
        "value": "Glossary of React Terms",
        "matchLevel": "none",
        "matchedWords": []
      },
      "lvl1": {
        "value": "<span class=\"algolia-docsearch-suggestion--highlight\">S</span>ingle-page Application",
        "matchLevel": "full",
        "fullyHighlighted": false,
        "matchedWords": ["s"]
      }
    },
    "hierarchy_camel": [
      {
        "lvl0": {
          "value": "Glossary of React Terms",
          "matchLevel": "none",
          "matchedWords": []
        },
        "lvl1": {
          "value": "<span class=\"algolia-docsearch-suggestion--highlight\">S</span>ingle-page Application",
          "matchLevel": "full",
          "fullyHighlighted": false,
          "matchedWords": ["s"]
        }
      }
    ]
  }
}

To go a bit more in detail about what each of these keys mean:

_highlightResult

This is an internal object, used for highlighting the text matching your query. It's finally this object that you will use to render to the page.

This object has the same shape as the main object, but with as the difference that it contains for each key the value, matchLevel and matchedWords. Value is what you use to display the match.

.hierarchy and .hierarchy_camel

(disclaimer: I'm not on the DocSearch team)

These two object contain the same information, but use hierarchy_camel first, and hierarchy as a fallback (it might be there for backwards compatibility).

This object contains the hierarchy of the documentation page's match. It is split up in each of the level so you can make hierarchical displays like the original DocSearch.js.

Other keys

These keys are used to do other things than displaying the match

"url": "https://reactjs.org/docs/glossary.html#single-page-application"

This is the url of the current match, with the target on a specific ID.

"content": null,

I'm not completely sure why this is null in the example I took and has content in your example, but this is the actual text which is matching. You'll also use the highlighted version of this (_highlightResult.content.value)

"anchor": "single-page-application"

A repeat of the anchor (ID) on the page of this match.

"objectID": "3940650362"

An internal unique ID for this record. It can be used as a key or for other uniqueness reasons.


If you have any more questions, I'm happy to find out the answer, and I'll let @s-pace fill in some extra info as needed as well.

bildungsroman commented 5 years ago

@Haroenv Thank you for the explanation, but I'm still having trouble translating that into a coherent view. For example, I just got TypeError: hit._highlightResult.content is undefined, can't access property "value" of it when trying to reference hit._highlightResult.content. It would be incredibly helpful if docsearch provided a template for displaying results in places other than the navbar.

bildungsroman commented 5 years ago

@Haroenv I found this template in the repo, I wonder if you would know how to implement it in my case (sorry for the basic questions, I'm still fairly new to JS): https://github.com/algolia/docsearch/blob/master/src/lib/templates.js

Haroenv commented 5 years ago

The templates of DocSearch use a different format than InstantSearch; but Sylvain who I pinged earlier will give some more info :)

s-pace commented 5 years ago

๐Ÿ‘‹ @bildungsroman

Thank you for reaching out.

What @Haroenv said is ๐Ÿ’ฏ ๐Ÿ™

Just a small amendment regarding the use of .hierarchy_camel, we are using it in order to avoid decomposition of camel case words on this attribute. It helps to promote record(s) that contains the full and not decomposed word from the query over records solely containing a camel case compound of it. You can find how we build these settings here.

Your index has record with content=null because of the way we build a record. We do push record at every time we had an element to the temporary index. If you want to avoid such behaviour, you can use min_indexed_level or only_content_level from your configuration file. It will be effective at the next crawling (or when we will deploy your PR)


You can find a bootstrap of an implementation of DocSearch with InstantSearch here. It will help you to use the templates.


Please note that you can place the dropdown somewhere else within you page:

You will need to integrate the following snippet:

<!-- at the end of the HEAD -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />

<!-- at the end of the BODY -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"></script>
<script type="text/javascript"> docsearch({
  apiKey: '1f37f3b055249d4873e7665523830077',
  indexName: 'stackery',
  inputSelector: '### REPLACE ME ####',
  debug: false // Set debug to true if you want to inspect the dropdown
});
</script>

Happy to give any further details or help

bildungsroman commented 5 years ago

@s-pace Thank you, I will try your jsfiddle and hopefully get it to work for me.

As for the dropdown, I would like it to remain on the navbar so that every page of my docs site has search. What I'm trying to implement is an additional instantsearch that only appears on the homepage of the docs site. From what I understand, I can't have two input selectors in docsearch, which is why I've been trying to use instantsearch like so (but of course my old instantsearch template doesn't work):

  const search = instantsearch({
    appId: '{{ site.algolia.application_id }}',
    indexName: '{{ site.algolia.index_name }}',
    apiKey: '{{ site.algolia.search_only_api_key }}'
  });

  const hitTemplate = function (hit) {
    let url = `{{ site.baseurl }}${hit.url}`;
    const title = hit._highlightResult.title.value;
    const content = hit._highlightResult.content.value;

    return `
    <div class="post-item col-12">
      <a class="post-link" href="${url}"><h4>${title}</h4></a>
      <div class="search-article-description">${content}</div>
      <a href="${url}" class="read-more">Read More ยป</a>
    </div>
  `;
  }

  // Adding searchbar and results widgets
  search.addWidget(
    instantsearch.widgets.searchBox({
      container: '#homepage-search',
      placeholder: 'Search',
      poweredBy: true
    })
  );

  search.addWidget(
    instantsearch.widgets.hits({
      container: '#search-hits',
      templates: {
        item: hitTemplate
      }
    })
  ); 
bildungsroman commented 5 years ago

@s-pace Is there no way to implement your jsfiddle example using Algolia's template, as in my example above? The problem is I'm using Docusaurus rather than a normal HTML site, which means I have an index.js page built in React, and no index.html where I could actually add the <script type="text/template" id="ais-results-template">...</script> template (this is pretty annoying on Docusarus' part, but it seams they're working to allow more customization in their V2). I've tried injecting it in the backend but keep getting SyntaxError: Unexpected token < :(

bildungsroman commented 5 years ago

i.e. something like this:

  const mainSearch = instantsearch({
    appId: '...',
    apiKey: '...',
    indexName: 'stackery',
    routing: true
  });

  const hitTemplate = function (hit) {
    return `
    <div class="ais-result">
      {{#hierarchy.lvl0}}
      <div class="ais-lvl0">
        {{{_highlightResult.hierarchy.lvl0.value}}}
      </div>
      {{/hierarchy.lvl0}}

      <div class="ais-lvl1">
        {{#hierarchy.lvl1}} {{{_highlightResult.hierarchy.lvl1.value}}} {{/hierarchy.lvl1}} {{#hierarchy.lvl2}} > {{{_highlightResult.hierarchy.lvl2.value}}} {{/hierarchy.lvl2}} {{#hierarchy.lvl3}} > {{{_highlightResult.hierarchy.lvl3.value}}} {{/hierarchy.lvl3}}
        {{#hierarchy.lvl4}} > {{{_highlightResult.hierarchy.lvl4.value}}} {{/hierarchy.lvl4}}
      </div>
      <div class="ais-content">
        {{{#content}}} {{{_highlightResult.content.value}}} {{{/content}}}
      </div>
    </div>
  `;
  }

  // initialize SearchBox
  mainSearch.addWidget(
    instantsearch.widgets.searchBox({
      container: '#search_input_main',
      placeholder: 'Search the Docs',
      autofocus: false,
      poweredBy: true
    })
  );
  // add hits cont
  mainSearch.addWidget(
    instantsearch.widgets.hits({
      container: '#search-hits',
      templates: {
        empty: 'No results',
        item: hitTemplate
        // item: $('#results-template').html()
      },
      hitsPerPage: 10
    })
  );
  // add pagination
  mainSearch.addWidget(
    instantsearch.widgets.pagination({
      container: '#pagination-container',
      maxPages: 10,
      // default is to scroll to 'body', here we disable this behavior
      scrollTo: false
    })
  );

  mainSearch.start();

(but that doesn't work as-is)

Haroenv commented 5 years ago

Can you link the repo with this so far?

bildungsroman commented 5 years ago

It's private unfortunately. But it uses Docusaurus without too much customization: https://github.com/facebook/Docusaurus/tree/master/v1/examples/basics

This is very similar to our index.js page: https://github.com/facebook/Docusaurus/blob/master/v1/examples/basics/pages/en/index.js

The main difference is I've added two React Components to our index.js, SearchInput and SearchHits:

const SearchInput = () => (
  <div>
    <div className='paddingTop productShowcaseSection' style={{textAlign: 'center'}}>
      <h3>Get the most out of Stackery's serverless toolkit</h3>
      <Container>
        <div className='search_input_div'>
          <input id="search_input_main" type="text" className="form-control" placeholder="Search the docs" aria-label="Search" aria-describedby="search"></input>
        </div>
      </Container>
    </div>
  </div>
);

const SearchHits = () => {
  return (
    <Container className='hidden' id='search-container'>
      <h3>Search results:</h3>
      <div id='search-hits'></div>
      <div id='pagination-container'></div>
    </Container>
  )
}

This is my entire instantSearch.js in its non-working version:

// instantSearch on homepage
document.addEventListener('DOMContentLoaded', () => {
  // Find the active nav item in the sidebar
  const mainSearch = instantsearch({
    appId: '...',
    apiKey: '...',
    indexName: 'stackery',
    routing: true
  });

  const resultsTemplate = function(hit) {
  let url = hit.url;
  const title = hit._highlightResult.hierarchy.lvl0.value;
  const content = hit._highlightResult.content.value;
  console.log(hit);

  return `
      <div class="post-item">
        <a class="post-link" href="${url}">
          <h4>${title}</h4>
        </a>
        <div class="search-article-description">${content}</div>
        <a href="${url}" class="read-more">Read More &raquo;</a>
      </div>
  `;
}

  // initialize SearchBox
  mainSearch.addWidget(
    instantsearch.widgets.searchBox({
      container: '#search_input_main',
      placeholder: 'Search the Docs',
      autofocus: false,
      poweredBy: true
    })
  );
  // add hits cont
  mainSearch.addWidget(
    instantsearch.widgets.hits({
      container: '#search-hits',
      templates: {
        empty: 'No results',
        // item: $('#results-template').html()
        item: resultsTemplate
      },
      hitsPerPage: 10
    })
  );
  // add pagination
  mainSearch.addWidget(
    instantsearch.widgets.pagination({
      container: '#pagination-container',
      maxPages: 10,
      // default is to scroll to 'body', here we disable this behavior
      scrollTo: false
    })
  );

  mainSearch.start();

  // Toggle search results
  const search_input_main = document.querySelector('#search_input_main');
  const searchContainer = document.querySelector('#search-container');

  // Bind keyup event on the input
  search_input_main.addEventListener('keyup', function() {
    if (search_input_main.value.length > 0) {
      searchContainer.classList.remove('hidden');
    } else {
      searchContainer.classList.add('hidden');
    }
  });

  // hide results on reset
  document.querySelector('.ais-search-box--reset').addEventListener('click', function() {
    searchContainer.classList.add('hidden');
  });

});

(the reason I'm using the variable mainSearch instead of search is because I already use search for Docsearch in the navbar.

s-pace commented 5 years ago

๐Ÿ‘‹ @bildungsroman ,

You can integrate in your homepage the provided snippet twice with a different inputSelector in order to have it in another place.

Since you are using react, what about using our dedicated react instantSearch library? It might be easier and more compliant.

bildungsroman commented 5 years ago

@s-pace I wish I could, but the way Docusaurus is set up is that it only uses stateless React components to set up the static site, so there's no actual React on the frontend, just HTML. I need to inject that template into the HTML in the process.

I made a stackoverflow question with all of this laid out (hopefully more clearly): https://stackoverflow.com/questions/52523180/add-html-with-template-script-to-docusaurus-react-page

I think my problem is a special and frustrating intersection of Docusaurus and Docsearch, though I imagine I won't be the only one facing it as Docsearch is recommended in the Docusaurus documentation. I could see having an instantsearch section on the homepage in addition to the navbar search be a popular feature, and other users will run into this issue if there's not a template to use.

bildungsroman commented 5 years ago

As for having a different inputSelector, that does work, but no results are displayed. Is there a way to modify where the results are displayed, rather than in a modal below the search input?

bildungsroman commented 5 years ago

Ok, I have a partial solution and a real solution! I hope the partial solution is incorporated into a template in the future :)

First, I followed @s-pace 's suggestion and set only_content_level=true in my configuration file. Then, for the partial solution, I used this template in my instantSearch.js file:

  const resultsTemplate = function(hit) {
  let url = hit.url;
  const title = hit.hierarchy.lvl1;
  const content = hit._highlightResult.content.value;

  return `
      <div class="post-item">
        <a class="post-link" href="${url}">
          <h4>${title}</h4>
        </a>
        <div class="search-article-description">${content}</div>
        <a href="${url}" class="read-more">Read More &raquo;</a>
      </div>
  `;
}

I say this is partial because it only showed the top result, over and over... but hey, progress and hopefully something to build on.

This ended up being the actual solution:

const React = require('react');

let resultsTemplate = `
<script type="text/template" id="results-template">
  <div class="ais-result">
    {{#hierarchy.lvl0}}
    <div class="ais-lvl0">
      {{{_highlightResult.hierarchy.lvl0.value}}}
    </div>
    {{/hierarchy.lvl0}}

    <div class="ais-lvl1">
      {{#hierarchy.lvl1}} {{{_highlightResult.hierarchy.lvl1.value}}} {{/hierarchy.lvl1}} {{#hierarchy.lvl2}} > {{{_highlightResult.hierarchy.lvl2.value}}} {{/hierarchy.lvl2}} {{#hierarchy.lvl3}} > {{{_highlightResult.hierarchy.lvl3.value}}} {{/hierarchy.lvl3}}
      {{#hierarchy.lvl4}} > {{{_highlightResult.hierarchy.lvl4.value}}} {{/hierarchy.lvl4}}
    </div>
    <div class="ais-content">
      {{{#content}}} {{{_highlightResult.content.value}}} {{{/content}}}
    </div>
  </div>
</script>
`
class Footer extends React.Component {
  ...
  render () {
    return (
      <footer className='nav-footer' id='footer'>
        ...
        <div
            dangerouslySetInnerHTML={{ __html: resultsTemplate }}
          />
      </footer>
    );
  }
}

module.exports = Footer;

Note the use of the quote marks used for codeblocks rather than the normal " or ' - that did the trick.

Thank you everyone for your help! I hope this is of use to a future struggling dev.

bildungsroman commented 5 years ago

One final question, @s-pace :

In your jsfiddle as well as in my site, I'm getting repeated results in the instantsearch that don't appear in the navbar docsearch (see screenshot). I'll work on optimizing the template myself, but any hints would be appreciated.

screen shot 2018-09-26 at 11 26 57 am 2
bildungsroman commented 5 years ago

Specifically, each result or the contents of <div class="ais-result">...</div> are being displayed 18 times... no idea why. In your jsfiddle, they're displayed only 6 times each, so it makes me think it has something to do with hitsPerPage, but changing that to 1 had no effect for me.

Haroenv commented 5 years ago

I know this would be repeating work, but since docusaurus uses react, is an option to use react instantsearch? https://community.algolia.com/react-instantsearch/

s-pace commented 5 years ago

๐Ÿ‘‹ @bildungsroman ,

The search-UI natively integrated within your Docusaurus website is filtering over the current version thanks to the parameter facetFilters

For example for this page, the HTML contains the following meta:

<meta name="docsearch:language" content="en">
<meta name="docsearch:version" content="1.5.5">

These information are indexed within your record.

From the integrated snippet, it looks this way:

<script type="text/javascript"> docsearch({
  apiKey: '1f37f3b055249d4873e7665523830077',
  indexName: 'stackery',
  inputSelector: '### REPLACE ME ####',
  algoliaOptions: { 'facetFilters': ["version:$VERSION", "language:$LANGUAGE"] },
  debug: false // Set debug to true if you want to inspect the dropdown
});
</script>

You will need to integrate these facetFilters within your search. You can find some documentation helping you in this way in the instantSearch related materials.

Let us know when you are done with it.

bildungsroman commented 5 years ago

@Haroenv Unfortunately Docusaurus uses React only on the back-end to build a static site, so that wouldn't work. What's served is static HTML, and stateful components don't work (I've tried this already and the Docusaurus docs support this).

@s-pace I'm not using the Docsearch UI for this, I'm using the Instantsearch UI, where the problem lies. I tried setting facetFilters and it did nothing:

  const mainSearch = instantsearch({
    appId: '...',
    apiKey: '...',
    indexName: 'stackery',
    algoliaOptions: { 'facetFilters': ["version:1.5.5", "language:en"] },
    routing: true
  });

The problem is I'm still seeing repeated results in my instantsearch that don't appear in my docsearch - see screenshot:

screen shot 2018-09-27 at 10 27 39 am

The "CLI Reference" item appears 18 times before the next item appears, whereas in the navbar Docsearch it only appears once, as it should.

Right now I'm realizing this is a problem of formatting and building the right template that makes instantsearch work with a docsearch index without displaying results from every version (and potentially every language).

bildungsroman commented 5 years ago

@s-pace Just to test, I tried changing to this:

docsearch({
  apiKey: "...",
  indexName: "stackery",
  inputSelector: "#search_input_react",
  algoliaOptions: { // sets which version's results are displayed
    facetFilters: [ "language:en", "version:next" ]
  },
  debug: true // Set debug to true if you want to inspect the dropdown
});

in every place Docsearch appears, and still no luck :(

I'm going to try to build a filtering function that eliminates multiple results, as I see no other way to do it.

bildungsroman commented 5 years ago

What would be super helpful is if Docsearch returned a version property (i.e. hit._highlightResult.version), then I could filter hit based on version. Is there a way to add that to my config file?

bildungsroman commented 5 years ago

I've hacked this together - not the best solution, but could be useful to anyone else using Docusaurus. The downside is you have to manually set the versions:

  mainSearch.addWidget(
    instantsearch.widgets.hits({
      container: '#search-hits',
      templates: {
        empty: 'No results',
        item: $('#results-template').html()
      },
      transformData: {
        item: function(hit) {
          const versions = ['0.0.1', '0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.1.0', '1.3.0', '1.4.3', '1.4.5', '1.4.6', '1.4.7', '1.5.0', '1.5.1', '1.5.2', '1.5.3', '1.5.4', 'next']
          for (let i=0;i<versions.length;i++) {
            if (hit.url.indexOf(versions[i]) > -1) {
              hit.version = versions[i];
              hit.oldVersion = true;
              break;
            } else {
              hit.version = "current";
            }
          }
          return hit;
        }
      },
      hitsPerPage: 10
    })
  );

Edit: I found another (major) downside: I still have to return hit;, even for an old version (otherwise I get no results for some reason). I added the hit.oldVersion flag to filter in the template:

let resultsTemplate = `
<script type="text/template" id="results-template">
  <div class="ais-result">
    {{^oldVersion}}
    {{#hierarchy.lvl0}}
    <div class="ais-lvl0">
      <a title="{{_highlightResult.hierarchy.lvl1.value}}" href="{{{url}}}"><h4>{{{_highlightResult.hierarchy.lvl0.value}}}</h4></a>
    </div>
    {{/hierarchy.lvl0}}

    <div class="ais-lvl1 breadcrumbs">
      {{#hierarchy.lvl1}} {{{_highlightResult.hierarchy.lvl1.value}}} {{/hierarchy.lvl1}} {{#hierarchy.lvl2}} > {{{_highlightResult.hierarchy.lvl2.value}}} {{/hierarchy.lvl2}} {{#hierarchy.lvl3}} > {{{_highlightResult.hierarchy.lvl3.value}}} {{/hierarchy.lvl3}}
      {{#hierarchy.lvl4}} > {{{_highlightResult.hierarchy.lvl4.value}}} {{/hierarchy.lvl4}}
    </div>

    <div class="ais-content">
      {{{#content}}} {{{_highlightResult.content.value}}} {{{/content}}}
    </div>
    {{/oldVersion}}
  </div>
</script>
`

While this works, the DOM still thinks there are more results, they're just not shown, but that means with pagination on I get empty pages followed by a page of several results. If I turn pagination off, I just get what would be on the first page of paginated results (with everything else hidden). Not sure what to do about this :(

Haroenv commented 5 years ago

you can filter on version in InstantSearch.js like this:

search.addWidget(
  instantsearch.widgets.configure({
    filters: 'version:XXX'
  })
);
bildungsroman commented 5 years ago

@Haroenv That ended up doing the trick, thank you!!

For any Docusaurus users googling their way here, this ended up being my working function:

In instantsearch.js (found in website/static/js):

// instantSearch on homepage
document.addEventListener('DOMContentLoaded', () => {
  // Set initial parameters for instantsearch on the homepage
  const mainSearch = instantsearch({
    appId: '...',
    apiKey: '...',
    indexName: 'stackery',
    searchParameters: {
      hitsPerPage: 10
    },
    routing: true
  });

  mainSearch.addWidget(
    instantsearch.widgets.configure({
      filters: 'version:1.5.5' // set latest version here
    })
  );

  // initialize SearchBox
  mainSearch.addWidget(
    instantsearch.widgets.searchBox({
      container: '#search_input_main',
      placeholder: 'Search the Docs',
      autofocus: false,
      poweredBy: true
    })
  );
  // add hits cont
  mainSearch.addWidget(
    instantsearch.widgets.hits({
      container: '#search-hits',
      templates: {
        empty: 'No results for the search term <em>"{{query}}"</em>.',
        item: $('#results-template').html() // this is the template at index.js
      },
      hitsPerPage: 10
    })
  );

  // add pagination
  mainSearch.addWidget(
    instantsearch.widgets.pagination({
      container: '#pagination-container',
      maxPages: 20,
      // default is to scroll to 'body', here we disable this behavior
      scrollTo: '#search-container'
    })
  );

  mainSearch.start();

  // Toggle search results
  const search_input_main = document.querySelector('#search_input_main');
  const searchContainer = document.querySelector('#search-container');

  // Bind keyup event on the input
  search_input_main.addEventListener('keyup', function() {
    if (search_input_main.value.length > 0) {
      searchContainer.classList.remove('hidden');
    } else {
      searchContainer.classList.add('hidden');
    }
  });

  // hide results on reset
  document.querySelector('.ais-search-box--reset').addEventListener('click', function() {
    searchContainer.classList.add('hidden');
  });

});

And this is my pages/en/index.js:

const React = require('react');
...
// for homepage instantsearch
let resultsTemplate = `
<script type="text/template" id="results-template">
  <div class="ais-result">
    {{#hierarchy.lvl0}}
    <div class="ais-lvl0">
      <a title="{{_highlightResult.hierarchy.lvl1.value}}" href="{{{url}}}"><h4>{{{_highlightResult.hierarchy.lvl0.value}}}</h4></a>
    </div>
    {{/hierarchy.lvl0}}

    <div class="ais-lvl1 breadcrumbs">
      {{#hierarchy.lvl1}} {{{_highlightResult.hierarchy.lvl1.value}}} {{/hierarchy.lvl1}} {{#hierarchy.lvl2}} > {{{_highlightResult.hierarchy.lvl2.value}}} {{/hierarchy.lvl2}} {{#hierarchy.lvl3}} > {{{_highlightResult.hierarchy.lvl3.value}}} {{/hierarchy.lvl3}}
      {{#hierarchy.lvl4}} > {{{_highlightResult.hierarchy.lvl4.value}}} {{/hierarchy.lvl4}}
    </div>

    <div class="ais-content">
      {{{#content}}} {{{_highlightResult.content.value}}} {{{/content}}}
    </div>
  </div>
</script>
`
...

const SearchInput = () => (
  <div>
    <div className='productShowcaseSection small-paddingBottom paddingTop' style={{textAlign: 'center'}}>
      <h3>Get the most out of Stackery's serverless toolkit</h3>
      <Container>
        <div className='search_input_div'>
          <input id="search_input_main" type="text" className="form-control" placeholder="Search the docs" aria-label="Search" aria-describedby="search"></input>
        </div>
      </Container>
    </div>
  </div>
);

const SearchHits = () => {
  return (
    <Container className='hidden' id='search-container'>
      <h3>Search results:</h3>
      <div id='search-hits'></div>
      <div id='pagination-container'></div>
      <div
        dangerouslySetInnerHTML={{ __html: resultsTemplate }}
      />
    </Container>
  )
}

class HomeSplash extends React.Component {
  render () {
    const language = this.props.language || '';
    return (
      <SplashContainer>
        <div className='inner'>
          <ProjectTitle />
          <PromoSection>
            ...
          </PromoSection>
          <SearchInput />
        </div>
      </SplashContainer>
    );
  }
}
...

class Index extends React.Component {
  render () {
    const language = this.props.language || '';

    return (
      <div>
        <HomeSplash language={language} />
        <SearchHits />
        <BrowseDocs />
        <TryIt />
      </div>
    );
  }
}

module.exports = Index;

Thank you again @Haroenv and @s-pace!

Haroenv commented 5 years ago

Glad this all got resolved, and sorry it took a while :)