Badzilla's Blog http://badzilla.co.uk/blog-home.xml en Raspberry Pi Music Box http://badzilla.co.uk/raspberry-pi-music-box <span>Raspberry Pi Music Box</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Wed, 06/02/2019 - 10:45</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Introduction</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/DSC_2199-scaled.JPG?itok=Y_HJMKTb 325w, /sites/default/files/styles/max_650x650/public/2019-02/DSC_2199-scaled.JPG?itok=CMspo0I2 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/DSC_2199-scaled.JPG?itok=tQFCS5wN 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/DSC_2199-scaled.JPG?itok=Y_HJMKTb" alt="Complete solution" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>This bog provides a tutorial for building a Raspberry Pi music box which can be controlled using a hand held device through a web interface. Ideal for a bedroom where there is no desire to have a monitor / keyboard /mouse combination. The cornerstone of the build is the <a href="http://www.runeaudio.com/">Rune Audio distro</a> which supports FLAC, Vorbis and mp3 amongst other formats. I will be using a Raspberry Pi 3 Model B+ along with an IQAudIO Pi-DAC+ daughter board. </p> <p>There is fierce debate between the proponents of Rune Audio, MoOde Audio, and Volumio. I elected for Rune purely because there is a native Android app available, and my smartphone is a Samsung.  </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Bill of Materials</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103647-scaled.jpg?itok=LZ6FxaYU 325w, /sites/default/files/styles/max_650x650/public/2019-02/20190202_103647-scaled.jpg?itok=DB6o7TCI 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190202_103647-scaled.jpg?itok=K2rutFZL 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103647-scaled.jpg?itok=LZ6FxaYU" alt="Raspberry Pi" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103757-scaled.jpg?itok=dZIBcP9c 244w, /sites/default/files/styles/max_650x650/public/2019-02/20190202_103757-scaled.jpg?itok=TqIOul8l 488w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190202_103757-scaled.jpg?itok=kGEQ67wX 750w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103757-scaled.jpg?itok=dZIBcP9c" alt="DAC" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103907-scaled.jpg?itok=s_r0OCUA 244w, /sites/default/files/styles/max_650x650/public/2019-02/20190202_103907-scaled.jpg?itok=uUzKTR5- 488w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190202_103907-scaled.jpg?itok=FbiqcJ5R 750w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190202_103907-scaled.jpg?itok=s_r0OCUA" alt="SD Card" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190202_104235-scaled.jpg?itok=q_uUdOAE 244w, /sites/default/files/styles/max_650x650/public/2019-02/20190202_104235-scaled.jpg?itok=0ylAVTAG 488w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190202_104235-scaled.jpg?itok=WRlvhl6R 750w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190202_104235-scaled.jpg?itok=q_uUdOAE" alt="Power" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/pi-case.jpg?itok=Zs6czw21 325w, /sites/default/files/styles/max_650x650/public/2019-02/pi-case.jpg?itok=o6LCXuJK 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/pi-case.jpg?itok=5oIoASJH 800w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/pi-case.jpg?itok=Zs6czw21" alt="Case" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/pdt-mhl-gigaworks_t40_series_II.jpg?itok=xZyPhSOb 325w, /sites/default/files/styles/max_650x650/public/2019-02/pdt-mhl-gigaworks_t40_series_II.jpg?itok=nywdZ9ky 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/pdt-mhl-gigaworks_t40_series_II.jpg?itok=mC1odJAi 875w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/pdt-mhl-gigaworks_t40_series_II.jpg?itok=xZyPhSOb" alt="Speakers" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><table border="1" bordercolor="#FFCC00" cellpadding="3" cellspacing="3" style="background-color:#FFFF99" width="100%"><tbody><tr><td>Raspberry Pi</td> <td><a href="https://www.amazon.co.uk/gp/product/B07BFH96M3/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B07BFH96M3&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=b1d3ac6bdde3349f017cbc69d659d8e3">3BPLUS-R 1.4 GHz 1 GB RAM 64-Bit</a></td> <td>£34.56</td> </tr><tr><td>DAC</td> <td>IQAudIO PI-DAC+</td> <td>£35.00</td> </tr><tr><td>SD Card</td> <td><a href="https://www.amazon.co.uk/gp/product/B07C5MQCYM/ref=as_li_qf_asin_il_tl?ie=UTF8&amp;tag=upcomingfilms-21&amp;creative=6738&amp;linkCode=as2&amp;creativeASIN=B07C5MQCYM&amp;linkId=f675bbec3439457893e9fe718ac2ea66">LXSINO 32GB Micro SD Class 10</a></td> <td>£7.99</td> </tr><tr><td>Power</td> <td><a href="https://www.amazon.co.uk/gp/product/B01CO1ELT8/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B01CO1ELT8&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=3573c47a2062b46091ab88fd5ddc571f">Official 5V 2.5A Power Adapter for the Raspberry Pi 3</a></td> <td>£7.99</td> </tr><tr><td>Case</td> <td>IQAudIO PI-CASE+</td> <td>£19.40</td> </tr><tr><td>Speakers</td> <td><a href="https://www.amazon.co.uk/gp/product/B001E5PJ56/ref=as_li_qf_asin_il_tl?ie=UTF8&amp;tag=upcomingfilms-21&amp;creative=6738&amp;linkCode=as2&amp;creativeASIN=B001E5PJ56&amp;linkId=71ad1b8e0e3adb9d9e4c78d600175179">Creative Gigaworks T40 Series II</a></td> <td>£79.99</td> </tr><tr><td> </td> <td align="RIGHT"><strong>TOTAL</strong></td> <td>£184.93</td> </tr></tbody></table><p>Of course I can't guarantee you'll get the same deals I got, but the pricing should at least be indicative. I already had the speakers I bought for a different project a few years ago that never saw the light of day, so that reduced my outlay to around £105 for this project. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Fit and test the DAC.</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190206_145604-scaled.jpg?itok=2BfvlsJM 325w, /sites/default/files/styles/max_650x650/public/2019-02/20190206_145604-scaled.jpg?itok=MaWdZ3mU 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190206_145604-scaled.jpg?itok=hs0M1aKf 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190206_145604-scaled.jpg?itok=2BfvlsJM" alt="Piggy Back" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Fitting the DAC to the Raspberry Pi is quite easy although there are instructions on the <a href="http://www.iqaudio.com/downloads/">IQaudIO</a> site. The DAC comes with some spacers and screws which align with the Pi's own. Since this is a quick sound test and I will be building the components into a proper case at some point, I only tightened the screws finger tight. </p> <p>Once this has been completed, it's time to test it works fine. IQaudIO provide downloadable bootable images that can be written to an SD disk and will automatically play mp3 files. The best file on their downloads page is IQ_0_quicktestDigi*.zip.</p> <p>Unfortunately the zip file was corrupted when I tried this. I reported this to their webmaster. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Build the Pi-CASE+</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/20190210_092212-scaled.jpg?itok=x5hG2iYb 325w, /sites/default/files/styles/max_650x650/public/2019-02/20190210_092212-scaled.jpg?itok=g2x-vYrw 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/20190210_092212-scaled.jpg?itok=vNlRrfkq 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/20190210_092212-scaled.jpg?itok=x5hG2iYb" alt="Pi-CASE+" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I couldn't find any build instructions for the case, but it is fairly straightforward. The first point to note is there are some standoffs which should be screwed into the four Raspberry Pi screw holes on the corners of the PCB. The case comprises six perspex sides, and they are slotted to ensure there is only one way it can all fit together - so the second point is it is crucial to dry run this first before it is screwed together to ensure there is no attempt to situate any of the panels the wrong way around.</p> <p>The panels fit together very snuggly without any slack and comes with some small plastic feet to prevent it rubbing and sliding on the chosen surface. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Flash the Rune Audio distro</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2017.35.17.png?itok=uMBZ4nLB 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2017.35.17.png?itok=6a-Ercen 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2017.35.17.png?itok=WNU5k7qd 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2017.35.17.png?itok=NnFRVqEq 1588w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2017.35.17.png?itok=uMBZ4nLB" alt="Etcher" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Time to download our chosen repo. This is a small inconvenience because Rune Audio do not have an official Raspberry Pi 3B+ image. However there is a workaround - a community member has created a rar file download that can be fetched from the <a href="http://www.runeaudio.com/forum/rpi-3-b-t6393-10.html">Rune Audio forums</a>.  </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">The file is in rar format so on a Mac you will need to install unrar <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ brew <span class="kw2">install</span> unrar ==<span class="sy0">&gt;</span> Pouring unrar-5.7.1.mojave.bottle.tar.gz <span class="sy0">/</span>usr<span class="sy0">/</span>local<span class="sy0">/</span>Cellar<span class="sy0">/</span>unrar<span class="sy0">/</span>5.7.1: <span class="nu0">7</span> files, 520.7KB</pre></div></div> Then we can run the unrar command <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ unrar e RuneAudio_0.4_20180629_SC_Pi3B_Plus_v3_tml3nr.rar   UNRAR <span class="nu0">5.70</span> beta <span class="nu0">1</span> freeware Copyright <span class="br0">(</span>c<span class="br0">)</span> <span class="nu0">1993</span>-<span class="nu0">2019</span> Alexander Roshal     Extracting from RuneAudio_0.4_20180629_SC_Pi3B_Plus_v3_tml3nr.rar   Extracting RuneAudio_0.4_20180629_SC_Pi3B+_v3_tml3nr.img OK All OK</pre></div></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>To write the image to the SD card I use Etcher. This is a simple utility app although the command line can be used too with dd - not recommended for those with low experience. Once Etcher has completed it will provide a useful desktop notification. At that point the SD card is ready to be inserted into the Raspberry Pi. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Booting Rune Audio</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot_2019-02-06_at_17_58_33-edited.png?itok=bm80gJOJ 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot_2019-02-06_at_17_58_33-edited.png?itok=N8w5JxVc 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot_2019-02-06_at_17_58_33-edited.png?itok=nVpONhXg 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot_2019-02-06_at_17_58_33-edited.png?itok=bx7fxnQz 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot_2019-02-06_at_17_58_33-edited.png?itok=bm80gJOJ" alt="Boot" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The Pi can be booted once a network cable has been attached. If all goes well, it should be possible to point a web browser at http://runeaudio.local (see screenshot above). We now have the genesis of a music box but it still needs configuring. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">MPD Configuration</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.26.34.png?itok=F_sBi50j 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2019.26.34.png?itok=E8rRGTwm 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2019.26.34.png?itok=Y-A2TzyN 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2019.26.34.png?itok=OG9XYkRA 1462w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.26.34.png?itok=F_sBi50j" alt="MPD" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>There are a number of config screens to work through to get the system functioning correctly. Some of the changes will be specific to my circumstances so your mileage may vary. The MPD (Music Player Daemon) configuration needed one change - the need to select the IQaudIO audio output interface. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Settings configuration</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.30.10.png?itok=uT5_-X_e 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2019.30.10.png?itok=RcP17p3G 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2019.30.10.png?itok=ax6rsT_q 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2019.30.10.png?itok=ocIM9V8K 1402w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.30.10.png?itok=uT5_-X_e" alt="Settings" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>This one in particular may not suit all my readers, but I want to use the Europe/London timezone. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Network settings</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.33.11.png?itok=Io_Rnksz 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2019.33.11.png?itok=ydU38Pta 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2019.33.11.png?itok=-lX_ZUoB 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2019.33.11.png?itok=buqO36XD 1678w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.33.11.png?itok=Io_Rnksz" alt="Network" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.33.58.png?itok=9aL1Nzyh 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2019.33.58.png?itok=VHHh8XWu 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2019.33.58.png?itok=eiokKvW3 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2019.33.58.png?itok=ZzwSSERp 1474w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.33.58.png?itok=9aL1Nzyh" alt="eth0" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>It makes sense to use a static IP address on your home network - so we can use a proxy in order for all devices to find their way to Rune. More of this later, but for now navigate to Network and you should see a sight similar to the first screenshot above. Click on eth0 and you will see the second screenshot. I have populated this form to use 192.168.0.204. My router has already been configured to assign DHCP addresses between 192.168.0.2-&gt;199. Anything 200 and above is reserved for my static IP address home servers. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Sources configuration</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.40.14.png?itok=vh-LJNeC 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2019.40.14.png?itok=AsaIkCY5 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2019.40.14.png?itok=Lsb6fdm_ 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2019.40.14.png?itok=wHwtG758 1674w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2019.40.14.png?itok=vh-LJNeC" alt="Sources" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.41.01-edited.png?itok=nPC3bQBF 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2020.41.01-edited.png?itok=QwF8X_dL 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2020.41.01-edited.png?itok=ec-HD2vy 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2020.41.01-edited.png?itok=Ou6QQABN 1484w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.41.01-edited.png?itok=nPC3bQBF" alt="Mount" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.19.04-edited.png?itok=CxKBa4xm 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2020.19.04-edited.png?itok=WI4z6yG5 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2020.19.04-edited.png?itok=4rT7AtLp 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2020.19.04-edited.png?itok=H7iOZswu 1686w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.19.04-edited.png?itok=CxKBa4xm" alt="Updating" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The final configuration activity is to make Rune Audio aware of where the audio files are stored, and in my case they are on an NFS drive on my NAS server. Go to the Sources page as per the first screenshot above and click on <em>Add New Mount</em>. The form to complete for NFS mounts just wouldn't play ball at all, so in desperation I tried SMB/CIFS(OS X Share) and that worked - see the second screenshot. I called my source <em>flac</em> since this share contains all my high fidelity rips. I then repeated the process by creating a mount for <em>mp3</em> for my low definition audio. The third screenshot shows the two mounts listed, and I have instigated a rebuild of the library, and this is shown by the <em>updating</em> spinner bottom left. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Playback</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.16.55.png?itok=FpT7Lbk5 325w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot%202019-02-06%20at%2020.16.55.png?itok=RL3-kUoT 650w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot%202019-02-06%20at%2020.16.55.png?itok=N3pSRzjt 1300w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot%202019-02-06%20at%2020.16.55.png?itok=RcsuCtrC 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot%202019-02-06%20at%2020.16.55.png?itok=FpT7Lbk5" alt="Playback" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>As soon as the library has been underway for a short period of time and has added a few artists / albums, playback can be tested. I picked an album at random for my end to end test, and it played seamlessly. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Native Android App - Runeaudio Remote Control</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot_20190206-214520_RuneAudio.jpg?itok=hDkSbqbb 183w, /sites/default/files/styles/max_650x650/public/2019-02/Screenshot_20190206-214520_RuneAudio.jpg?itok=WvHY_5d- 366w, /sites/default/files/styles/max_1300x1300/public/2019-02/Screenshot_20190206-214520_RuneAudio.jpg?itok=_f49HEHl 731w, /sites/default/files/styles/max_2600x2600/public/2019-02/Screenshot_20190206-214520_RuneAudio.jpg?itok=kmh-mEND 1080w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2019-02/Screenshot_20190206-214520_RuneAudio.jpg?itok=hDkSbqbb" alt="Native App" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The native app scans the local network and automatically shows all the connected RuneAudio players, allowing you to control them from the same place. It also displays the RuneUI in a true full screen, with no browsers bars and weird scroll behaviours.The app uses the avahi protocol to scan the local network. It detects the connected RuneAudio players which expose the avahi service and lists them, showing also their IP and MAC addresses.</p> <p>The resulting UI is actually the same as a web browser but I found it immensely frustrating. My habit is to tap the built-in Samsung back key to navigate and that immediately drops me out of the app. Restarting the app requires the initial network discovery to be run again, and mine steadfastly refused to connect initially, but then suddenly appeared to figure out my network and was rock steady afterwards. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Resolving runeaudio.local - Desktop</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Rune Audio uses the Avahi protocol which theoretically means when added to a local network, pointing a browser to http://runeaudio.local should 'just work'. Whilst setting up Rune Audio I noticed this capability was very hit and miss. There are ways of getting a rock solid configuration - for a desktop the simple solution would be to add runeadio.local to the <em>/etc/hosts</em> file. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><strong>/etc/hosts</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">192.168.0.204 runeaudio.local</pre></div></div> Obviously the IP address should be swapped with whichever you have used.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Resolving runeaudio.local - Handheld Device</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Since /etc/hosts can't be edited on a handheld device such as a tablet or a smartphone, we need another solution. The workaround for this is to use a proxy server as per <a href="/raspberry-pi-squid-proxy-server-testing-hand-held-devices-when-developing-sandbox">my blog where I utilised another Raspberry Pi as a proxy.</a> Firstly log into the proxy server and edit its /etc/hosts like the desktop so you will have an entry <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">192.168.0.204 runeaudio.local</pre></div></div> The Squid proxy will then need a reboot. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co4">$ </span><span class="kw2">sudo</span> service squid3 restart</pre></div></div> You will then have to ensure your handheld device is using the proxy for internet access. Screenshots for achieving this on an iPad and an Android phone are in my other blog just mentioned and are not duplicated here for brevity. Just click though if you need guidance.</div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/hardware" hreflang="en">Hardware</a></span> <span class="field--item"><a href="/raspberrypi" hreflang="en">Raspberry Pi</a></span> </span> </div> Wed, 06 Feb 2019 10:45:32 +0000 nigel 165 at http://badzilla.co.uk Drupal 8 as a Static Site: Contact Form Architecture http://badzilla.co.uk/drupal-8-static-site-contact-form-architecture <span>Drupal 8 as a Static Site: Contact Form Architecture</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Wed, 26/12/2018 - 11:06</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>As I have already mentioned in this sequence of blogs, the intention is to use AWS products for my static site contact form. It's worthwhile explaining the proposed architecture and steps involved to implement such a solution. On the face of it, it may appear somewhat convoluted, but AWS provide a number of complementary products to achieve what we want, and the configuration can be saved in code so it is easy to reproduce. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Architecture</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-12/Form%20Submission%20%281%29.jpg?itok=10FV-4E6 325w, /sites/default/files/styles/max_650x650/public/2018-12/Form%20Submission%20%281%29.jpg?itok=KWSN4VeQ 650w, /sites/default/files/styles/max_1300x1300/public/2018-12/Form%20Submission%20%281%29.jpg?itok=bO9_BzQz 960w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-12/Form%20Submission%20%281%29.jpg?itok=10FV-4E6" alt="Architecture Diagram" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The above diagram shows the architecture of our contact form, and the flow through the form ecosystem. Let's have a look at the components. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Webpage with Contact Form</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Drupal comes with a contact form in core, and therefore this is fairly straightforward, albeit with a couple of hurdles to jump. Firstly I would like to place my contact form on my <a href="/about">About Me</a> page underneath my existing bio. To achieve this I need the form to be a block which will require the installation of a contrib module. In addition I want my form's action to POST the form data to an external URL which isn't supported in the core form - so that will need a solution too. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">AWS API Gateway</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The form will be submitted for processing to AWS since we don't have a backend on our own static site - hence why the form needs an external URL for its action parameter. </p> <p>We will use AWS API Gateway which is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. This will give us access to AWS compute backend services to process our POSTed form. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">AWS Lambda Function</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>AWS Lambda is a FaaS (Function as a Service) compute service. It is excellent at running a snippet of code on demand once it's been triggered, for instance in our case by the contact form being POSTed.</p> <p>PHP isn't a supported Lambda language, but there are workarounds for this - as explained in my sequence of blogs <a href="/real-world-php-lambda-app">here</a>. Using these workarounds, we will develop a PHP app to process the form, and in addition we will need the AWS PHP SDK to interface with AWS SES (Simple Email Service). </p> <p>The PHP code will reside in Amazon's S3 object storage, and we will use the <a href="https://serverless.com/">serverless</a> product along with the aws console to manage the deploy process. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">AWS SES</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>AWS SES is a very cost effective method of sending marketing, notification and transactional emails. For the amount of usage a blog contact form would see, it's highly unlikely to go above the AWS free tier threshold. It is the perfect tool for sending the contact form contents onto my personal email account. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Inbox</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The objective is the form contents should arrive into my Gmail inbox for me to read and action if necessary. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Thank you page</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The last activity of the Lambda function is to redirect the submitted form back to the website's thank you page which will have to be constructed as part of this exercise. The redirect will be a 307 to preserve the POSTed parameters that can be parsed and injected into the thank you message with a few lines of JavaScript.</p></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> <span class="field--item"><a href="/lambda" hreflang="en">lambda</a></span> </span> </div> Wed, 26 Dec 2018 11:06:18 +0000 nigel 162 at http://badzilla.co.uk Drupal 8 as a Static Site: JavaScript Search Client http://badzilla.co.uk/drupal-8-static-site-javascript-search-client <span>Drupal 8 as a Static Site: JavaScript Search Client</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Sat, 15/12/2018 - 10:19</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>We now need to plug the Elasticsearch query that was built in <a href="/drupal-8-static-site-elasticsearch-query">the previous blog</a> into a JavaScript client that has been loaded on the search page as described in another <a href="/drupal-8-static-site-search-architecture-and-drupal-configuration">earlier blog</a>. So a few lines of JavaScript will be required here, and it will need to work on my three environments:</p> <ol><li>Sandbox Drupal site connecting to local Elasticsearch server.</li> <li>Sandbox static site connecting to local Elasticsearch server.</li> <li>Production static site connecting to production Elasticsearch server. </li> </ol><p>The JavaScript will use the elasticsearch.js API library which can be downloaded from <a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">here</a>. We need to make our theme aware of our JavaScript files and we only wanted them loading on the node search page route. Our JavaScript file will be called beezee_elastic.js, which is the name of my theme (beezee8) and elastic obviously.  </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Add the JavaScript to the Beezee theme</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Firstly we need an entry in our theme's info.yml file under the Libraries heading. <br /><strong>beezee8.info.yml</strong><br /><div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">libraries: - <span class="st_h">'beezee8/elastic-library'</span></pre></div></div> Next we need to append to the libraries yml file the location of our JavaScript files - the Elastic API library and our own personal script. <br /><strong>beezee8.libraries.yml</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">elastic-library: js: js<span class="sy0">/</span>elasticsearch-js<span class="sy0">/</span>elasticsearch.min.js: <span class="br0">{</span><span class="br0">}</span> js<span class="sy0">/</span>beezee<span class="sy0">/</span>beezee_elastic.js: <span class="br0">{</span><span class="br0">}</span> </pre></div></div> Obviously we then have to copy our JS files into the filesystem at those locations - for now I will have a placeholder empty file for beezee_elastic.js which we will flesh out later.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Load the JavaScript files on the search page only</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">There are a few ways of doing this, but my favourite is to use a preprocessor function in the {themename}.theme file. We can use hook_preprocess_page for this.<br /><strong>beezee8.theme</strong> <div class="codeblock geshifilter"><code><span style="color: #000000"><span style="color: #0000BB">&lt;?php<br /></span><span style="color: #FF8000">/**<br /> * @param $variables<br /> */<br /></span><span style="color: #007700">function </span><span style="color: #0000BB">beezee8_preprocess_page</span><span style="color: #007700">(&amp;</span><span style="color: #0000BB">$variables</span><span style="color: #007700">) {<br /><br />    </span><span style="color: #FF8000">// If we are on the search page, load the JS search client api<br />    // and our implementation to Elasticsearch<br />    </span><span style="color: #007700">if (\</span><span style="color: #0000BB">Drupal</span><span style="color: #007700">::</span><span style="color: #0000BB">routeMatch</span><span style="color: #007700">()-&gt;</span><span style="color: #0000BB">getRouteName</span><span style="color: #007700">() == </span><span style="color: #DD0000">'search.view_node_search'</span><span style="color: #007700">) {<br />        </span><span style="color: #0000BB">$variables</span><span style="color: #007700">[</span><span style="color: #DD0000">'#attached'</span><span style="color: #007700">][</span><span style="color: #DD0000">'library'</span><span style="color: #007700">][] = </span><span style="color: #DD0000">'beezee8/elastic-library'</span><span style="color: #007700">;<br />    }<br />}<br /></span><span style="color: #0000BB">?&gt;</span></span></code></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">The JavaScript search client</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Ok so finally we are ready to build out our front end search client. Here we go!<br /><strong>beezee_elastic.js</strong> <div class="geshifilter"><div class="javascript geshifilter-javascript"><pre class="de1"><span class="br0">(</span><span class="kw1">function</span> <span class="br0">(</span>$<span class="sy0">,</span> Drupal<span class="br0">)</span> <span class="br0">{</span>   <span class="kw1">var</span> elastic_once<span class="sy0">;</span>   <span class="kw1">function</span> BeezeeElastic<span class="br0">(</span><span class="br0">)</span> <span class="br0">{</span> <span class="kw1">if</span> <span class="br0">(</span><span class="sy0">!</span>elastic_once<span class="br0">)</span> <span class="br0">{</span> elastic_once <span class="sy0">=</span> <span class="kw2">true</span><span class="sy0">;</span>   <span class="co1">// Get the params from the url</span> <span class="kw1">var</span> pageURL <span class="sy0">=</span> window.<span class="me1">location</span>.<span class="me1">search</span>.<span class="me1">substring</span><span class="br0">(</span><span class="nu0">1</span><span class="br0">)</span><span class="sy0">;</span> <span class="kw1">var</span> URLVariables <span class="sy0">=</span> pageURL.<span class="me1">split</span><span class="br0">(</span><span class="st0">'&amp;'</span><span class="br0">)</span><span class="sy0">;</span> <span class="kw1">var</span> keys<span class="sy0">;</span> i <span class="sy0">=</span> <span class="nu0">0</span><span class="sy0">;</span> while <span class="br0">(</span>i <span class="sy0">&lt;</span> URLVariables.<span class="me1">length</span><span class="br0">)</span> <span class="br0">{</span> <span class="kw1">var</span> ParameterName <span class="sy0">=</span> URLVariables<span class="br0">[</span>i<span class="br0">]</span>.<span class="me1">split</span><span class="br0">(</span><span class="st0">'='</span><span class="br0">)</span><span class="sy0">;</span> <span class="kw1">if</span> <span class="br0">(</span>ParameterName<span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span> <span class="sy0">==</span> <span class="st0">'keys'</span><span class="br0">)</span> <span class="br0">{</span> keys <span class="sy0">=</span> ParameterName<span class="br0">[</span><span class="nu0">1</span><span class="br0">]</span><span class="sy0">;</span> <span class="br0">}</span> i<span class="sy0">++;</span> <span class="br0">}</span>   <span class="co1">// Did we get anything from the URL? If not bail, otherwise do the search.</span> <span class="co1">// We shouldn't get to here so don't show a message to user.</span> <span class="kw1">if</span> <span class="br0">(</span><span class="sy0">!</span>keys<span class="br0">)</span> <span class="br0">{</span> console.<span class="me1">log</span><span class="br0">(</span><span class="st0">'Invalid keys'</span><span class="br0">)</span><span class="sy0">;</span> <span class="kw1">return</span><span class="sy0">;</span> <span class="br0">}</span>   <span class="co1">// Add the search key to the h1 selector</span> $<span class="br0">(</span><span class="st0">"h1"</span><span class="br0">)</span>.<span class="me1">append</span><span class="br0">(</span>Drupal.<span class="me1">t</span><span class="br0">(</span><span class="st0">" for "</span><span class="br0">)</span> <span class="sy0">+</span> keys<span class="br0">)</span><span class="sy0">;</span>   <span class="co1">// Endpoint defined during instantiation of API Client</span> <span class="kw1">var</span> client <span class="sy0">=</span> <span class="kw1">new</span> elasticsearch.<span class="me1">Client</span><span class="br0">(</span><span class="br0">{</span> host<span class="sy0">:</span> <span class="st0">'http://meedjum.test:9200/'</span> <span class="coMULTI">/*,*/</span> <span class="coMULTI">/* log: 'trace' */</span> <span class="br0">}</span><span class="br0">)</span><span class="sy0">;</span>   client.<span class="me1">search</span><span class="br0">(</span><span class="br0">{</span> index<span class="sy0">:</span> <span class="st0">'elasticsearch_index_badzilla_meedjum_staticcontent'</span><span class="sy0">,</span> body<span class="sy0">:</span> <span class="br0">{</span> query<span class="sy0">:</span> <span class="br0">{</span> function_score<span class="sy0">:</span> <span class="br0">{</span> query<span class="sy0">:</span> <span class="br0">{</span> query_string<span class="sy0">:</span> <span class="br0">{</span> query<span class="sy0">:</span> keys<span class="sy0">,</span> fields<span class="sy0">:</span> <span class="br0">[</span> <span class="st0">"title"</span><span class="sy0">,</span> <span class="st0">"*body"</span><span class="sy0">,</span> <span class="st0">"term"</span> <span class="br0">]</span><span class="sy0">,</span> default_operator<span class="sy0">:</span> <span class="st0">"OR"</span> <span class="br0">}</span> <span class="br0">}</span><span class="sy0">,</span> functions<span class="sy0">:</span> <span class="br0">[</span> <span class="br0">{</span> linear<span class="sy0">:</span> <span class="br0">{</span> <span class="st0">"created"</span><span class="sy0">:</span> <span class="br0">{</span> origin<span class="sy0">:</span> <span class="st0">"now"</span><span class="sy0">,</span> offset<span class="sy0">:</span> <span class="st0">"365d"</span><span class="sy0">,</span> scale<span class="sy0">:</span> <span class="st0">"1460d"</span><span class="sy0">,</span> decay<span class="sy0">:</span> <span class="nu0">0.5</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">]</span> <span class="br0">}</span> <span class="br0">}</span><span class="sy0">,</span> highlight<span class="sy0">:</span> <span class="br0">{</span> number_of_fragments<span class="sy0">:</span> <span class="nu0">1</span><span class="sy0">,</span> pre_tags<span class="sy0">:</span> <span class="br0">[</span><span class="st0">"&lt;strong&gt;"</span><span class="br0">]</span><span class="sy0">,</span> post_tags<span class="sy0">:</span> <span class="br0">[</span><span class="st0">"&lt;/strong&gt;"</span><span class="br0">]</span><span class="sy0">,</span> fragment_size<span class="sy0">:</span> <span class="nu0">400</span><span class="sy0">,</span> no_match_size<span class="sy0">:</span> <span class="nu0">400</span><span class="sy0">,</span> phrase_limit<span class="sy0">:</span> <span class="nu0">1</span><span class="sy0">,</span> fields<span class="sy0">:</span> <span class="br0">{</span> <span class="st0">"*body"</span><span class="sy0">:</span> <span class="br0">{</span><span class="br0">}</span><span class="sy0">,</span> <span class="st0">"title"</span><span class="sy0">:</span> <span class="br0">{</span><span class="br0">}</span><span class="sy0">,</span> <span class="st0">"term"</span><span class="sy0">:</span> <span class="br0">{</span><span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span><span class="sy0">,</span> size<span class="sy0">:</span> <span class="nu0">10</span> <span class="br0">}</span> <span class="br0">}</span><span class="sy0">,</span> response<span class="br0">)</span><span class="sy0">;</span>   <span class="coMULTI">/* Process the response from Elastic */</span> <span class="kw1">function</span> response<span class="br0">(</span>err<span class="sy0">,</span> resp<span class="sy0">,</span> status<span class="br0">)</span> <span class="br0">{</span>   <span class="co1">// Are we logged in? If so our jQuery calls will be slightly different due to additional DOM stuff</span> <span class="co1">// for non anonymous</span> <span class="co1">// These are our targets for injecting the content on the page. Markup is Bootstrap 3</span> <span class="kw1">var</span> h1_find<span class="sy0">;</span> <span class="kw1">var</span> h2_find<span class="sy0">;</span> <span class="kw1">if</span> <span class="br0">(</span>$<span class="br0">(</span><span class="st0">".user-logged-in"</span><span class="br0">)</span>.<span class="me1">length</span><span class="br0">)</span> <span class="br0">{</span> h1_find <span class="sy0">=</span> <span class="st0">"h1 + nav:last-child"</span><span class="sy0">;</span> h2_find <span class="sy0">=</span> <span class="st0">"h1 + nav + h2:last-child"</span><span class="sy0">;</span> <span class="br0">}</span> <span class="kw1">else</span> <span class="br0">{</span> h1_find <span class="sy0">=</span> <span class="st0">"h1"</span><span class="sy0">;</span> h2_find <span class="sy0">=</span> <span class="st0">"h1 + h2:last-child"</span><span class="sy0">;</span> <span class="br0">}</span>   <span class="kw1">var</span> hits <span class="sy0">=</span> resp.<span class="me1">hits</span>.<span class="me1">hits</span><span class="sy0">;</span>   <span class="kw1">if</span> <span class="br0">(</span>resp.<span class="me1">hits</span>.<span class="me1">total</span> <span class="sy0">==</span> <span class="nu0">0</span><span class="br0">)</span> <span class="br0">{</span> $<span class="br0">(</span>h1_find<span class="br0">)</span>.<span class="me1">after</span><span class="br0">(</span><span class="st0">"&lt;h3&gt;"</span> <span class="sy0">+</span> Drupal.<span class="me1">t</span><span class="br0">(</span><span class="st0">"Your search yielded no results."</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">"&lt;/h3&gt;"</span><span class="br0">)</span><span class="sy0">;</span> <span class="br0">}</span> <span class="kw1">else</span> <span class="br0">{</span> $<span class="br0">(</span>h1_find<span class="br0">)</span>.<span class="me1">after</span><span class="br0">(</span><span class="st0">"&lt;h2&gt;"</span> <span class="sy0">+</span> Drupal.<span class="me1">t</span><span class="br0">(</span><span class="st0">"Search results"</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">"&lt;/h2&gt;"</span><span class="br0">)</span><span class="sy0">;</span>   <span class="kw1">var</span> inject <span class="sy0">=</span> <span class="st0">"&lt;ol&gt;"</span><span class="sy0">;</span> $.<span class="me1">each</span><span class="br0">(</span>hits<span class="sy0">,</span> <span class="kw1">function</span> <span class="br0">(</span>key<span class="sy0">,</span> value<span class="br0">)</span> <span class="br0">{</span>   <span class="co1">// Url, title and Body</span> <span class="kw1">var</span> prefix <span class="sy0">=</span> <span class="st0">"&lt;li&gt;&lt;h3&gt;"</span><span class="sy0">;</span> <span class="kw1">var</span> suffix <span class="sy0">=</span> <span class="st0">"&lt;/li&gt;"</span><span class="sy0">;</span> <span class="kw1">var</span> title <span class="sy0">=</span> <span class="st0">"&lt;a href=<span class="es0">\"</span>"</span> <span class="sy0">+</span> value._source.<span class="me1">url</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span> <span class="sy0">+</span> <span class="st0">"<span class="es0">\"</span>&gt;"</span> <span class="sy0">+</span> value.<span class="me1">highlight</span>.<span class="me1">title</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span> <span class="sy0">+</span> <span class="st0">"&lt;/a&gt;&lt;/h3&gt;"</span><span class="sy0">;</span>   <span class="co1">// do we have a book body or a paragraph body?</span> <span class="kw1">var</span> body<span class="sy0">;</span> <span class="kw1">if</span> <span class="br0">(</span><span class="kw1">typeof</span> value.<span class="me1">highlight</span>.<span class="me1">blog_body</span> <span class="sy0">!=</span> <span class="st0">'undefined'</span><span class="br0">)</span> <span class="br0">{</span> body <span class="sy0">=</span> value.<span class="me1">highlight</span>.<span class="me1">blog_body</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span><span class="sy0">;</span> <span class="br0">}</span> <span class="kw1">else</span> <span class="br0">{</span> body <span class="sy0">=</span> value.<span class="me1">highlight</span>.<span class="me1">body</span><span class="sy0">;</span> <span class="br0">}</span> body <span class="sy0">=</span> <span class="st0">"&lt;p&gt;"</span> <span class="sy0">+</span> body <span class="sy0">+</span> <span class="st0">"&lt;/p&gt;"</span><span class="sy0">;</span>   inject <span class="sy0">+=</span> prefix <span class="sy0">+</span> title <span class="sy0">+</span> body <span class="sy0">+</span> suffix<span class="sy0">;</span>   <span class="co1">// Add the tag</span> <span class="kw1">if</span> <span class="br0">(</span><span class="kw1">typeof</span> value.<span class="me1">highlight</span>.<span class="me1">term</span> <span class="sy0">!=</span> <span class="st0">'undefined'</span><span class="br0">)</span> <span class="br0">{</span> inject <span class="sy0">+=</span> <span class="st0">"&lt;p&gt;"</span> <span class="sy0">+</span> Drupal.<span class="me1">t</span><span class="br0">(</span><span class="st0">"Tag:"</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">" "</span> <span class="sy0">+</span> value.<span class="me1">highlight</span>.<span class="me1">term</span> <span class="sy0">+</span> <span class="st0">"&lt;/p&gt;"</span><span class="sy0">;</span> <span class="br0">}</span>   <span class="co1">// Author and date</span> <span class="kw1">var</span> gb_obj <span class="sy0">=</span> <span class="kw1">new</span> <span class="kw4">Date</span><span class="br0">(</span>value._source.<span class="me1">created</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span> <span class="sy0">*</span> <span class="nu0">1000</span><span class="br0">)</span><span class="sy0">;</span> <span class="kw1">var</span> gb_date <span class="sy0">=</span> <span class="br0">(</span><span class="st0">'0'</span> <span class="sy0">+</span> gb_obj.<span class="me1">getDate</span><span class="br0">(</span><span class="br0">)</span><span class="br0">)</span>.<span class="me1">slice</span><span class="br0">(</span><span class="sy0">-</span><span class="nu0">2</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">"/"</span> <span class="sy0">+</span> <span class="br0">(</span><span class="st0">'0'</span> <span class="sy0">+</span> <span class="br0">(</span>gb_obj.<span class="me1">getMonth</span><span class="br0">(</span><span class="br0">)</span> <span class="sy0">+</span> <span class="nu0">1</span><span class="br0">)</span><span class="br0">)</span>.<span class="me1">slice</span><span class="br0">(</span><span class="sy0">-</span><span class="nu0">2</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">"/"</span> <span class="sy0">+</span> gb_obj.<span class="me1">getFullYear</span><span class="br0">(</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">" "</span> <span class="sy0">+</span> <span class="st0">"-"</span> <span class="sy0">+</span> <span class="st0">"&amp;nbsp"</span> <span class="sy0">+</span> gb_obj.<span class="me1">getHours</span><span class="br0">(</span><span class="br0">)</span> <span class="sy0">+</span> <span class="st0">":"</span> <span class="sy0">+</span> gb_obj.<span class="me1">getMinutes</span><span class="br0">(</span><span class="br0">)</span><span class="sy0">;</span>   inject <span class="sy0">+=</span> <span class="st0">"&lt;p&gt;"</span> <span class="sy0">+</span> value._source.<span class="me1">author</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span> <span class="sy0">+</span> <span class="st0">" "</span> <span class="sy0">+</span> <span class="st0">"-"</span> <span class="sy0">+</span> <span class="st0">" "</span> <span class="sy0">+</span> gb_date<span class="sy0">;</span>   <span class="br0">}</span><span class="br0">)</span><span class="sy0">;</span> inject <span class="sy0">+=</span> <span class="st0">"&lt;/ol&gt;"</span><span class="sy0">;</span> $<span class="br0">(</span>h2_find<span class="br0">)</span>.<span class="me1">after</span><span class="br0">(</span>inject<span class="br0">)</span><span class="sy0">;</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span>     Drupal.<span class="me1">behaviors</span>.<span class="me1">beezee_elastic</span> <span class="sy0">=</span> <span class="br0">{</span> attach<span class="sy0">:</span> <span class="kw1">function</span> <span class="br0">(</span>context<span class="sy0">,</span> settings<span class="br0">)</span> <span class="br0">{</span> BeezeeElastic<span class="br0">(</span><span class="br0">)</span><span class="sy0">;</span> <span class="br0">}</span> <span class="br0">}</span><span class="sy0">;</span> <span class="br0">}</span><span class="br0">)</span><span class="br0">(</span>jQuery<span class="sy0">,</span> Drupal<span class="br0">)</span><span class="sy0">;</span></pre></div></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Code commentary</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I'd like to think the code is self-explanatory, but here are a few words for additional clarification. </p> <p>The first block of code is processing the URL params. We are expecting the <em>?keys={search_phrase}</em>. The search phrase needs to be injected into the Elasticsearch query later. </p> <p>The <em>client</em> object is returned from me instantiating the Elasticsearch API. I needed a parameter of the Elasticsearch endpoint which is currently hardcoded. It will work for my two sandbox environments (Drupal and static) but not for production. I will have to come up with a solution to rewrite the endpoint during the deployment process!</p> <p>The <em>client.search</em> call is our query created earlier, and I have added the search term from the URL. The Elasticsearch API provides two mechanisms for this call - it can return a promise or invoke a callback. I opted for the latter. </p> <p>The <em>response</em> function is the callback and it handles the Elastic response. My first task is to set the selectors where I will inject the results - note that the DOM is different whether I am logged into my sandbox Drupal or not - so my injection anchor points are different dependent upon this circumstance. </p> <p>I add the title, body text, tag, author and published date to my output, concatenating them together, and then inject them into the content area of the screen around ordered list tags. </p> <p>The next result is very similar visually to the default Drupal core search with the addition of my blog tags, although of course it can be styled differently if wanted. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">The results</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-12/Screenshot%202018-12-15%20at%2015.04.52.png?itok=ZnDNHXFC 325w, /sites/default/files/styles/max_650x650/public/2018-12/Screenshot%202018-12-15%20at%2015.04.52.png?itok=w9IqdciG 650w, /sites/default/files/styles/max_1300x1300/public/2018-12/Screenshot%202018-12-15%20at%2015.04.52.png?itok=WNHqHJYw 1300w, /sites/default/files/styles/max_2600x2600/public/2018-12/Screenshot%202018-12-15%20at%2015.04.52.png?itok=XruPF1MZ 1766w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-12/Screenshot%202018-12-15%20at%2015.04.52.png?itok=ZnDNHXFC" alt="results" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The script works well on both the Drupal and static sites in sandbox. The screenshot above shows a search on the word <em>nginx</em>. At the moment I haven't built in a paginator, but that could come along later. </p></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> <span class="field--item"><a href="/js" hreflang="en">JavaScript</a></span> </span> </div> Sat, 15 Dec 2018 10:19:04 +0000 nigel 161 at http://badzilla.co.uk Drupal 8 as a Static Site: Elasticsearch Query http://badzilla.co.uk/drupal-8-static-site-elasticsearch-query <span>Drupal 8 as a Static Site: Elasticsearch Query</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Wed, 28/11/2018 - 14:35</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The most fundamental part of the search is the query - this will determine which documents will be retrieved after a search. It must obviously reflect the search phrase input into the search form, and the results returned should be presented in an order of most relevant first. </p> <p>Elasticsearch is excellent at this - but it's crucial that the query reflects exactly what I want to achieve and it searches the correct fields. No two sites are the same and therefore no two search requirements are identical. I have decided that I want to search on the following:</p> <ol><li>Blog title</li> <li>Blog body text (a custom defined Paragraph type)</li> <li>Body text (only used with the book content type and not my regular blogs)</li> <li>Technology vocabulary which I use to tag my blog</li> </ol><p>Each of these will be assigned a score during the search process and the results delivered back in high to low score order.</p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Initial basic query</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Unless you re an expert in Elastsearch queries (I am definitely not that), you will need a useful tool with automatic syntax suggestions, and lo and behold Kibana is excellent at this. My first stab at the query is shown below, and it behaved pretty much to my expectations <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">GET <span class="sy0">/</span>myindex<span class="sy0">/</span>_search <span class="br0">{</span> <span class="st0">"query"</span>: <span class="br0">{</span> <span class="st0">"query_string"</span>: <span class="br0">{</span> <span class="st0">"query"</span>: <span class="st0">"Search phrase here"</span>, <span class="st0">"fields"</span>: <span class="br0">[</span> <span class="st0">"title"</span>, <span class="st0">"*body"</span>, <span class="st0">"term"</span> <span class="br0">]</span>, <span class="st0">"default_operator"</span>: <span class="st0">"or"</span> <span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"highlight"</span>: <span class="br0">{</span> <span class="st0">"number_of_fragments"</span> : <span class="nu0">1</span>, <span class="st0">"pre_tags"</span>: <span class="br0">[</span> <span class="st0">"&lt;strong&gt;"</span> <span class="br0">]</span>, <span class="st0">"post_tags"</span>: <span class="br0">[</span> <span class="st0">"&lt;/strong&gt;"</span> <span class="br0">]</span>, <span class="st0">"fragment_size"</span>: <span class="nu0">400</span>, <span class="st0">"no_match_size"</span>: <span class="nu0">400</span>, <span class="st0">"phrase_limit"</span>: <span class="nu0">1</span>, <span class="st0">"fields"</span> : <span class="br0">{</span> <span class="st0">"*body"</span>: <span class="br0">{</span><span class="br0">}</span>, <span class="st0">"title"</span>: <span class="br0">{</span><span class="br0">}</span>, <span class="st0">"term"</span>: <span class="br0">{</span><span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"size"</span>: <span class="nu0">10</span> <span class="br0">}</span></pre></div></div> Some of this is self-explanatory, some less so. Let's cherry pick our explanations.<br /><strong>*body</strong> - I have two fields which contain the word body and these can be searched using a wildcard.<br /><strong>highlight</strong> - This is where I specify what I actually want to display back to the end user. The following parameters defined this. <br /><strong>number_of_fragments</strong> - The maximum number of fragments to return.<br /><strong>pre_tags</strong> and <strong>post_tags</strong> - When the search term is found, we can specify what tags we want before and after. I chose <strong>strong</strong> instead of the default <strong>em</strong><br /><strong>fragment_size</strong> - How many characters the snipped of the find will contain as a maximum, although it could be less depending upon how Elasticsearch's paragraph and line break algorithm interprets the result<br /><strong>no_match_size</strong> - As above if there are no matches in a particular field. I still want to show the snippet so the user has got some context.<br /><strong>phrase_limit</strong> - Controls the number of matching phrases in a document that are considered.<br /><br /> The major shortcoming of this query is it shows no favouritism towards the date the blog is published. Whether it was published yesterday or ten years ago, the score will be same.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Adding a date decay to favour more recent blogs</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/DateDecay.png?itok=hlw5y4c6 325w, /sites/default/files/styles/max_650x650/public/2018-11/DateDecay.png?itok=g_CEfRV0 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/DateDecay.png?itok=9dtA-6wg 813w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/DateDecay.png?itok=hlw5y4c6" alt="Elasticsearch date decay" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Technical blogs are ephemeral and transitory. They tend to have a shelf life when they are very popular, but then their popularity will wane as time goes by. This can be due to a number of factors - such as the popularity of the subject of the blog, the technology written about evolving thus obsoleting the origin blog, and the subject of the blog becoming commonplace or covered more extensively in official documentation.</p> <p>Therefore we need a mechanism to favour newer blogs in our search that will be more relevant to our users. Elasticsearch has the <strong>decay</strong> feature, and it supports three different mathematical formula. There is <em>gauss</em>, <em>linear</em> and <em>exp</em>. I had a think here - I could use the default Gaussian approach and with some subtle parameter settings I think it would have done a good job. I could also have used Exponential, with the relatively shallow curve to present the passing of time. In the end I decided I liked a linear representation. </p> <p>My rationale is this: A blog will be totally relevant for about a year, then it will decay at a linear rate for a number of years, then it will hit the bottom. Sheer speculation drove me to use a total decay period of eight years although I concede this is somewhat arbitrary and probably sub-optimal. </p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><br />The syntax for representing this is a little arcane. Here we go: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="st0">"function_score"</span>: <span class="br0">{</span> <span class="st0">"functions"</span>: <span class="br0">[</span> <span class="br0">{</span> <span class="st0">"linear"</span>: <span class="br0">{</span> <span class="st0">"created"</span>: <span class="br0">{</span> <span class="st0">"origin"</span>: <span class="st0">"now"</span>, <span class="st0">"offset"</span>: <span class="st0">"365d"</span>, <span class="st0">"scale"</span>: <span class="st0">"1460d"</span>, <span class="st0">"decay"</span>: <span class="nu0">0.5</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">]</span> <span class="br0">}</span></pre></div></div> Here is the commentary:<br /><strong>created</strong> - This is the name of the date field for the date the content was published<br /><strong>origin</strong> - The start point for our time span, which will always be anchored at the current date.<br /><strong>offset</strong> - This is the initial flat spot at the top of the graph above. I am saying here - all documents in the first year will be scored equally from a date created perspective.<br /><strong>scale</strong> - Defines the distance from origin + offset at which the computed score will equal decay parameter. <br /><strong>decay</strong> - The decay parameter defines how documents are scored at the distance given at scale. So if you divide decay into scale you get the figure of 8 years.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">The complete query</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Adding the decay to the initial query means that they <strong>multiplied together</strong> (inferred by default). <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">GET <span class="sy0">/</span>myindex<span class="sy0">/</span>_search <span class="br0">{</span>   <span class="st0">"query"</span>: <span class="br0">{</span> <span class="st0">"function_score"</span>: <span class="br0">{</span> <span class="st0">"query"</span>: <span class="br0">{</span> <span class="st0">"query_string"</span>: <span class="br0">{</span> <span class="st0">"query"</span>: <span class="st0">"Search phrase here"</span>, <span class="st0">"fields"</span>: <span class="br0">[</span> <span class="st0">"title"</span>, <span class="st0">"*body"</span>, <span class="st0">"term"</span> <span class="br0">]</span>, <span class="st0">"default_operator"</span>: <span class="st0">"OR"</span> <span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"functions"</span>: <span class="br0">[</span> <span class="br0">{</span> <span class="st0">"linear"</span>: <span class="br0">{</span> <span class="st0">"created"</span>: <span class="br0">{</span> <span class="st0">"origin"</span>: <span class="st0">"now"</span>, <span class="st0">"offset"</span>: <span class="st0">"365d"</span>, <span class="st0">"scale"</span>: <span class="st0">"1460d"</span>, <span class="st0">"decay"</span>: <span class="nu0">0.5</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span> <span class="br0">]</span> <span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"highlight"</span>: <span class="br0">{</span> <span class="st0">"number_of_fragments"</span> : <span class="nu0">1</span>, <span class="st0">"pre_tags"</span>: <span class="br0">[</span> <span class="st0">"&lt;strong&gt;"</span> <span class="br0">]</span>, <span class="st0">"post_tags"</span>: <span class="br0">[</span> <span class="st0">"&lt;/strong&gt;"</span> <span class="br0">]</span>, <span class="st0">"fragment_size"</span>: <span class="nu0">400</span>, <span class="st0">"no_match_size"</span>: <span class="nu0">400</span>, <span class="st0">"phrase_limit"</span>: <span class="nu0">1</span>, <span class="st0">"fields"</span> : <span class="br0">{</span> <span class="st0">"*body"</span>: <span class="br0">{</span><span class="br0">}</span>, <span class="st0">"title"</span>: <span class="br0">{</span><span class="br0">}</span>, <span class="st0">"term"</span>: <span class="br0">{</span><span class="br0">}</span> <span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"size"</span>: <span class="nu0">10</span> <span class="br0">}</span></pre></div></div></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> </span> </div> Wed, 28 Nov 2018 14:35:57 +0000 nigel 160 at http://badzilla.co.uk Drupal 8 as a Static Site: Disable Core Search http://badzilla.co.uk/drupal-8-static-site-disable-core-search <span>Drupal 8 as a Static Site: Disable Core Search</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Sun, 18/11/2018 - 10:04</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Since the static site will not have access to the Drupal 8 Core Search facility, it can be removed by using the command <em>drush pm_uninstall search</em> on the command line, or by visiting <em>admin/modules/uninstall.</em></p> <p>The problem with that approach is core search also has the search form that appears as a block in the navbar on every page of my website. If I uninstall core search I lose the form at the same time. It is my desire to keep on using the search form despite having an Elasticsearch backend. The proposed Elasticsearch JavaScript client will be installed on the /search route which is where the core search form action is directed to. </p> <p>Okay, so feasibly I could uninstall the core search and do either:</p> <ol><li>Create a custom block with a search form that replicates (from an HTML frontend perspective) the core search form.</li> <li>Copy the generated markup from the core search form, and paste it into the page template.</li> </ol><p>I don't like either approach. I would prefer to keep the core search and stub it so it doesn't actually do any backend searching. That way I will have near identical functionality for both my sandbox static and sandbox Drupal sites. </p> <p>To stub the search I would have to alter the default search route and replace it with my own custom route with a controller that returns nothing.</p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Define the route subscriber config</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">The first step is to add to our custom module's services file.<br /><strong>badzilla_static.services.yml</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"> <span class="sy0">&lt;</span>-- previous entry snipped --<span class="sy0">&gt;</span> badzilla.route_subscriber: class: Drupal\badzilla_static\Routing\RouteSubscriber tags: - <span class="br0">{</span> name: event_subscriber <span class="br0">}</span></pre></div></div> We have now defined where our route subscriber lives. Let's create.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Route subscriber class</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">We need to create the directory structure and the file itself first. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw2">mkdir</span> src<span class="sy0">/</span>Routing $ <span class="kw2">touch</span> src<span class="sy0">/</span>Routing<span class="sy0">/</span>RouteSubscriber.php</pre></div></div> And the file contents are: <br /><strong>RouteSubscriber.php</strong> <div class="codeblock geshifilter"><code><span style="color: #000000"><br /><span style="color: #0000BB">&lt;?php<br /><br /></span><span style="color: #FF8000">/**<br />* @file<br />* Contains \Drupal\mymodule\Routing\RouteSubscriber.<br />*/<br /><br /></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">badzilla_static</span><span style="color: #007700">\</span><span style="color: #0000BB">Routing</span><span style="color: #007700">;<br /><br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Routing</span><span style="color: #007700">\</span><span style="color: #0000BB">RouteSubscriberBase</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Symfony</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Routing</span><span style="color: #007700">\</span><span style="color: #0000BB">RouteCollection</span><span style="color: #007700">;<br /><br /><br /><br /></span><span style="color: #FF8000">/**<br />* Listens to the dynamic route events.<br />*/<br /></span><span style="color: #007700">class </span><span style="color: #0000BB">RouteSubscriber </span><span style="color: #007700">extends </span><span style="color: #0000BB">RouteSubscriberBase </span><span style="color: #007700">{<br /><br />    </span><span style="color: #FF8000">/**<br />    * {@inheritdoc}<br />    */<br />    </span><span style="color: #007700">public function </span><span style="color: #0000BB">alterRoutes</span><span style="color: #007700">(</span><span style="color: #0000BB">RouteCollection $collection</span><span style="color: #007700">)<br />    {<br />        </span><span style="color: #FF8000">// Replace dynamically created "search.view_node_search" route's Controller with my own.<br />        </span><span style="color: #007700">if (</span><span style="color: #0000BB">$route </span><span style="color: #007700">= </span><span style="color: #0000BB">$collection</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">get</span><span style="color: #007700">(</span><span style="color: #DD0000">'search.view_node_search'</span><span style="color: #007700">)) {<br />            </span><span style="color: #0000BB">$route</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">setDefault</span><span style="color: #007700">(</span><span style="color: #DD0000">'_controller'</span><span style="color: #007700">, </span><span style="color: #DD0000">'\Drupal\badzilla_static\Controller\BadzillaStaticSearchController::view'</span><span style="color: #007700">);<br />        }<br />    }<br />}<br /></span><span style="color: #0000BB">?&gt;</span></span></code></div> Here I am replacing the built-in search module's controller with my own. Now let's build out my own do-nothing controller.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Custom search controller</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">As previously, create the file structure. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw2">mkdir</span> src<span class="sy0">/</span>Controller $ <span class="kw2">touch</span> src<span class="sy0">/</span>Controller<span class="sy0">/</span>BadzillaStaticSearchController.php</pre></div></div> Here's the controller in all its glory.<br /><strong>BadzillaStaticSearchController.php</strong> <div class="codeblock geshifilter"><code><span style="color: #000000"><span style="color: #0000BB">&lt;?php<br /></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">badzilla_static</span><span style="color: #007700">\</span><span style="color: #0000BB">Controller</span><span style="color: #007700">;<br /><br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">search</span><span style="color: #007700">\</span><span style="color: #0000BB">SearchPageInterface</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Symfony</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">HttpFoundation</span><span style="color: #007700">\</span><span style="color: #0000BB">Request</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">search</span><span style="color: #007700">\</span><span style="color: #0000BB">Controller</span><span style="color: #007700">\</span><span style="color: #0000BB">SearchController</span><span style="color: #007700">;<br /><br /></span><span style="color: #FF8000">/**<br /> * Override the Route controller for search.<br /> */<br /></span><span style="color: #007700">class </span><span style="color: #0000BB">BadzillaStaticSearchController </span><span style="color: #007700">extends </span><span style="color: #0000BB">SearchController </span><span style="color: #007700">{<br /><br />    </span><span style="color: #FF8000">/**<br />     * {@inheritdoc}<br />     */<br />    </span><span style="color: #007700">public function </span><span style="color: #0000BB">view</span><span style="color: #007700">(</span><span style="color: #0000BB">Request $request</span><span style="color: #007700">, </span><span style="color: #0000BB">SearchPageInterface $entity</span><span style="color: #007700">)<br />    {<br />        </span><span style="color: #0000BB">$build </span><span style="color: #007700">= [];<br />        </span><span style="color: #0000BB">$plugin </span><span style="color: #007700">= </span><span style="color: #0000BB">$entity</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getPlugin</span><span style="color: #007700">();<br />        </span><span style="color: #0000BB">$build</span><span style="color: #007700">[</span><span style="color: #DD0000">'#title'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$plugin</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">suggestedTitle</span><span style="color: #007700">();<br /><br />        </span><span style="color: #0000BB">$results </span><span style="color: #007700">= [];<br />        </span><span style="color: #0000BB">$build</span><span style="color: #007700">[</span><span style="color: #DD0000">'search_results'</span><span style="color: #007700">] = [<br />            </span><span style="color: #DD0000">'#theme' </span><span style="color: #007700">=&gt; [<br />                </span><span style="color: #DD0000">'item_list__search_results__' </span><span style="color: #007700">. </span><span style="color: #0000BB">$plugin</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getPluginId</span><span style="color: #007700">(),<br />                </span><span style="color: #DD0000">'item_list__search_results'</span><span style="color: #007700">,<br />            ],<br />            </span><span style="color: #DD0000">'#items' </span><span style="color: #007700">=&gt; </span><span style="color: #0000BB">$results</span><span style="color: #007700">,<br />            </span><span style="color: #DD0000">'#empty' </span><span style="color: #007700">=&gt; [<br />                </span><span style="color: #DD0000">'#markup' </span><span style="color: #007700">=&gt; </span><span style="color: #DD0000">''</span><span style="color: #007700">,<br />            ],<br />            </span><span style="color: #DD0000">'#list_type' </span><span style="color: #007700">=&gt; </span><span style="color: #DD0000">'ol'</span><span style="color: #007700">,<br />            </span><span style="color: #DD0000">'#context' </span><span style="color: #007700">=&gt; [<br />                </span><span style="color: #DD0000">'plugin' </span><span style="color: #007700">=&gt; </span><span style="color: #0000BB">$plugin</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getPluginId</span><span style="color: #007700">(),<br />            ],<br />        ];<br /><br />        return </span><span style="color: #0000BB">$build</span><span style="color: #007700">;<br />    }<br />}<br /></span><span style="color: #0000BB">?&gt;</span></span></code></div> The view function must return a render array, and so I have honoured the convention of the parent SearchController::view to large extent although obviously in my case there will always be no results returned. A useful stub!</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Clear Rebuild and Test</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-18_at_15_22_08-edited.png?itok=SkBm2FM6 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-18_at_15_22_08-edited.png?itok=ckGbh1Np 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-18_at_15_22_08-edited.png?itok=a925Zvff 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-18_at_15_22_08-edited.png?itok=9SaizRUC 2314w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-18_at_15_22_08-edited.png?itok=SkBm2FM6" alt="Seach1" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-18_at_15_21_23-edited.png?itok=Hx9JuUhC 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-18_at_15_21_23-edited.png?itok=uFQLs4hF 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-18_at_15_21_23-edited.png?itok=MapWzEyf 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-18_at_15_21_23-edited.png?itok=5giBsaDr 2312w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-18_at_15_21_23-edited.png?itok=Hx9JuUhC" alt="Search2" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Since we've added to our services yaml file we need to do the customary cache rebuild for our changes to take effect. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ drush cr <span class="br0">[</span>success<span class="br0">]</span> Cache rebuild complete.</pre></div></div> Next let's attempt a search on the dynamic and the static version of my sandbox Badzilla sites. The two screenshots above show that both the static and the dynamic version of the sandbox sites are correctly showing a blank search page. Next we have to plug in the client....</div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> <span class="field--item"><a href="/development" hreflang="en">Development</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/php" hreflang="en">PHP</a></span> </span> </div> Sun, 18 Nov 2018 10:04:27 +0000 nigel 159 at http://badzilla.co.uk Drupal 8 as a Static Site: Search Architecture and Drupal Configuration http://badzilla.co.uk/drupal-8-static-site-search-architecture-and-drupal-configuration <span>Drupal 8 as a Static Site: Search Architecture and Drupal Configuration</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Thu, 15/11/2018 - 14:21</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>As we've already discussed in earlier blogs, any successful odyssey towards a static version of the Badzilla blog using the Drupal module Tome will depend upon having a good replacement search facility in place. The built-in Drupal search won't work since there will be no back-end to process the search request. There are many alternative ways of achieving a viable search solution on a static site, and I've listed a few in my introductory page <a href="/Drupal8-Static">Drupal 8 as a Static Site</a>. Ideally I would like a solution that will work on both my dynamic, 'Drupal' version of the site (i.e. where I create my content) and the Tome generated static version of the site (i.e. the version viewable by the public). </p> <p>To my mind, the obvious answer is Elasticsearch combined with a JavaScript client. I would have opted for Google Site Search but it was closed down in April 2018 and replaced with Custom Search Engine (CSE). CSE offers similar capabilities to Site Search but carries ads. That automatically deselects itself from consideration. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Elasticsearch Architecture</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>My proposed architecture is simplicity itself. I have a local sandboxed Virtual Machine with my development environment installed on it. For reference, as well as blatant self-promotion, I use the <a href="https://github.com/sanddevil/badzillavm">BadzillaVM</a> which is a Ubuntu image with a raft of utilities and tools pre-installed, including Elasticsearch. </p> <p>The architectural solution is to create an Elasticsearch index on my sandbox. To that end I'll use the Drupal Elasticsearch connector and the Search API modules. Every time content is added on to my local version of Drupal, the index will be updated automatically.</p> <p>I will then trigger an automated build process to generate the static pages and to dump the Elasticsearch index using elasticdump. This will then be deployed to badzilla.co.uk - the static html into the traditional /var/www/html structure on the prod server, and the elastic index imported into the Elasticsearch server I will have on prod. Clearly locking down the Elasticsearch instance on prod will be crucial - and we'll cover that later. </p> <p>Ok - so that's the theory. Let's see how easy (or otherwise) it is in practice. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Drupal configuration - Add and enable the modules</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Let's make a start. Firstly we need to add and enable the two modules we'll be using - the Elasticsearch connector and the Search API. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ composer require drupal<span class="sy0">/</span>search_api <span class="sy0">&lt;</span>-snipped-<span class="sy0">&gt;</span> $ composer require drupal<span class="sy0">/</span>elasticsearch_connector:^<span class="nu0">6.0</span>-alpha1 <span class="sy0">&lt;</span>-snipped-<span class="sy0">&gt;</span> $ drush en search_api elasticsearch_connector <span class="re5">-y</span></pre></div></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Drupal configuration - Add an Elasticsearch cluster</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-15_at_16_14_39-edited.png?itok=HMLYbDcI 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-15_at_16_14_39-edited.png?itok=ANn7vl0Y 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-15_at_16_14_39-edited.png?itok=rHpe3VnE 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-15_at_16_14_39-edited.png?itok=jThwfjvE 2436w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-15_at_16_14_39-edited.png?itok=HMLYbDcI" alt="Cluster" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Navigate to admin/config/search/elasticsearch-connector/cluster/add to create a cluster. I have called mine the arbitrary BadzillaStatic and note I have pointed it to my local Elasticsearch instance on my sandbox with a url of http://localhost:9200</p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Drupal configuration - add a Search API server</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-16_at_16_27_15-edited.png?itok=vOfghKE2 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-16_at_16_27_15-edited.png?itok=i-j7uJIP 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-16_at_16_27_15-edited.png?itok=Dk3f-dO9 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-16_at_16_27_15-edited.png?itok=tYTG27xz 1946w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-16_at_16_27_15-edited.png?itok=vOfghKE2" alt="Search API Server" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-16%20at%2016.28.08.png?itok=aT0AjDMr 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-16%20at%2016.28.08.png?itok=eeTGzfg8 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-16%20at%2016.28.08.png?itok=_qIGljd5 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-16%20at%2016.28.08.png?itok=FsTOhhAU 1942w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-16%20at%2016.28.08.png?itok=aT0AjDMr" alt="Notification" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Now we have to add a Search API server - this is achieved by navigating to admin/config/search/search-api/add-server. I have added the name DrupalStatic, and I also checked that the cluster and the  backend were selected correctly. Once I submitted this form, I got an encouraging notification page suggesting everything went well. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Drupal Configuration: Add index</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-add-index-2018-11-16-16_43_01-edited.png?itok=l6TYaCqg 215w, /sites/default/files/styles/max_650x650/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-add-index-2018-11-16-16_43_01-edited.png?itok=3RXXshmH 429w, /sites/default/files/styles/max_1300x1300/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-add-index-2018-11-16-16_43_01-edited.png?itok=8GRe8uMW 858w, /sites/default/files/styles/max_2600x2600/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-add-index-2018-11-16-16_43_01-edited.png?itok=BtEsJNME 1716w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-add-index-2018-11-16-16_43_01-edited.png?itok=l6TYaCqg" alt="Index" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Navigate to admin/config/search/search-api/add-index to create a search index. Whilst the screenshot will be small, it is quite easy and intuitive to populate. Add an index name - I went for StaticContent. Then I ticked the 'Content' checkbox since that is all I want for my search. Further down I selected my content types. Many in my list are artefacts from my original D6 blog and not required any more. I checked English language, made sure the radio button for the search API server DrupalStatic was selected, and also made sure the Enabled checkbox is ticked. Finally I checked 'Index items immediately'. This is imperative for me since I don't want to wait for a cron run once I've create or updated content in my sandbox. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-fields-add-nojs-2018-11-16-16_46_27.-edited_0.png?itok=Xz0_5BCc 158w, /sites/default/files/styles/max_650x650/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-fields-add-nojs-2018-11-16-16_46_27.-edited_0.png?itok=M8W7Rmh2 315w, /sites/default/files/styles/max_1300x1300/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-fields-add-nojs-2018-11-16-16_46_27.-edited_0.png?itok=m3KXZwIM 631w, /sites/default/files/styles/max_2600x2600/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-fields-add-nojs-2018-11-16-16_46_27.-edited_0.png?itok=kpWsQPLd 1261w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-fields-add-nojs-2018-11-16-16_46_27.-edited_0.png?itok=Xz0_5BCc" alt="Fields" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Next fields have to be added to the index. This is where we need to be careful. For instance what I want for the 'authored by' field is not the default value - the uid - but the user name instead. so when picking the fields, it is necessary to expand the entries to ensure no ids are picked instead of the actual value. Don't forget we won't have views or any other backend preprocessor at our disposal and therefore we need real values and not primary keys. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-17_at_15_29_01-edited.png?itok=BMVjjq6c 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-17_at_15_29_01-edited.png?itok=55M5ncA4 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-17_at_15_29_01-edited.png?itok=4Ge0hB9- 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-17_at_15_29_01-edited.png?itok=Hc_8CuRX 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-17_at_15_29_01-edited.png?itok=BMVjjq6c" alt="Field Management" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Next is the field management screen - this gives a good overview of what you have selected, and providing the fields are fulltext they can be boosted to increase their search precedence. I have done this for title, terms and body text although I reserve the right to loop back and change these values! Also note that in most instances I have changed the machine names to something more applicable. This will pay dividends later when we are dealing with data fetched by the client in the app's frontend. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-processors-2018-11-17-14_53_23.png?itok=_IPr-Kt1 187w, /sites/default/files/styles/max_650x650/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-processors-2018-11-17-14_53_23.png?itok=uK4eH06U 373w, /sites/default/files/styles/max_1300x1300/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-processors-2018-11-17-14_53_23.png?itok=q1LiJOCm 746w, /sites/default/files/styles/max_2600x2600/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-processors-2018-11-17-14_53_23.png?itok=_CCzEP9F 1492w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/screencapture-meedjum-test-admin-config-search-search-api-index-staticcontent-processors-2018-11-17-14_53_23.png?itok=_IPr-Kt1" alt="Processors" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>By clicking on the Processors tab there is the opportunity to fine tune the index. I won't go into these in any great detail since they are purely personal preference and the supporting text on the Processors page is self-explanatory. I elected for Entity Status, HTML Filter and Transliteration. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-16_at_16_52_38-edited.png?itok=AKeBX8e_ 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-16_at_16_52_38-edited.png?itok=ueAhahjp 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-16_at_16_52_38-edited.png?itok=XWc5RGdb 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-16_at_16_52_38-edited.png?itok=JzD2LNiU 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-16_at_16_52_38-edited.png?itok=AKeBX8e_" alt="Indexing" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Finally we are ready for indexing. This can be achieved through cron or by using UI and indexing the lot in one go by setting the parameters at the bottom of the screen. Note this doesn't mean the state of the index will be reflected on the server - double check everything has completed by referring to the ringed message in the screenshot above. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Checking the search works</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-17%20at%2015.46.03.png?itok=z7mDh34S 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-17%20at%2015.46.03.png?itok=_UfNMYYk 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-17%20at%2015.46.03.png?itok=BEwiVJan 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-17%20at%2015.46.03.png?itok=bRK9sZEM 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-17%20at%2015.46.03.png?itok=z7mDh34S" alt="Kibana" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">If you are using Kibana, or even better Kibana in my BadzillaVM, then you can use this excellent tool to check the search is working. Point a browser to {your_ip}:5601 and then navigate to Dev Tools and apply the following query: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">GET <span class="sy0">/</span>elasticsearch_index_badzilla_meedjum_staticcontent<span class="sy0">/</span>_search <span class="br0">{</span> <span class="st0">"query"</span>: <span class="br0">{</span> <span class="st0">"match_all"</span>: <span class="br0">{</span><span class="br0">}</span> <span class="br0">}</span>, <span class="st0">"size"</span>: <span class="nu0">5</span> <span class="br0">}</span></pre></div></div> and as per the screenshot above, you should see the results output. Note that your index in the query will be different.</div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> </span> </div> Thu, 15 Nov 2018 14:21:18 +0000 nigel 158 at http://badzilla.co.uk Drupal 8 as a Static Site: Add feed.rss to web server config and views http://badzilla.co.uk/drupal-8-static-site-add-feedrss-web-server-config-and-views <span>Drupal 8 as a Static Site: Add feed.rss to web server config and views</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Tue, 13/11/2018 - 14:16</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Fix Views RSS feeds paths</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01.png?itok=VoeQmD5h 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01.png?itok=mTwIxTbN 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01.png?itok=eYo8v6yF 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01.png?itok=x-Koyb-m 2486w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01.png?itok=VoeQmD5h" alt="Taxonomy Term" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01_0.png?itok=gM--2VeD 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01_0.png?itok=HwWJ_TFx 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01_0.png?itok=KGkPolig 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01_0.png?itok=xcGfY4lU 2486w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-13%20at%2014.28.01_0.png?itok=gM--2VeD" alt="Planet Feed" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>My objective here is to ensure that my existing <em>/taxonomy/term/%/feed</em> and <em>/planet/feed</em> links still work correctly so I won't lose any RSS feed subscribers to my website. As I have mentioned previously, Tome will automatically create an <em>index.html </em>page when there is no file extension on a path. So it's best to add extensions then! Navigate to <em>/admin/structure/views</em> and edit <em>Taxonomy term</em> then click the feed display. Click on the <em>path</em> and edit according to my first screenshot above and save. I also have a Drupal Planet feed which acts on my frontpage but with tag filters,  so I repeated those steps and added a feed.rss path to that too. </p> <p>We aren't done yet though - my web server config needs to be changed too so that the RSS pages can be served up correctly. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">nginx configuration</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0"># Default server configuration</span> <span class="co0">#</span> server <span class="br0">{</span> listen <span class="nu0">80</span> default_server; listen <span class="br0">[</span>::<span class="br0">]</span>:<span class="nu0">80</span> default_server;   <span class="co0"># SSL configuration</span>   root <span class="br0">{</span><span class="br0">{</span> path_to_your_docroot <span class="br0">}</span><span class="br0">}</span>;   server_name <span class="br0">{</span><span class="br0">{</span> your_server_name <span class="br0">}</span><span class="br0">}</span>;   location ~ <span class="br0">(</span><span class="sy0">/</span>planet<span class="sy0">/</span>feed<span class="sy0">|/</span>taxonomy<span class="sy0">/</span>term<span class="sy0">/</span>\d+<span class="sy0">/</span>feed<span class="br0">)</span> <span class="br0">{</span> index feed.rss; <span class="br0">}</span>   location <span class="sy0">/</span> <span class="br0">{</span>   index index.html;   <span class="co0"># First attempt to serve request as file, then</span> <span class="co0"># as directory, then fall back to displaying a 404.</span> try_files <span class="re1">$uri</span> <span class="re1">$uri</span><span class="sy0">/</span> =<span class="nu0">404</span>; <span class="br0">}</span>   <span class="co0"># deny access to .htaccess files, if Apache's document root</span> <span class="co0"># concurs with nginx's one</span> <span class="co0">#</span> location ~ <span class="sy0">/</span>\.ht <span class="br0">{</span> deny all; <span class="br0">}</span> <span class="br0">}</span></pre></div></div> Note that I have added a new location region in the configuration specifically for the feed.rss files. Here we are saying if the path is either /planet/rss or /taxonomy/feed/%/feed then ensure that feed.rss files are served and not index.html files.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Apache configuration</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="sy0">&lt;</span>VirtualHost <span class="sy0">*</span>:<span class="nu0">80</span><span class="sy0">&gt;</span> <span class="co0"># change this to 443 is you are using ssl</span>   <span class="sy0">&lt;</span>Directory <span class="st0">"{{ path_to_your_docroot }}/planet/feed"</span><span class="sy0">&gt;</span> DirectoryIndex feed.rss <span class="sy0">&lt;/</span>Directory<span class="sy0">&gt;</span> <span class="sy0">&lt;</span>Directory ~ <span class="st0">"{{ path_to_your_docroot }}/taxonomy/term/[0-9]+/feed"</span><span class="sy0">&gt;</span> DirectoryIndex feed.rss <span class="sy0">&lt;/</span>Directory<span class="sy0">&gt;</span> ServerName <span class="br0">{</span><span class="br0">{</span> your_server_name <span class="br0">}</span><span class="br0">}</span> DocumentRoot <span class="st0">"{{ path_to_your_docroot }}"</span> DirectoryIndex index.html     <span class="co0"># Comment out the block below for SSL</span> <span class="co0">#SSLEngine on</span> <span class="co0">#SSLCertificateFile "/path/to/www.example.com.cert"</span> <span class="co0">#SSLCertificateKeyFile "/path/to/www.example.com.key"</span>   <span class="sy0">&lt;</span>Directory <span class="st0">"{{ path_to_your_docroot }}"</span><span class="sy0">&gt;</span> AllowOverride All <span class="sy0">&lt;/</span>Directory<span class="sy0">&gt;</span>   <span class="co0"># Logs</span> Errorlog <span class="co1">${APACHE_LOG_DIR}</span><span class="sy0">/</span>static.error.log CustomLog <span class="co1">${APACHE_LOG_DIR}</span><span class="sy0">/</span>static.access.log combined   <span class="sy0">&lt;/</span>VirtualHost<span class="sy0">&gt;</span></pre></div></div> The Apache configuration uses two new Directory directives. The second one contains a wildcard tilde and a regex to ensure that the path /taxonomy/term/%/feed is limited to numerics only. However I had another configuration problem - my Apache server was unaware of the .xml and .rss filename extensions, so wouldn't serve the content up. This was fixed by adding a filetype to the Apache configuration. <br /><strong>mods-available/mime.conf</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">AddType application<span class="sy0">/</span>xml .xml .rss</pre></div></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Success!</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-13_at_16_36_17-edited.png?itok=jP9mrwTx 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot_2018-11-13_at_16_36_17-edited.png?itok=7mSzP7RJ 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot_2018-11-13_at_16_36_17-edited.png?itok=fUCdYjIa 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot_2018-11-13_at_16_36_17-edited.png?itok=-P6OL8pL 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot_2018-11-13_at_16_36_17-edited.png?itok=jP9mrwTx" alt="Success" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The picture above provides the evidence the fixes worked! </p></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> </span> </div> Tue, 13 Nov 2018 14:16:59 +0000 nigel 157 at http://badzilla.co.uk Drupal 8 as a Static Site: Manually Add Paths http://badzilla.co.uk/drupal-8-static-site-manually-add-paths <span>Drupal 8 as a Static Site: Manually Add Paths</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Tue, 13/11/2018 - 09:24</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>We established in my earlier blog <a href="/drupal-8-static-site-problems-identified">Drupal 8 as a Static Site: Problems Identified</a> that Tome isn't capable of expanding wildcards such as the taxonomy RSS subscription paths in Views at taxonomy/term/%/feed. This tutorial shows how to manually add paths so when the command <em>drush tome:static </em>is run they are created correctly in the static version of the website. </p> <p>Thankfully the Tome modules makes this relatively easy although we will need to develop a custom event subscriber. The subscriber will be looking for a collect paths event which is triggered when the <em>drush tome:static </em>command is run. At that point we can add our missing paths. In our case it will be all the taxonomy term feed paths which the Badzilla website uses. </p> <p>So the first thing to do is to create a custom module and an event subscriber within it. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Create a skeleton custom module</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Let's create a custom module <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw3">cd</span> docroot<span class="sy0">/</span>modules<span class="sy0">/</span>custom $ <span class="kw2">mkdir</span> badzilla_static $ <span class="kw3">cd</span> badzilla_static<span class="sy0">/</span> $ <span class="kw2">touch</span> badzilla_static.info.yml</pre></div></div> And populate <strong>badzilla_static.info.yml</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">name: Badzilla Static description: Custom code <span class="kw1">for</span> facilitating the generation of a static version of Badzilla package: Badzilla type: module core: <span class="nu0">8</span>.x</pre></div></div> Now we need the services yml file which will hold the event subscriber namespace and parameter information. <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co4">$ </span><span class="kw2">touch</span> badzilla_static.services.yml</pre></div></div> and let's populate <strong> badzilla_static.services.yml</strong> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">services: badzilla.route_path_subscriber: class: Drupal\badzilla_static\EventSubscriber\RoutePathSubscriber arguments: <span class="br0">[</span><span class="st_h">'@router.route_provider'</span><span class="br0">]</span> tags: - <span class="br0">{</span> name: event_subscriber <span class="br0">}</span></pre></div></div> Now we need to create the RouteEventSubscriber so let's create the directory <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw2">mkdir</span> <span class="re5">-p</span> src<span class="sy0">/</span>EventSubscriber $ <span class="kw2">touch</span> src<span class="sy0">/</span>EventSubscriber<span class="sy0">/</span>RoutePathSubscriber.php</pre></div></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Event Subscriber</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">The event subscriber must implement the EventSubscriberInterface. Let's have a look at the code before the commentary. <div class="codeblock geshifilter"><code><span style="color: #000000"><br /><span style="color: #0000BB">&lt;?php<br /><br /></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">badzilla_static</span><span style="color: #007700">\</span><span style="color: #0000BB">EventSubscriber</span><span style="color: #007700">;<br /><br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Routing</span><span style="color: #007700">\</span><span style="color: #0000BB">RouteProviderInterface</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">tome</span><span style="color: #007700">\</span><span style="color: #0000BB">Event</span><span style="color: #007700">\</span><span style="color: #0000BB">CollectPathsEvent</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Symfony</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">EventDispatcher</span><span style="color: #007700">\</span><span style="color: #0000BB">EventSubscriberInterface</span><span style="color: #007700">;<br />use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">tome</span><span style="color: #007700">\</span><span style="color: #0000BB">Event</span><span style="color: #007700">\</span><span style="color: #0000BB">TomeEvents</span><span style="color: #007700">;<br /><br /><br /><br /></span><span style="color: #FF8000">/**<br /> * Adds route paths to the list of paths to export.<br /> */<br /></span><span style="color: #007700">class </span><span style="color: #0000BB">RoutePathSubscriber </span><span style="color: #007700">implements </span><span style="color: #0000BB">EventSubscriberInterface<br /></span><span style="color: #007700">{<br /><br />    </span><span style="color: #FF8000">/**<br />     * The route provider.<br />     *<br />     * @var \Drupal\Core\Routing\RouteProviderInterface<br />     */<br />    </span><span style="color: #007700">protected </span><span style="color: #0000BB">$routeProvider</span><span style="color: #007700">;<br /><br /><br />    </span><span style="color: #FF8000">/**<br />     * Constructs the RoutePathSubscriber object.<br />     *<br />     * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider<br />     *   The route provider.<br />     */<br />    </span><span style="color: #007700">public function </span><span style="color: #0000BB">__construct</span><span style="color: #007700">(</span><span style="color: #0000BB">RouteProviderInterface $route_provider</span><span style="color: #007700">) {<br />        </span><span style="color: #0000BB">$this</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">routeProvider </span><span style="color: #007700">= </span><span style="color: #0000BB">$route_provider</span><span style="color: #007700">;<br />    }<br /><br /><br />    </span><span style="color: #FF8000">/**<br />     * Reacts to a collect paths event - adds the taxonomy feeds paths<br />     *<br />     * @param \Drupal\tome\Event\CollectPathsEvent $event<br />     *   The collect paths event.<br />     */<br />    </span><span style="color: #007700">public function </span><span style="color: #0000BB">collectPaths</span><span style="color: #007700">(</span><span style="color: #0000BB">CollectPathsEvent $event</span><span style="color: #007700">) {<br />        </span><span style="color: #0000BB">$terms </span><span style="color: #007700">= \</span><span style="color: #0000BB">Drupal</span><span style="color: #007700">::</span><span style="color: #0000BB">entityTypeManager</span><span style="color: #007700">()-&gt;</span><span style="color: #0000BB">getStorage</span><span style="color: #007700">(</span><span style="color: #DD0000">'taxonomy_term'</span><span style="color: #007700">)-&gt;</span><span style="color: #0000BB">loadTree</span><span style="color: #007700">(</span><span style="color: #DD0000">'technology'</span><span style="color: #007700">);<br />        if (</span><span style="color: #0000BB">is_array</span><span style="color: #007700">(</span><span style="color: #0000BB">$terms</span><span style="color: #007700">) &amp;&amp; </span><span style="color: #0000BB">count</span><span style="color: #007700">(</span><span style="color: #0000BB">$terms</span><span style="color: #007700">)) {<br />            foreach(</span><span style="color: #0000BB">$terms </span><span style="color: #007700">as </span><span style="color: #0000BB">$term</span><span style="color: #007700">) {<br />                </span><span style="color: #0000BB">$event</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">addPath</span><span style="color: #007700">(</span><span style="color: #0000BB">sprintf</span><span style="color: #007700">(</span><span style="color: #DD0000">'/taxonomy/term/%d/feed/feed.rss'</span><span style="color: #007700">, </span><span style="color: #0000BB">$term</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">tid</span><span style="color: #007700">));<br />            }<br />        }<br />    }<br /><br /><br />    </span><span style="color: #FF8000">/**<br />     * {@inheritdoc}<br />     */<br />    </span><span style="color: #007700">public static function </span><span style="color: #0000BB">getSubscribedEvents</span><span style="color: #007700">() {<br />        </span><span style="color: #0000BB">$events</span><span style="color: #007700">[</span><span style="color: #0000BB">TomeEvents</span><span style="color: #007700">::</span><span style="color: #0000BB">COLLECT_PATHS</span><span style="color: #007700">][] = [</span><span style="color: #DD0000">'collectPaths'</span><span style="color: #007700">];<br />        return </span><span style="color: #0000BB">$events</span><span style="color: #007700">;<br />    }<br />}<br /></span><span style="color: #0000BB">?&gt;</span></span></code></div></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>The code is largely a copy / paste exercise from Tome's own event subscriber but the difference is in the method <em>collectPaths(). </em>My objective is to get the tids from the vocabulary called <em>technology</em> I use on my listing pages. To achieve this I load all the terms of that vocabulary, iterate through them, and inject the tids into the path /taxonomy/term/%/feed/feed.rss. Note how I have added <em>feed.rss</em> to the url? This is so Tome, seeing a file extension will not create the standard <em>index.html</em> file in the <em>feed</em> directory. </p> <p>This solution does require a configuration change at the web server level and in the Views. See the chapter <a href="/drupal-8-static-site-add-feedrss-web-server-config-and-views">Drupal 8 as a Static Site: Add feed.rss to web server config and views</a></p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Enabling and running</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">Now we have the coding complete. we need to enable the module the usual way: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ drush en badzilla_static <span class="re5">-y</span> <span class="br0">[</span>success<span class="br0">]</span> Successfully enabled: badzilla_static</pre></div></div> Now when I run the drush tome:static I see: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">Generating static HTML... <span class="nu0">260</span><span class="sy0">/</span><span class="nu0">260</span> <span class="br0">[</span>▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓<span class="br0">]</span> <span class="nu0">100</span><span class="sy0">%</span></pre></div></div> as opposed to the lower value of paths before my code: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">Generating static HTML... <span class="nu0">227</span><span class="sy0">/</span><span class="nu0">227</span> <span class="br0">[</span>▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓<span class="br0">]</span> <span class="nu0">100</span><span class="sy0">%</span></pre></div></div> Also if I ls the directory with the terms I will now see my entries correctly: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw2">ls</span> ..<span class="sy0">/</span>static<span class="sy0">/</span>html<span class="sy0">/</span>taxonomy<span class="sy0">/</span>term<span class="sy0">/</span> <span class="nu0">100</span> <span class="nu0">102</span> <span class="nu0">104</span> <span class="nu0">106</span> <span class="nu0">109</span> <span class="nu0">111</span> <span class="nu0">13</span> <span class="nu0">20</span> <span class="nu0">27</span> <span class="nu0">36</span> <span class="nu0">47</span> <span class="nu0">53</span> <span class="nu0">55</span> <span class="nu0">78</span> <span class="nu0">80</span> <span class="nu0">82</span> <span class="nu0">84</span> <span class="nu0">86</span> <span class="nu0">88</span> <span class="nu0">9</span> <span class="nu0">91</span> <span class="nu0">93</span> <span class="nu0">95</span> <span class="nu0">97</span> <span class="nu0">99</span> <span class="nu0">101</span> <span class="nu0">103</span> <span class="nu0">105</span> <span class="nu0">107</span> <span class="nu0">110</span> <span class="nu0">112</span> <span class="nu0">18</span> <span class="nu0">26</span> <span class="nu0">32</span> <span class="nu0">44</span> <span class="nu0">49</span> <span class="nu0">54</span> <span class="nu0">57</span> <span class="nu0">79</span> <span class="nu0">81</span> <span class="nu0">83</span> <span class="nu0">85</span> <span class="nu0">87</span> <span class="nu0">89</span> <span class="nu0">90</span> <span class="nu0">92</span> <span class="nu0">94</span> <span class="nu0">96</span> <span class="nu0">98</span></pre></div></div></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> <span class="field--item"><a href="/development" hreflang="en">Development</a></span> <span class="field--item"><a href="/php" hreflang="en">PHP</a></span> </span> </div> Tue, 13 Nov 2018 09:24:34 +0000 nigel 156 at http://badzilla.co.uk Drupal 8 as a Static Site: Problems Identified http://badzilla.co.uk/drupal-8-static-site-problems-identified <span>Drupal 8 as a Static Site: Problems Identified</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Thu, 08/11/2018 - 10:05</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Search doesn&#039;t work</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.10.38.png?itok=ITkKCy3e 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-08%20at%2010.10.38.png?itok=CfRa3oep 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-08%20at%2010.10.38.png?itok=ooh8YZqm 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-08%20at%2010.10.38.png?itok=XCfsKAV4 2236w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.10.38.png?itok=ITkKCy3e" alt="Search" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>It's churlish to say that search doesn't work. It's like jumping on the wrong bus and then complaining it didn't go past your house. Of course search doesn't work - it needs to bootstrap Drupal once the search form is submitted. But we do need to find a solution to that - and I will be discussing Elasticsearch in a later chapter. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Drupal Planet feed rendered as HTML</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.24.29-edited.png?itok=-woPfKIK 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-08%20at%2010.24.29-edited.png?itok=Mb_GF7_r 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-08%20at%2010.24.29-edited.png?itok=wfS8Am_E 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-08%20at%2010.24.29-edited.png?itok=s1qmAmh_ 2170w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.24.29-edited.png?itok=-woPfKIK" alt="Planet View" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.43.13.png?itok=UGNZQS5K 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-08%20at%2010.43.13.png?itok=Rr3trM79 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-08%20at%2010.43.13.png?itok=kS9u_Y78 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-08%20at%2010.43.13.png?itok=RKfaGXgZ 2060w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2010.43.13.png?itok=UGNZQS5K" alt="html" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item">This is another blindingly obvious problem and something that should be expected. I have a Drupal Planet RSS feed which is generated by a View (see first screenshot) and outputs XML to the route /planet/feed. Tome will render all routes with no extension using the following rule: create a directory with the route name, and then create an index.html file with the content in that directory. This can be seen below: <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">$ <span class="kw3">pwd</span> <span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html<span class="sy0">/</span>meedjum<span class="sy0">/</span>static<span class="sy0">/</span>html<span class="sy0">/</span>planet<span class="sy0">/</span>feed vagrant<span class="sy0">@</span>nigel-dev-box:<span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html<span class="sy0">/</span>meedjum<span class="sy0">/</span>static<span class="sy0">/</span>html<span class="sy0">/</span>planet<span class="sy0">/</span>feed$ <span class="kw2">ls</span> <span class="re5">-las</span> total <span class="nu0">8</span> <span class="nu0">0</span> drwxrwxr-x <span class="nu0">3</span> <span class="nu0">501</span> dialout <span class="nu0">96</span> Nov <span class="nu0">8</span> <span class="nu0">10</span>:<span class="nu0">38</span> . <span class="nu0">0</span> drwxrwxr-x <span class="nu0">5</span> <span class="nu0">501</span> dialout <span class="nu0">160</span> Nov <span class="nu0">7</span> 09:<span class="nu0">14</span> .. <span class="nu0">8</span> <span class="re5">-rw-rw-r--</span> <span class="nu0">1</span> <span class="nu0">501</span> dialout <span class="nu0">6817</span> Nov <span class="nu0">7</span> 09:00 index.html</pre></div></div> The second screenshot shows what happens when an html file of RSS format XML is presented to a browser. It is totally mangled. The solution involves web server configuration as well as a few tweaks at the Drupal end, and will be explained <a href="/drupal-8-static-site-add-feedrss-web-server-config-and-views">in a separate blog</a> since it also part-involves the problem identified below.</div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Term feeds are not created</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2011.16.48-edit.png?itok=UCF0NCSN 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-08%20at%2011.16.48-edit.png?itok=5DaebjiE 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-08%20at%2011.16.48-edit.png?itok=iCPE7jrw 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-08%20at%2011.16.48-edit.png?itok=Sc1TyPt0 2400w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-08%20at%2011.16.48-edit.png?itok=UCF0NCSN" alt="terms" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I am extensively using the Views term landing pages in my blog, and they always have an RSS subscribe button at the bottom of the page which will link to the path /taxonomy/term/%/feed. Tome doesn't have the capability to generate these routes since they are using wildcards. This is bad news from my perspective since I could have many subscribers to these feeds although of course there is no easy way of determining this.</p> <p>Regardless, <a href="/drupal-8-static-site-manually-add-paths">there is a solution</a> to this which involves custom code and the development of an event subscriber. Tome itself uses the same concept to garner all the routes to render - <a href="https://cgit.drupalcode.org/tome/tree/modules/tome_static/src/EventSubscriber/RoutePathSubscriber.php?h=8.x-1.x">RoutePathSubscriber.php</a>. A custom module would need to be created along the same lines, only in my instance I would need to provide all the routes that use /taxonomy/term/%/feed where % will be the term id.</p> <p>Once I've done that - in a forthcoming chapter  - I will still be faced with the same problem I identified above. I will now have for instance a route /taxonomy/term/71/feed where 71 would be the term id of the term 'Drupal'. So Tome would create a directory structure /taxonomy/term/71/feed and populate it with an index.html file. Thus <a href="/drupal-8-static-site-add-feedrss-web-server-config-and-views">the solution I find for my previous problem</a> will have to be extended for these routes too.  </p></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/static-site" hreflang="en">Static Site</a></span> <span class="field--item"><a href="/drupal" hreflang="en">Drupal</a></span> <span class="field--item"><a href="/drupal8" hreflang="en">Drupal 8</a></span> </span> </div> Thu, 08 Nov 2018 10:05:47 +0000 nigel 155 at http://badzilla.co.uk Building a Home FreeNAS Server - Hardware Upgrade http://badzilla.co.uk/building-home-freenas-server-hardware-upgrade <span>Building a Home FreeNAS Server - Hardware Upgrade</span> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">nigel</span></span> <span>Wed, 07/11/2018 - 12:43</span> <div class="field field--name-field-heading-image-text field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">A Late 2018 Upgrade</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/20181109_102500-scaled.jpg?itok=sRlF3Fq8 325w, /sites/default/files/styles/max_650x650/public/2018-11/20181109_102500-scaled.jpg?itok=d3E-yykm 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/20181109_102500-scaled.jpg?itok=P6vNZXpY 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/20181109_102500-scaled.jpg?itok=sRlF3Fq8" alt="FreNAS Server" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Since I built my FreeNAS server six years ago I have come to utterly depend up it. All the media I have resides on it (I <strong>won't</strong> use the cloud!!). I can access the server from everywhere in my apartment. My Plex server uses it. My Android TV device uses it. I have a dedicated HiFi laptop that streams FLAC files to my TEAC DAC. It is absolutely crucial to my life. </p> <p>My hardware was initially from 2007 although in the intervening years I did upgrade it to a new motherboard albeit that too was secondhand. The chassis was the same and the power supply was the same. It has become unreliable over the years and needs a reboot about once a week. The console messages are suggesting that power is the issue. Hardly surprising considering it is in a tower case from 2007 with minimal power. </p> <p>So the decision was made to upgrade the hardware to new products. Radical! But since I depend upon it so much, it must have good hardware. My disks are still on 30% capacity so there is no urgency to upgrade those - thankfully - since a weakness of FreeNAS is it is seriously difficult to add new drives. </p> <p>So let's have a look at my (sometimes strange) hardware decisions!</p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Motherboard</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/2018030911394343_big.png?itok=Oasv7kH8 325w, /sites/default/files/styles/max_650x650/public/2018-11/2018030911394343_big.png?itok=rBqsV7Rs 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/2018030911394343_big.png?itok=HEZeIQ-Y 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/2018030911394343_big.png?itok=Oasv7kH8" alt="motherboard" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I decided upon using an 8th generation Intel chip, and wanted a socket LGA 1151 as a consequence. Ideally I was going to go for a Supermicro dedicated server motherboard. However my enthusiasm was somewhat tempered when I couldn't find a new board under £200. That quite frankly was too much for my budget. If this was a commercial machine, I would have spent the money, but since this is a hobby machine I can't really justify the expense. So I decided upon a gaming motherboard instead that was a fraction of the price. It had the prerequisite 4 SATA connections and a capacity of 32GB RAM - just what I wanted. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">RAM</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/51O3wmuwT7L._SX679_.jpg?itok=4dcDk4Ll 325w, /sites/default/files/styles/max_650x650/public/2018-11/51O3wmuwT7L._SX679_.jpg?itok=3da0Na5G 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/51O3wmuwT7L._SX679_.jpg?itok=xvY3AC1q 679w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/51O3wmuwT7L._SX679_.jpg?itok=4dcDk4Ll" alt="RAM" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Whilst I wanted my motherboard to have a capacity of 32GB, I was only ready to buy 16GB this time around. I was looking for one stick, DDR4 2400MHz. The Corsair chip above came in marginally cheaper than its competitors so got the nod. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">CPU</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/hpit-443_hpit_443_01_800x800.jpg?itok=K8OkNMpu 325w, /sites/default/files/styles/max_650x650/public/2018-11/hpit-443_hpit_443_01_800x800.jpg?itok=B2EAhl-S 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/hpit-443_hpit_443_01_800x800.jpg?itok=GvGUHi4m 800w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/hpit-443_hpit_443_01_800x800.jpg?itok=K8OkNMpu" alt="CPU" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I wanted an 8th generation Coffee Lake Intel chip, and the i3 8100 is a tremendous compromise between performance and cost. It offers 3.6 GHz yet costs only just above a £100 which is fantastic value. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Chassis</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/section1-img.png?itok=6cViFlgt 325w, /sites/default/files/styles/max_650x650/public/2018-11/section1-img.png?itok=7fb8zEjL 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/section1-img.png?itok=1DrIOjd_ 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/section1-img.png?itok=6cViFlgt" alt="Chassis" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Ok so mea culpa. I decided to have some fun with the chassis and I got a gaming chassis complete with multiple fans and neon lighting. It can even be used as the base of a water cooled rig! The capacity for 3.5" drives is a stupendous 10 drives however so this case should stand me in great stead for the next decade regardless of how many additional drives I need to purchase. The model is the Cooler Master Mastercase MC500M and is considered a mini tower yet weighs a ton and has an enormous footprint. </p> <p>I lucked out with this case. I got it 30% cheaper than list price because it had been returned by a previous purchaser to Amazon with all the original packaging intact and totally unopened. Bargain. </p> <p>Of course both this chassis and the motherboard could be repurposed as the backbone of a gaming machine at a later date if I want to go for  a more orthodox server architecture. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">PSU</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/CP-9020049-NA-VS450_sideview_b.png?itok=-F6dU_AM 325w, /sites/default/files/styles/max_650x650/public/2018-11/CP-9020049-NA-VS450_sideview_b.png?itok=0THVY4dI 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/CP-9020049-NA-VS450_sideview_b.png?itok=woE44Tv1 750w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/CP-9020049-NA-VS450_sideview_b.png?itok=-F6dU_AM" alt="PSU" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I used an online power requirement calculator which suggested I was going to need 300-350 watts. I elected to give myself a little more headroom and went for the Corsair VS450 450W. It was a good price and quiet too. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Fans</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/81aFB1f%2BqIL._SL1500_.jpg?itok=-WTwjr53 265w, /sites/default/files/styles/max_650x650/public/2018-11/81aFB1f%2BqIL._SL1500_.jpg?itok=jrDXEGyx 530w, /sites/default/files/styles/max_1300x1300/public/2018-11/81aFB1f%2BqIL._SL1500_.jpg?itok=fQiukSz2 1059w, /sites/default/files/styles/max_2600x2600/public/2018-11/81aFB1f%2BqIL._SL1500_.jpg?itok=4SyHZnK1 1222w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/81aFB1f%2BqIL._SL1500_.jpg?itok=-WTwjr53" alt="Fans" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Yes so I wanted my FreeNAS server to be the most fun serious server, and I bought three Corsair CO-9050024-WW Air Series SP140 LED 140mm fans. I am not going to add this frippery to the bill of materials below. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Boot Drive</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/20181109_092233-scaled.jpg?itok=dBfX9lkj 280w, /sites/default/files/styles/max_650x650/public/2018-11/20181109_092233-scaled.jpg?itok=icxN6k6r 560w, /sites/default/files/styles/max_1300x1300/public/2018-11/20181109_092233-scaled.jpg?itok=ADGppUNY 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/20181109_092233-scaled.jpg?itok=dBfX9lkj" alt="Boot drive" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Since I built the FreeNAS back in 2012, I've been booting it with a USB stick. This is in fact the orthodox way - the loader contains the FreeNAS system which is hosted on a bootable freebsd image. However in latter times it has become more normal to use a small SSD drive instead. The only corruptions I've had on my system have been in the loader rather than the data volumes, and of course a sticking out USB drive can easily be knocked and broken. SSDs on the other hand are considerably more durable, and of course quicker although since the drives are only used in the boot process, that isn't significant. </p> <p>Thankfully I had a 64GB SSD which I bought in 2011 and had never used. It was in its original box waiting for a home theatre project to come along. Home theatre PCs are now obsolete since the advent of systems such as Kodi and Plex. Time to use my SSD! </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Bill of Materials</div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><table border="1" bordercolor="#FFCC00" cellpadding="3" cellspacing="3" style="background-color:#FFFF99" width="100%"><tbody><tr><td>Motherboard</td> <td><a href="https://www.amazon.co.uk/gp/product/B07BKK9LHR/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B07BKK9LHR&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=ca1709791f8e42a9fc08718c59604377" target="_blank">GIGABYTE H310M S2H</a></td> <td>£53.48</td> </tr><tr><td>Chassis</td> <td><a href="https://www.amazon.co.uk/gp/product/B078G9QNMP/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B078G9QNMP&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=46afebaeffe5717c0b5be8aafccd5a4e" target="_blank">Cooler Master Mastercase MC500M</a></td> <td>£103.83</td> </tr><tr><td>RAM</td> <td><a href="https://www.amazon.co.uk/gp/product/B017NW5NZY/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B017NW5NZY&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=2402f7bb91987e18245addea4a70bdee" target="_blank">Corsair CMK16GX4M1A2400C16 DDR4 16GB 2400MHz</a></td> <td>£118.97</td> </tr><tr><td>CPU</td> <td><a href="https://www.amazon.co.uk/gp/product/B0759FTRZL/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B0759FTRZL&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=1049fed0273618fd7511d22c2fa0bdc5" target="_blank">Intel i3 8100 Coffee Lake</a></td> <td>£129.95</td> </tr><tr><td>PSU</td> <td><a href="https://www.amazon.co.uk/gp/product/B078Y44N73/ref=as_li_tl?ie=UTF8&amp;camp=1634&amp;creative=6738&amp;creativeASIN=B078Y44N73&amp;linkCode=as2&amp;tag=upcomingfilms-21&amp;linkId=0069a5c2800911a7f9bde43b9e1502d4" target="_blank">Corsair VS450 450 W Active PFC 80 PLUS</a></td> <td>£40.50</td> </tr><tr><td>Fans</td> <td>Corsair CO-9050024-WW Air Series SP140 LED 140mm fans x 3</td> <td>n/a</td> </tr><tr><td>Boot Drive</td> <td>SanDisk SSD 64GB</td> <td>£0</td> </tr><tr><td> </td> <td align="RIGHT"><strong>TOTAL</strong></td> <td>£446.63</td> </tr></tbody></table><p>Of course I can't guarantee you'll get the same deals I got, but the pricing should at least be indicative.</p> </div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">The Build</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/20181109_100250-scaled.jpg?itok=INHte5Zt 325w, /sites/default/files/styles/max_650x650/public/2018-11/20181109_100250-scaled.jpg?itok=a6YWE61S 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/20181109_100250-scaled.jpg?itok=FJsfdAzH 1000w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/20181109_100250-scaled.jpg?itok=INHte5Zt" alt="build" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Ok so 3 out of 10 for cable management. This was a large case and a small micro ATX motherboard, and as a consequence practically all of the cables were too short to use the correct cable management channels and features built into the case. I could have bought a bunch of extension cables - but that would have taken more time and probably extended the costs substantially. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">BIOS</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/181108155708.jpg?itok=s4OGWJN3 325w, /sites/default/files/styles/max_650x650/public/2018-11/181108155708.jpg?itok=hXhFRjKk 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/181108155708.jpg?itok=30cxu-Cc 1024w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/181108155708.jpg?itok=s4OGWJN3" alt="CPU Speed" typeof="foaf:Image" class="img-responsive" /> </div> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/181109100838.jpg?itok=A1WezY6e 325w, /sites/default/files/styles/max_650x650/public/2018-11/181109100838.jpg?itok=Xz8YRhVW 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/181109100838.jpg?itok=jnewh8Si 1024w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/181109100838.jpg?itok=A1WezY6e" alt="Boot order" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>Once the build was complete the machine can be spun up. The first image shows rather skewed representation of the CPU temperature - the case is open, the fans are spinning and the CPU is doing nothing!</p> <p>The second image shows the boot order and we can see that the first choice will be the SSD drive (actually manufactured by Hitachi and not SanDisk). The second option is actually a spare USB memory stick I used to store the screenshot. The third is the first volume in the FreeNAS RAID - but that can't be booted. </p></div> </div> </div> <div class="field--item"> <div class="paragraph paragraph--type--blog-heading-picture-text paragraph--view-mode--default"> <div class="field field--name-field-heading field--type-string field--label-hidden field--item">Software upgrade too!</div> <div class="field field--name-field-blog-image field--type-image field--label-hidden field--items"> <div class="field--item"> <img srcset="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-09%20at%2010.45.33.png?itok=vJVa9n8u 325w, /sites/default/files/styles/max_650x650/public/2018-11/Screenshot%202018-11-09%20at%2010.45.33.png?itok=TQ8Yc1NB 650w, /sites/default/files/styles/max_1300x1300/public/2018-11/Screenshot%202018-11-09%20at%2010.45.33.png?itok=iZ9u14nA 1300w, /sites/default/files/styles/max_2600x2600/public/2018-11/Screenshot%202018-11-09%20at%2010.45.33.png?itok=GCWomJQA 2600w" sizes="(min-width: 1290px) 1290px, 100vw" src="/sites/default/files/styles/max_325x325/public/2018-11/Screenshot%202018-11-09%20at%2010.45.33.png?itok=vJVa9n8u" alt="GUI" typeof="foaf:Image" class="img-responsive" /> </div> </div> <div class="field field--name-field-blog-text field--type-text-long field--label-hidden field--item"><p>I used the opportunity to upgrade FreeNAS from 9.10-STABLE to 11.2-STABLE during the process. This is one of the major attractions of FreeNAS - the software upgrade process is simplicity. Providing your config has been saved somewhere, you can trash your old USB FreeNAS knowing full well you just have to download the latest version of FreeNAS, install it, then import the saved config and you are good to go! Furthermore when I was moving my old drives to my new machine I didn't have to make a note of which disk goes in which SATA drive. FreeNAS sorts all this out for you.</p> <p>If you are still using the cloud to save all your personal media - think again. There are so many compelling reasons the cloud is a BAD PLACE. Either buy a NAS or build a FreeNAS! </p></div> </div> </div> </div> <div class="field field--name-field-blog-youtube field--type-entity-reference-revisions field--label-hidden field--items"> <div class="field--item"> <div class="paragraph paragraph--type--blog-text-youtube paragraph--view-mode--default"> </div> </div> </div> <div class="field field--name-field-blog-terms field--type-entity-reference field--label-inline"> <div class="field--label">blog terms</div> <span class="field__items"> <span class="field--item"><a href="/freenas" hreflang="en">Freenas</a></span> <span class="field--item"><a href="/hardware" hreflang="en">Hardware</a></span> </span> </div> Wed, 07 Nov 2018 12:43:04 +0000 nigel 154 at http://badzilla.co.uk