Sistemas y TecnologĂ­as Web: Servidor

Master de II. ULL. 1er cuatrimestre. 2020/2021


Organization ULL-MII-SYTWS-2021   Classroom ULL-MII-SYTWS-2021   Campus Virtual SYTWS   Chat Chat   Profesor Casiano

Table of Contents

PrĂĄctica p10-t3-jekyll-search

Adding a Simple Search to our Jekyll Site

El ejercicio consiste en que añada la capacidad de bĂșsqueda al sitio asociado a alguna de las prĂĄcticas, por ejemplo a p8-t3-jekyll-netlify.

Estos son algunos requisitos:

  • Queremos que busque en todos los ficheros, no solo los de los posts sino tambiĂ©n los de las pĂĄginas
  • Que admita expresiones regulares
  • Queremos que los resultados vayan apareciendo conforme tecleamos
  • Se mostrarĂĄ una lista de enlaces a los ficheros que contienen la expresiĂłn buscada y un resumen de las primeros caracteres del fichero

ÂżComo hacerlo?

  1. Since Jekyll has no server side execution, we have to rely on storing all the required content in a single file and search our keyword from that file.
  2. We will be creating a JSON file in which we will store title, url, content, excerpt, etc., at building time

    1
    2
    
     $ bundle exec jekyll build
     $ head -n 30 _site/assets/src/search.json 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     [
         {
             "title": "Clase del Lunes 30/09/2019",
             "excerpt": "Clase del Lunes 30/09/2019\n\n",         ⇐ Resumen
             "content": "Clase del Lunes 30/09/2019\n\n\n  ...",  ⇐ Contenido del fichero
             "url": "/clases/2019/09/30/leccion.html"
         },
         ...
     ]
    

Liquid template search.json to generate at build time the _site/assets/src/search.json

Para lograrlo usaremos este template Liquid: search.json (protected)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- - -
layout: null
sitemap: false
- - -

{ % capture json % }
[
  { % assign collections = site.collections | where_exp:'collection','collection.output != false' %}
  { % for collection in collections %}
    { % assign docs = collection.docs | where_exp:'doc','doc.sitemap != false' %}
    { % for doc in docs %}
      {
        "title": { { doc.title | jsonify }},
        "excerpt": { { doc.excerpt | markdownify | strip_html | jsonify }},
        "content": { { doc.content | markdownify | strip_html | jsonify }},
        "url": { { site.baseurl | append: doc.url | jsonify }}
      },
    { % endfor %}
  { % endfor %}
  { % assign pages = site.html_pages | where_exp:'doc','doc.sitemap != false' | where_exp:'doc','doc.title != null' %}
  { % for page in pages % }
  {
    "title": { { page.title | jsonify }},
    "excerpt": { { page.excerpt | markdownify | strip_html | jsonify }},
    "content": { { page.content | markdownify | strip_html | jsonify }},
    "url": { { site.baseurl | append: page.url | jsonify }}
  }{ % unless forloop.last %},{ % endunless %}
  { % endfor %}
]
{ % endcapture %}

{ { json | lstrip }}

Entendiendo la lĂ­nea "content": { { page.content | markdownify | strip_html | jsonify }},

  • page.content el contenido de la pĂĄgina todavia sin renderizar (se supone que es fundamentalmente markdown, pero puede contener yml en el front-matter, html, scripts, liquid, etc.)
  • markdownify: Convert a Markdown-formatted string into HTML.
  • strip_html: Removes any HTML tags from a string.
  • jsonify: If the data is an array or hash you can use the jsonify filter to convert it to JSON.

Por ejemplo, supongamos que tenemos estas definiciones en el front-matter de nuestra pĂĄgina:

1
2
3
4
5
6
7
chuchu: "Cadena **negritas** e *italicas*"
html: "<h1>hello</h1> <b>world</b>"
colors:
  - red
  - blue
  - green
---

y que en el contenido de nuestra pĂĄgina tenemos algo asĂ­:

1
2
3
4
5
Compara < script>{ { page.chuchu }} </script> con su markdownify: < script>{ { page.chuchu | markdownify }}</script>

Compara < script> { { page.colors}} </script> con su jsonify: < script>{ { page.colors | jsonify }} </script>

Compara < script>{ {page.html}}</script> con su `strip_html` < script> { { page.html | strip_html }} </script>

Esta es la salida que produce jekyll 4.0.0:

1
2
3
4
5
6
<p>Compara < script>Cadena **negritas** e *italicas* </script> con su markdownify: < script>&lt;p&gt;Cadena <strong>negritas</strong> e <em>italicas</em>&lt;/p&gt;
</script></p>

<p>Compara < script> redbluegreen </script> con su jsonify: < script>["red","blue","green"] </script></p>

<p>Compara < script>&lt;h1&gt;hello&lt;/h1&gt; <b>world</b></script> con su <code class="highlighter-rouge">strip_html</code> < script> hello world </script></p>

La idea general es que necesitamos suprimir los tags, tanto yml, markdown, HTML, etc. para que no confundan al método de busca. Por eso convertimos el markdown a HTML y después suprimimos los tags HTML. También convertimos el yml a JSON.

La pĂĄgina de BĂșsqueda: search.md

Fichero search.md:

La idea es que vamos a escribir una clase JekyllSearch que implementa la bĂșsqueda. Debe disponer de un constructor al que se le pasan cuatro argumentos:

  1. La ruta donde esta disponible el fichero .json generado durante la construcciĂłn (jekyll build)
  2. El id del objeto del DOM en la pĂĄgina en la que estĂĄ el tag input de la bĂșsqueda
  3. El iddel objeto del DOM en el que se deben volcar los resultados
  4. La url del lugar en el que estĂĄ el deployment (pudiera ser que el site en el que queremos buscar fuera una subcarpeta de todo el site)
1
2
3
4
5
6
7
const search = new JekyllSearch(
    '/assets/src/search.json',
    '#search',
    '#list',
    ''
  );
  search.init(); 

Los objetos JekyllSearch deben disponer de un mĂ©todo init que realiza la bĂșsqueda especificada en el elemento del DOM #search y añade los resultados en en el elemento del DOM #list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- - -
layout: error 
permalink: /search/
title: Search
- - -

{ % capture initSearch % }

<h1>Search</h1>

<form id="search-form" action="">
  <label class="label" for="search">Search term (accepts a regex):</label>
  <br/>
  <input class="input" id="search" type="text" name="search" autofocus placeholder="e.g. Promise" autocomplete="off">
  
  <ul class="list  list--results" id="list">
  </ul>
</form>

< script type="text/javascript" src="/assets/src/fetch.js"></script>
< script type="text/javascript" src="/assets/src/search.js"></script>


< script type="text/javascript">

  const search = new JekyllSearch(
    '{ {site.url}  }/assets/src/search.json',
    '#search',
    '#list',
    '{ {site.url}  }'
  );
  search.init(); 
  
</script>

<noscript>Please enable JavaScript to use the search form.</noscript>

{ % endcapture % }

{ { initSearch | lstrip } }
  • autocomplete="off"
    • En algunos casos, el navegador continuarĂĄ sugiriendo valores de autocompletado incluso si el atributo autocompletar estĂĄ desactivado. Este comportamiento inesperado puede resultar bastante confuso para los desarrolladores. El truco para realmente no aplicar el autocompletado es asignar un valor no vĂĄlido al atributo, por ejemplo:
    1
    
    autocomplete="nope"
    

    Dado que este valor no es vĂĄlido para el atributo autocompletar, el navegador no tiene forma de reconocerlo y deja de intentar autocompletarlo.

  • Filters are simple methods that modify the output of numbers, strings, variables and objects. They are placed within an output tag { { } } and are denoted by a pipe character |.
  • Clearing Up Confusion Around baseurl. About site.url vs site.baseurl

https://byparker.com/img/what-is-a-baseurl.jpg

La clase JekyllSearch: Fichero search.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class JekyllSearch {
  constructor(dataSource, searchField, resultsList, siteURL) {
    this.dataSource = dataSource                           // ruta al fichero json
    this.searchField = document.querySelector(searchField) // DOM el. del input
    this.resultsList = document.querySelector(resultsList) // DOM el. para el output
    this.siteURL = siteURL                                 // folder

    this.data = '';
  }

  fetchedData() {
    return fetch(this.dataSource)
      .then(blob => blob.json())
  }

  async findResults() {
    this.data = await this.fetchedData()
    const regex = new RegExp(this.searchField.value, 'gi')
    return this.data.filter(item => {
      return item.title.match(regex) || item.content.match(regex)
    })
  }

  async displayResults() {
    const results = await this.findResults()
    const html = results.map(item => {
      return `
        <li class="result">
            <article class="result__article  article">
                <h4>
                  <a href="${this.siteURL + item.url}">${item.title}</a>
                </h4>
                <p>${item.excerpt}</p>
            </article>
        </li>`
    }).join('')
    if ((results.length == 0) || (this.searchField.value == '')) {
      this.resultsList.innerHTML = `<p>Sorry, nothing was found</p>`
    } else {
      this.resultsList.innerHTML = html
    }
  }

  // https://stackoverflow.com/questions/43431550/async-await-class-constructor
  init() {
    const url = new URL(document.location)
    if (url.searchParams.get("search")) {
      this.searchField.value = url.searchParams.get("search") // Update the attribute
      this.displayResults()
    }
    this.searchField.addEventListener('keyup', () => {
      this.displayResults()
      // So that when going back in the browser we keep the search
      url.searchParams.set("search", this.searchField.value)
      window.history.pushState('', '', url.href)
    })
    
    // to not send the form each time <enter> is pressed
    this.searchField.addEventListener('keypress', event => {
      if (event.keyCode == 13) {
        event.preventDefault()
      }
    })
  }
}

url.searchParams

If the URL of your page is https://example.com/?name=Jonathan%20Smith&age=18 you could parse out the name and age parameters using:

1
2
3
let params = (new URL(document.location)).searchParams;
let name = params.get('name'); // is the string "Jonathan Smith".
let age = parseInt(params.get('age')); // is the number 18

Live collections

All methods getElementsBy* return a live collection. Such collections always reflect the current state of the document and auto-update when it changes. In contrast, querySelectorAll returns a static collection. It’s like a fixed array of elements.

1
2
3
4
5
6
7
8
constructor(dataSource, searchField, resultsList, siteURL) {
    this.dataSource = dataSource                           // ruta al fichero json
    this.searchField = document.querySelector(searchField) // DOM el. del input
    this.resultsList = document.querySelector(resultsList) // DOM el. para el output
    this.siteURL = siteURL                                 // folder

    this.data = '';
  }

window.history.pushState

The window object provides access to the browser’s session history through the history object. The history.pushState(state, title, url) method adds a state to the browser’s session history stack.

1
2
3
4
5
6
7
   ... // inside init
   this.searchField.addEventListener('keyup', () => {
      this.displayResults()
      // So that when going back in the browser we keep the search
      url.searchParams.set("search", this.searchField.value)
      window.history.pushState('', '', url.href)
    })

Caching

The resources downloaded through fetch(), similar to other resources that the browser downloads, are subject to the HTTP cache.  

1
2
3
fetchedData() {
    return fetch(this.dataSource).then(blob => blob.json())
  }

This is usually fine, since it means that if your browser has a cached copy of the response to the HTTP request, it can use the cached copy instead of wasting time and bandwidth re-downloading from a remote server.

The search.json is not going to change until the next push

Fetch Polyfill

Polyfill
A polyfill is a piece of code (usually JavaScript on the Web) used to provide modern functionality on older browsers that do not natively support it. The polyfill uses non-standard features in a certain browser to give JavaScript a standards-complaint way to access the feature

Estructura del sitio

Esta imagen muestra los ficheros implicados en este ejercicio dentro de la estructura del sitio de estos apuntes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ tree -I _site
.
├── 404.md
├── assets
│   ├── css
│   │   └── style.scss
│   ├── images
│   │   ├── event-emitter-methods.png
│   │   └── ,,,
│   └── src
│       ├── fetch.js          ⇐ Polyfill for fetch
│       ├── search.js         ⇐ LibrerĂ­a con la Clase JekyllSearch que implementa el CĂłdigo de bĂșsqueda
│       └── search.json       ⇐ Plantilla Liquid para generar el fichero JSON 
├── search.md                 ⇐ PĂĄgina de la bĂșsqueda. Formulario y script de arranque 
├── clases.md
├── _config.yml               ⇐ Fichero de configuración de Jekyll
├── degree.md
├── favicon.ico
├── Gemfile
├── Gemfile.lock
├── _includes                 ⇐ The include tag allows to include the content of files stored here
│   ├── navigation-bar.html
│   └── ...
├── _layouts                  ⇐ templates that wrap around your content
│   ├── default.html
│   ├── error.html
│   └── post.html
├── _posts                    ⇐ File names must follow YEAR-MONTH-DAY-title.MARKUP and  must begin with front matter 
│   ├── ...
│   └── 2019-12-02-leccion.md
├── _practicas                ⇐ Folder for the collection "practicas" (list of published "practicas") 
│   ├── ...
│   └── p9-t3-transfoming-data.md 
├── practicas.md              ⇐ { % for practica in site.practicas % } ... { % endfor % }
├── Rakefile                  ⇐ For tasks
├── README.md
├── references.md
├── resources.md
├── tema0-presentacion        ⇐  Pages folders 
│   ├── README.md
│   └── ...
├── tema ...
├── tfa
│   └── README.md
└── timetables.md

58 directories, 219 files

Referencias

Comment with GitHub Utterances

Comment with Disqus

thread de discusion