fix(hooks): Apply unique objectID after all hooks
This commit is contained in:
parent
86e1e59956
commit
bd826c5ee0
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
67
lib/jekyll/algolia/hooks.rb
Normal file
67
lib/jekyll/algolia/hooks.rb
Normal 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
|
@ -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
|
@ -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 }
|
||||
|
@ -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
|
||||
|
41
spec/jekyll/algolia/hooks_spec.rb
Normal file
41
spec/jekyll/algolia/hooks_spec.rb
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user