├── .eslintrc ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── README.md ├── _assets └── website │ ├── Algolia_logo_bg-white.svg │ └── style.css ├── algolia-ranking.png ├── index.js ├── package.json └── src ├── components └── AlgoliaResults.js ├── index.js └── unescape.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "gitbook/plugin" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Exclude prepublish dependencies 28 | assets/algoliasearch.min.js 29 | # vim swapfile 30 | *.swp 31 | 32 | # Plugin assets 33 | _assets/plugin.js 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Publish assets on NPM 2 | !_assets/plugin.js 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Building the plugin 2 | 3 | You need to have `npm` installed. Install the dependencies: 4 | 5 | ``` 6 | npm install 7 | 8 | ``` 9 | 10 | Now, you can build the plugin by running: 11 | 12 | ``` 13 | npm run build-js 14 | ``` 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power search using Algolia 2 | 3 | Use Algolia as a back-end to index and search your book's content. 4 | 5 | This plugins requires gitbook `>=4.0.0`. 6 | 7 | ## How to use it? 8 | 9 | ### Create an Algolia account 10 | 11 | 1. Sign up to [Algolia](https://www.algolia.com) 12 | 2. Set up a basic index. 13 | 3. Access your [API keys settings](https://www.algolia.com/api-keys), and keep this page open for the next steps. 14 | 15 | ### Set up the plugin in your book. 16 | 17 | 1. Open Settings > Plugins Store interface in the GitBook Editor. 18 | 2. Install the `algolia` plugin. 19 | 3. Specify the following values in the modal for your book: 20 | * **Algolia Index Name**, which is the name of the index you set up in Algolia web. 21 | * **Algolia Application ID**, which is your unique application ID for API identification. 22 | * **Algolia Search-Only API Key**, which is the unique ID for search queries. 23 | * **Is your Algolia account free**, which you leave as `true` if you have what Algolia call a "Hacker Account". 24 | 25 | Or if you prefer, you can declare the plug-in values in the `book.json` file. Add the plugin and its configuration to your `book.json`: 26 | 27 | ```JSON 28 | { 29 | "plugins": ["algolia"], 30 | "pluginsConfig": { 31 | "algolia": { 32 | "index": "algolia-index-name", 33 | "applicationID": "algolia-application-id", 34 | "publicKey": "algolia-search-only-api-key", 35 | "freeAccount": "true" 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ### Set Algolia Environment Variables 42 | 43 | If you are using the GitBook Editor, you can specify the `ALGOLIA_PRIVATEKEY` in the Settings of your book. 44 | 45 | 1. Login to https://www.gitbook.com 46 | 2. Go to the `Settings` of the book you want to add the environment variable to. 47 | 4. Scroll down to the `Environment Variables` group. 48 | 5. Add a new variable, named `ALGOLIA_PRIVATEKEY`, and put your **Admin API Key** as value. 49 | 50 | If you are using the command line, pass your **Admin API Key** as an environment variable when launching gitbook: 51 | 52 | ```Bash 53 | $ ALGOLIA_PRIVATEKEY="algolia-admin-api-key" gitbook serve 54 | ``` 55 | 56 | #### A note about the index 57 | 58 | **WARNING** The plugin will replace the entire index at each build phase. Do not use an existing index, unless you no longer require its contents. 59 | 60 | When setting up the basic index on Algolia, you will be prompted to generate or import the index so Algolia can begin analysing your book. 61 | 62 | You do not need to create the index manually for the plugin to work. 63 | 64 | 65 | ### Adding keywords to a page 66 | 67 | You can specify explicit keywords for any page. 68 | 69 | ```md 70 | --- 71 | search: 72 | keywords: ['keyword1', 'keyword2', 'etc.'] 73 | 74 | --- 75 | 76 | # My Page 77 | 78 | This page will rank better if we search for 'keyword1'. 79 | ``` 80 | 81 | ### Disabling indexing of a page 82 | 83 | You can disable the indexing of a specific page by adding a YAML header to the page: 84 | 85 | ```md 86 | --- 87 | search: false 88 | --- 89 | 90 | # My Page 91 | 92 | This page is not indexed in by Algolia. 93 | ``` 94 | 95 | ### Fine tuning the Algolia search rankings 96 | 97 | After having indexed your book at least once, you can configure your Algolia index from your [dashboard](https://www.algolia.com/explorer). You can go to the _Ranking_ settings and tell Algolia how to search your pages. You can see below an example configuration, that tells Algolia to search, by order of importance, the keywords of your pages, their title, and finally their content (body). 98 | 99 | ![Example ranking configuration](./algolia-ranking.png) 100 | -------------------------------------------------------------------------------- /_assets/website/Algolia_logo_bg-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Algolia_logo_bg-white 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /_assets/website/style.css: -------------------------------------------------------------------------------- 1 | .Algolia { 2 | opacity: 0.5; 3 | font-size: 10pt; 4 | margin-top: 50px; 5 | text-align: right; 6 | } 7 | 8 | .Algolia a { 9 | color: inherit; 10 | } 11 | 12 | .Algolia-logo { 13 | height: 20pt; 14 | vertical-align: middle; 15 | } -------------------------------------------------------------------------------- /algolia-ranking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitbookIO/plugin-algolia/e84a54107045c438fc08279409fc1f80367293e2/algolia-ranking.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const algoliasearch = require('algoliasearch'); 2 | 3 | let client = null; 4 | let index = null; 5 | 6 | module.exports = { 7 | hooks: { 8 | init() { 9 | // Don't index when not generating website 10 | if (this.output.name != 'website') return; 11 | 12 | // Check that private key has been provided 13 | if (!process.env.ALGOLIA_PRIVATEKEY) { 14 | this.log.warn.ln('Algolia indexation is disabled:'); 15 | this.log.warn.ln('You need to configure the ALGOLIA_PRIVATEKEY environment variable using your Admin API Key.'); 16 | return; 17 | } 18 | 19 | // Initialize Algolia client 20 | const config = this.config.get('pluginsConfig.algolia') || this.config.get('algolia'); 21 | client = algoliasearch(config.applicationID, process.env.ALGOLIA_PRIVATEKEY); 22 | 23 | // Initialize index with book's title 24 | index = client.initIndex(config.index); 25 | 26 | // Clean index for updates 27 | return index.clearIndex() 28 | .then(function(content) { 29 | return index.waitTask(content.taskID); 30 | }); 31 | }, 32 | 33 | page(page) { 34 | const searchConfig = page.attributes.search; 35 | 36 | // Don't index when not generating website or if index has not been set 37 | if (this.output.name != 'website' || !index || searchConfig === false) { 38 | return page; 39 | } 40 | 41 | this.log.debug.ln('index page', page.path); 42 | // Transform as text 43 | const text = page.content.replace(/(<([^>]+)>)/ig, ''); 44 | 45 | let keywords = []; 46 | if (searchConfig) { 47 | keywords = searchConfig.keywords || []; 48 | } 49 | 50 | // Add to index 51 | return index.addObject({ 52 | url: this.output.toURL(page.path), 53 | path: page.path, 54 | title: page.title, 55 | keywords, 56 | body: text, 57 | level: page.level, 58 | depth: page.depth 59 | }) 60 | .then(function() { 61 | return page; 62 | }); 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitbook-plugin-algolia", 3 | "description": "Use Algolia as a backend for research", 4 | "main": "index.js", 5 | "version": "2.0.0", 6 | "browser": "./_assets/plugin.js", 7 | "dependencies": { 8 | "gitbook-core": "^4.0.0", 9 | "algoliasearch": "3.18.1" 10 | }, 11 | "devDependencies": { 12 | "gitbook-plugin": "^4.0.0", 13 | "eslint": "3.7.1", 14 | "eslint-config-gitbook": "1.4.0" 15 | }, 16 | "engines": { 17 | "gitbook": ">=4.0.0-alpha.0" 18 | }, 19 | "scripts": { 20 | "build-js": "gitbook-plugin build ./src/index.js ./_assets/plugin.js", 21 | "prepublish": "npm run build-js" 22 | }, 23 | "author": "GitBook Team ", 24 | "contributors": [ 25 | { 26 | "name": "Johan Preynat", 27 | "email": "johan@gitbook.com" 28 | } 29 | ], 30 | "homepage": "https://github.com/GitbookIO/plugin-algolia", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/GitbookIO/plugin-algolia.git" 34 | }, 35 | "license": "Apache-2.0", 36 | "bugs": { 37 | "url": "https://github.com/GitbookIO/plugin-algolia/issues" 38 | }, 39 | "gitbook": { 40 | "properties": { 41 | "index": { 42 | "type": "string", 43 | "title": "Algolia index name", 44 | "required": true 45 | }, 46 | "applicationID": { 47 | "type": "string", 48 | "title": "Algolia Application ID", 49 | "required": true 50 | }, 51 | "publicKey": { 52 | "type": "string", 53 | "title": "Algolia Search-Only API Key", 54 | "required": true 55 | }, 56 | "freeAccount": { 57 | "type": "bool", 58 | "title": "Is your Algolia account free", 59 | "default": true 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/AlgoliaResults.js: -------------------------------------------------------------------------------- 1 | const GitBook = require('gitbook-core'); 2 | const { React } = GitBook; 3 | 4 | /** 5 | * Displays the default search results, but adds the Algolia logo. 6 | */ 7 | const AlgoliaResults = React.createClass({ 8 | propTypes: { 9 | file: GitBook.PropTypes.File.isRequired, 10 | children: React.PropTypes.node 11 | }, 12 | 13 | render() { 14 | const { file, children } = this.props; 15 | 16 | return ( 17 |
18 | 19 | { children } 20 |
21 | 22 | Powered by 24 | 25 |
26 |
27 | ); 28 | } 29 | }); 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | file: state.file 34 | }; 35 | } 36 | 37 | 38 | module.exports = GitBook.connect(AlgoliaResults, mapStateToProps); 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const GitBook = require('gitbook-core'); 2 | const algoliasearch = require('algoliasearch'); 3 | 4 | const AlgoliaResults = require('./components/AlgoliaResults'); 5 | const unescape = require('./unescape'); 6 | 7 | const HITS_PER_PAGE = 15; 8 | 9 | module.exports = GitBook.createPlugin({ 10 | activate: (dispatch, getState, { Search, Components }) => { 11 | dispatch(Search.registerHandler('algolia', searchHandler)); 12 | 13 | if (isFreeAccount(getState)) { 14 | dispatch(Components.registerComponent(AlgoliaResults, { role: 'search:results' })); 15 | } 16 | }, 17 | 18 | reduce: (state, action) => state 19 | }); 20 | 21 | /** 22 | * Search against a query 23 | * @param {String} query 24 | * @return {Promise>} 25 | */ 26 | function searchHandler(query, dispatch, getState) { 27 | const index = getAlgoliaIndex(getState); 28 | 29 | return new GitBook.Promise((resolve, reject) => { 30 | return index.search(query, { hitsPerPage: HITS_PER_PAGE }) 31 | .then(function(res) { 32 | const results = res.hits.map(function(hit) { 33 | return { 34 | title: hit.title, 35 | // hit.body is escaped HTML, we need to unescape it 36 | body: unescape(hit.body), 37 | url: hit.url 38 | }; 39 | }); 40 | 41 | return resolve(results); 42 | }, reject); 43 | }); 44 | } 45 | 46 | /** 47 | * #param {Function} getState 48 | * @return {Object} The plugin config 49 | */ 50 | function getConfig(getState) { 51 | return getState().config.getIn(['pluginsConfig', 'algolia']); 52 | } 53 | 54 | /** 55 | * Return the Algolia client. Initialize it if needed. 56 | *#param {Function} getState 57 | */ 58 | function getAlgoliaIndex(getState) { 59 | const config = getConfig(getState); 60 | const client = algoliasearch(config.get('applicationID'), config.get('publicKey')); 61 | return client.initIndex(config.get('index')); 62 | } 63 | 64 | /** 65 | * @param getState 66 | * @return {Boolean} true if the configured account is free 67 | */ 68 | function isFreeAccount(getState) { 69 | return getConfig(getState).get('freeAccount'); 70 | } 71 | -------------------------------------------------------------------------------- /src/unescape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unescape an HTML string. XSS vulnerable! 3 | * @param {String} str 4 | * @return {String} 5 | */ 6 | function unescape(str) { 7 | const el = document.createElement('div'); 8 | // XSS can happen here... But we trust the results from Algolia, 9 | // and the impact of XSS is limited on books 10 | el.innerHTML = str; 11 | return el.textContent; 12 | } 13 | 14 | module.exports = unescape; 15 | --------------------------------------------------------------------------------