fix(hooks): Apply unique objectID after all hooks

This commit is contained in:
Pixelastic 2017-12-19 18:49:00 +01:00
parent 86e1e59956
commit bd826c5ee0
12 changed files with 282 additions and 141 deletions

View File

@ -10,6 +10,72 @@ themes. I could use the Hyde theme as a great use-case and show how to implement
it with InstantSearch.js.
This will be the simplest example. How to add search to Jekyll blog. We'll use
the default minima theme as an example.
We'll change the front page. Instead of displaying the list of topics, it will
display a searchable list (and we'll add an excerpt for good measure).
Because I'm editing a pre-pakaged theme, the first thing to do is to copu the
files from the theme to my locl jekyll section. I take the posts layout, to
change the display of the full page
Then, we will add the files needed by Algolia. The IS js file, as well as two
CSS files to style it. The first one provides just "usable" default, the second
one provides some theming that happen to be similar to the one of minima. great
We will start by configuring the call and instanciating. We'll need to reuse
some of our credentials, so we can have them directly from the config.yml file
We'll also need a earch only API key. This one is a public key, that can only
read the idnex (not edit stuff). It's safe to put it in the markup. Just to stay
consustent, I'll put it along with the other keys, to have all my credentials at
the same place, ut it's not officially part of the plugin. you can name it the
way your want
Now that we have that, well it does not do much. What we'll do is add the
results to be displayed, and adding our first widget
the moment the widget is instanciates, it will replace its target with the
result grabbed from the index. we will put the target to where the list is
already displayed. it means that on page load, everything will be here, but then
the js lib will kick in and replace static results with dynamic one
at that point it works but its too raw. we'll add a template so it looks exactly
like the static version. we re-use the same kind of markup, but we might have to
do a few adjustements.
the original version had no excerpt, but we'll add it (both to the static and
dynamic one, so there is no "jump" from one to the other). we'll also have to
add some margin around the elements and rpelace it with divs. instantsearch adds
divs by default, and divs inside ul won't work so we change things around
looks the same. Now we had search, buy adding a search bar, defining its
placeholder and width
works well, but we have some display issues we should fix. inside the template
function we'll change a few values. the date should be formatted using moment,
and the results should be highlighted with what is matching
- Copy file form minima
- Include algolia.html where we put everything
- include JS and CSS
- instanciate the instance with credentials
- add dynamic results
- style results so they look the same
- add excerpt to both sides, format the date
- add search bar
- add highlight on results

View File

@ -78,6 +78,21 @@ want to keep this key secret and not commit it to your versioning system.
_Note that the method can be simplified to `jekyll algolia` by using an
[alternative way][6] of loading the API key and using [rubygems-bundler][7]._
## Front-end
The plugin only takes care of extracting your data and pushing it to an Algolia
index. Building the front-end that will allow your users to search into that
data is not part of the plugin.
As it would depend too much on the theming you applied to Jekyll, we could not
create a one-size-fits-all solution. Instead, the best solution is to use our
[InstantSearch.js][8] library (also available for [Vue.js][9] and [React][10]).
It's an easy-to-use set of UI widgets you can use to build your own search in
a matter of minutes.
You can also head to the [Examples][11] section to see some tutorials
on the most common use-cases.
[1]: https://jekyllrb.com/
[2]: https://www.ruby-lang.org/en/
@ -86,3 +101,7 @@ _Note that the method can be simplified to `jekyll algolia` by using an
[5]: https://www.algolia.com/licensing
[6]: ./commandline.html#algolia-api-key-file
[7]: https://github.com/rvm/rubygems-bundler
[8]: https://community.algolia.com/instantsearch.js/
[9]: https://community.algolia.com/vue-instantsearch/
[10]: https://community.algolia.com/react-instantsearch/
[11]: ./examples.html

View File

@ -26,7 +26,9 @@ The file should have the following structure:
```ruby
module Jekyll
module Algolia
# Add your hooks here
module Hooks
# Add your hooks here
end
end
end
```
@ -40,7 +42,7 @@ indexed if it returns `false`.
| Key | Value |
| ---- | ---- |
| Signature | `hook_should_be_excluded?(filepath)` |
| Signature | `should_be_excluded?(filepath)` |
| Arguments | <ul><li>`filepath`: The source path of the file</li></ul> |
| Expected returns | <ul><li>`true` if the file should be excluded</li><li>`false` if it should be indexed</li></ul> |
@ -52,10 +54,12 @@ indexed if it returns `false`.
```ruby
module Jekyll
module Algolia
def self.hook_should_be_excluded?(filepath)
# Do not index blog posts from 2015
return true if filepath =~ %r{_posts/2015-}
false
module Hooks
def self.should_be_excluded?(filepath)
# Do not index blog posts from 2015
return true if filepath =~ %r{_posts/2015-}
false
end
end
end
end
@ -75,7 +79,7 @@ representation of the HTML node the record was extracted from (as specified in
| Key | Value |
| ---- | ---- |
| Signature | `hook_before_indexing_each(record, node)` |
| Signature | `before_indexing_each(record, node)` |
| Arguments | <ul><li>`record`: A hash of the record that will be pushed</li><li>`node`: A [Nokogiri][7] representation of the HTML node it was extracted from</li></ul> |
| Expected returns | <ul><li>A hash of the record to be indexed</li><li>`nil` if the record should not be indexed</li></ul> |
@ -84,13 +88,15 @@ representation of the HTML node the record was extracted from (as specified in
```ruby
module Jekyll
module Algolia
def self.hook_before_indexing_each(record, node)
# Do not index deprecation warnings
return nil if node.attr('class') =~ 'deprecation-notice'
# Add my name as an author to each record
record[:author] = 'Myself'
module Hooks
def self.before_indexing_each(record, node)
# Do not index deprecation warnings
return nil if node.attr('class') =~ 'deprecation-notice'
# Add my name as an author to each record
record[:author] = 'Myself'
record
record
end
end
end
end
@ -110,7 +116,7 @@ knowing the full context of what is going to be pushed.
| Key | Value |
| ---- | ---- |
| Signature | `hook_before_indexing_all(records)` |
| Signature | `before_indexing_all(records)` |
| Arguments | <ul><li>`records`: An array of hashes representing the records that are going to be pushed</li></ul> |
| Expected returns | <ul><li>An array of hashes to be pushed as records</li></ul> |
@ -119,17 +125,19 @@ knowing the full context of what is going to be pushed.
```ruby
module Jekyll
module Algolia
def self.hook_before_indexing_all(records)
# Add a tags array to each record
records.each do |record|
record[:tags] = []
# Add 'blog' as a tag if it's a post
record[:tags] << 'blog' if record[:type] == 'post'
# Add js as a tag if it's about javascript
record[:tags] << 'js' if record[:title].include?('js')
end
module Hooks
def self.before_indexing_all(records)
# Add a tags array to each record
records.each do |record|
record[:tags] = []
# Add 'blog' as a tag if it's a post
record[:tags] << 'blog' if record[:type] == 'post'
# Add js as a tag if it's about javascript
record[:tags] << 'js' if record[:title].include?('js')
end
records
records
end
end
end
end

View File

@ -8,7 +8,7 @@ module Jekyll
module Algolia
require 'jekyll/algolia/version'
require 'jekyll/algolia/utils'
require 'jekyll/algolia/user_hooks'
require 'jekyll/algolia/hooks'
require 'jekyll/algolia/configurator'
require 'jekyll/algolia/logger'
require 'jekyll/algolia/error_handler'
@ -91,7 +91,12 @@ module Jekyll
end
# Applying the user hook on the whole list of records
records = Jekyll::Algolia.hook_before_indexing_all(records)
records = Hooks.apply_all(records)
# Adding a unique objectID to each record
records.map! do |record|
Extractor.add_unique_object_id(record)
end
Logger.verbose("I:Found #{files.length} files")

View File

@ -31,7 +31,7 @@ module Jekyll
# Apply custom user-defined hooks
# Users can return `nil` from the hook to signal we should not index
# such a record
record = apply_hook_each(record, node)
record = Hooks.apply_each(record, node)
next if record.nil?
records << record
@ -40,22 +40,10 @@ module Jekyll
records
end
# Public: Apply the hook_before_indexing_each hook to the record.
# Returning nil from this hook will skip the record. If the record has
# been changed from the hook, its internal objectID should be updated
# accordingly.
#
# record - The hash of the record to be pushed
# node - The Nokogiri node of the element
def self.apply_hook_each(record, node)
hooked_record = Jekyll::Algolia.hook_before_indexing_each(record, node)
return nil if hooked_record.nil?
# If the record has been changed, we need to update its objectID
if hooked_record != record
record = hooked_record
record[:objectID] = AlgoliaHTMLExtractor.uuid(hooked_record)
end
# Public: Adds a unique :objectID field to the hash, representing the
# current content of the record
def self.add_unique_object_id(record)
record[:objectID] = AlgoliaHTMLExtractor.uuid(record)
record
end

View File

@ -92,7 +92,7 @@ module Jekyll
#
# file - The Jekyll file
def self.excluded_from_hook?(file)
Jekyll::Algolia.hook_should_be_excluded?(file.path)
Hooks.should_be_excluded?(file.path)
end
# Public: Return the path to the original file, relative from the Jekyll

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
module Jekyll
module Algolia
# Applying user-defined hooks on the processing pipeline
module Hooks
# Public: Apply the before_indexing_each hook to the record.
# This method is a simple wrapper around methods that can be overwritten
# by users. Using a wrapper around it makes testing their behavior easier
# as they can be mocked in tests.
#
# record - The hash of the record to be pushed
# node - The Nokogiri node of the element
def self.apply_each(record, node)
before_indexing_each(record, node)
end
# Public: Apply the before_indexing_all hook to all records.
# This method is a simple wrapper around methods that can be overwritten
# by users. Using a wrapper around it makes testing their behavior easier
# as they can be mocked in tests.
#
# records - The list of all records to be indexed
def self.apply_all(records)
before_indexing_all(records)
end
# Public: Check if the file should be indexed or not
#
# filepath - The path to the file, before transformation
#
# This hook allow users to define if a specific file should be indexed or
# not. Basic exclusion can be done through the `files_to_exclude` option,
# but a custom hook like this one can allow more fine-grained
# customisation.
def self.should_be_excluded?(_filepath)
false
end
# Public: Custom method to be run on the record before indexing it
#
# record - The hash of the record to be pushed
# node - The Nokogiri node of the element
#
# Users can modify the record (adding/editing/removing keys) here. It can
# be used to remove keys that should not be indexed, or access more
# information from the HTML node.
#
# Users can return nil to signal that the record should not be indexed
def self.before_indexing_each(record, _node)
record
end
# Public: Custom method to be run on the list of all records before
# indexing them
#
# records - The list of all records to be indexed
#
# Users can modify the full list from here. It might provide an easier
# interface than `hook_before_indexing_each` when knowing the full context
# is necessary
def self.before_indexing_all(records)
records
end
end
end
end

View File

@ -1,43 +0,0 @@
# frozen_string_literal: true
module Jekyll
# Hooks that can be safely overwritten by the user
module Algolia
# Public: Check if the file should be indexed or not
#
# filepath - The path to the file, before transformation
#
# This hook allow users to define if a specific file should be indexed or
# not. Basic exclusion can be done through the `files_to_exclude` option,
# but a custom hook like this one can allow more fine-grained customisation.
def self.hook_should_be_excluded?(_filepath)
false
end
# Public: Custom method to be run on the record before indexing it
#
# record - The hash of the record to be pushed
# node - The Nokogiri node of the element
#
# Users can modify the record (adding/editing/removing keys) here. It can be
# used to remove keys that should not be indexed, or access more information
# from the HTML node.
#
# Users can return nil to signal that the record should not be indexed
def self.hook_before_indexing_each(record, _node)
record
end
# Public: Custom method to be run on the list of all records before indexing
# them
#
# records - The list of all records to be indexed
#
# Users can modify the full list from here. It might provide an easier
# interface than `hook_before_indexing_each` when knowing the full context
# is necessary
def self.hook_before_indexing_all(records)
records
end
end
end

View File

@ -6,6 +6,8 @@ require 'spec_helper'
describe(Jekyll::Algolia) do
let(:current) { Jekyll::Algolia }
let(:indexer) { Jekyll::Algolia::Indexer }
let(:hooks) { Jekyll::Algolia::Hooks }
let(:extractor) { Jekyll::Algolia::Extractor }
# Suppress Jekyll log about not having a config file
before do
@ -87,17 +89,21 @@ describe(Jekyll::Algolia) do
source: File.expand_path('./spec/site')
)
end
# The actual indexing should be done on the list of records + one added
# through the custom hook
RSpec::Matchers.define :a_custom_record_added_at_the_end do
match do |actual|
actual[-1][:name] == 'Last one'
end
end
let(:records_after_hook) { [{ foo: 'bar', objectID: 'AAA' }] }
let(:record_after_unique_id) { { foo: 'bar', objectID: 'BBB' } }
before do
allow(Jekyll.logger).to receive(:info)
expect(indexer).to receive(:run).with(a_custom_record_added_at_the_end)
expect(hooks)
.to receive(:apply_all)
.and_return(records_after_hook)
expect(extractor)
.to receive(:add_unique_object_id)
.with(records_after_hook[0])
.and_return(record_after_unique_id)
expect(indexer)
.to receive(:run)
.with([record_after_unique_id])
end
it { current.init(configuration).run }

View File

@ -6,6 +6,7 @@ require 'spec_helper'
describe(Jekyll::Algolia::Extractor) do
let(:configurator) { Jekyll::Algolia::Configurator }
let(:filebrowser) { Jekyll::Algolia::FileBrowser }
let(:hooks) { Jekyll::Algolia::Hooks }
let(:current) { Jekyll::Algolia::Extractor }
let(:site) { init_new_jekyll_site }
@ -76,8 +77,8 @@ describe(Jekyll::Algolia::Extractor) do
context 'with mock data' do
let!(:file) { site.__find_file('html.html') }
before do
allow(Jekyll::Algolia)
.to receive(:hook_before_indexing_each)
allow(hooks)
.to receive(:apply_each)
.with(anything, anything) { |input| input }
allow(current)
@ -133,41 +134,18 @@ describe(Jekyll::Algolia::Extractor) do
end
end
describe '.apply_hook_each' do
subject { current.apply_hook_each(record, node) }
let(:record) { {} }
let(:node) { nil }
describe '.add_unique_object_id' do
subject { current.add_unique_object_id(record) }
let(:record) { { foo: 'bar' } }
let(:objectID) { nil }
before do
allow(Jekyll::Algolia)
.to receive(:hook_before_indexing_each)
.and_return(hook_each_value)
allow(AlgoliaHTMLExtractor)
.to receive(:uuid)
.and_return(:objectID)
end
describe 'should update the value' do
let(:record) { { foo: 'bar' } }
let(:hook_each_value) { { new_foo: 'new_bar' } }
it { expect(subject).to include(new_foo: 'new_bar') }
end
context 'when returning nil from the hook' do
let(:hook_each_value) { nil }
it { should be_nil }
end
describe 'should update the objectID' do
let(:record) { { foo: 'bar', objectID: 'AAA' } }
let(:hook_each_value) { { new_foo: 'new_bar', objectID: 'AAA' } }
it {
expect(AlgoliaHTMLExtractor)
.to receive(:uuid)
.and_return('BBB')
expect(subject[:objectID]).to_not eq 'AAA'
expect(subject[:objectID]).to eq 'BBB'
}
end
it { expect(subject).to include(objectID: :objectID) }
end
end
# rubocop:enable Metrics/BlockLength

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
describe(Jekyll::Algolia::Hooks) do
let(:current) { Jekyll::Algolia::Hooks }
describe '.apply_each' do
subject { current.apply_each(record, node) }
let(:record) { { foo: 'bar' } }
let(:node) { double('Nokogiri Node') }
let(:record_after_hook) { {} }
before do
expect(current)
.to receive(:before_indexing_each)
.with(record, node)
.and_return(record_after_hook)
end
it { should eq record_after_hook }
end
describe '.apply_all' do
subject { current.apply_all(records) }
let(:records) { [{ foo: 'bar' }, { foo: 'baz' }] }
let(:records_after_hook) { {} }
before do
expect(current)
.to receive(:before_indexing_all)
.with(records)
.and_return(records_after_hook)
end
it { should eq records_after_hook }
end
end
# rubocop:enable Metrics/BlockLength

View File

@ -1,18 +1,24 @@
# frozen_string_literal: true
module Jekyll
# Custom hooks
module Algolia
def self.hook_should_be_excluded?(filepath)
filepath == 'excluded-from-hook.html'
end
def self.hook_before_indexing_each(record, _node)
record[:added_through_each] = true
record
end
def self.hook_before_indexing_all(records)
records << {
name: 'Last one'
}
records
# Custom user hooks
module Hooks
def self.should_be_excluded?(filepath)
filepath == 'excluded-from-hook.html'
end
def self.before_indexing_each(record, _node)
record[:added_through_each] = true
record
end
def self.before_indexing_all(records)
records << {
name: 'Last one'
}
records
end
end
end
end