Cree una aplicación de marcadores con FaunaDB, Netlify y 11ty

 

 

 

  • Taller de diseño conductual, con Susan y Guthrie Weinschenk
  • Patrones de diseño de interfaces inteligentes, vídeo de 10h + formación UX

  • Índice
    1. ¿Por qué un sitio de marcadores?
    2. Nuestras tecnologías
      1. Funciones de hosting y sin servidor: Netlify
      2. Base de datos: FaunaDB
      3. Generador de sitios estáticos: 11ty
      4. Atajos de iOS
    3. Configuración de FaunaDB a través de Netlify Dev
      1. Creando nuestros primeros datos
    4. Instalar 11ty y extraer datos en una plantilla
      1. Archivos de datos de 11ty
      2. getBookmarks()
      3. mapBookmarks()
    5. Ingrese a las funciones de Netlify
      1. Flujo Básico
      2. Requisitos
      3. Función de limpieza
      4. getDetails(url)
      5. saveBookmark(details)
      6. rebuildSite()
    6. ¡Hacer un esfuerzo adicional!

    En este artículo, crearemos un sitio de marcadores personales utilizando FaunaDB, Netlify Functions y archivos de datos 11ty.

     

    La revolución JAMstack (JavaScript, API y Markup) está en pleno apogeo. Los sitios estáticos son seguros, rápidos, confiables y divertidos para trabajar. En el corazón de JAMstack se encuentran los generadores de sitios estáticos (SSG) que almacenan sus datos como archivos planos: Markdown, YAML, JSON, HTML, etc. A veces, gestionar los datos de esta forma puede resultar demasiado complicado. A veces, todavía necesitamos una base de datos.

    Con eso en mente, Netlify (un host de sitio estático) y FaunaDB (una base de datos en la nube sin servidor) colaboraron para facilitar la combinación de ambos sistemas.

    ¿Por qué un sitio de marcadores?

    JAMstack es excelente para muchos usos profesionales, pero uno de mis aspectos favoritos de este conjunto de tecnología es su baja barrera de entrada para herramientas y proyectos personales.

    Hay muchos buenos productos en el mercado para la mayoría de las aplicaciones que se me ocurren, pero ninguno está exactamente configurado para mí. Ninguno me daría control total sobre mi contenido. Ninguno vendría sin un costo (monetario o informativo).

    Teniendo esto en cuenta, podemos crear nuestros propios miniservicios utilizando métodos JAMstack. En este caso, crearemos un sitio para almacenar y publicar cualquier artículo interesante que encuentre en mi lectura diaria sobre tecnología.

    Paso mucho tiempo leyendo artículos que se han compartido en Twitter. Cuando me gusta uno, presiono el ícono del "corazón". Luego, al cabo de unos días, es casi imposible encontrarlos debido a la afluencia de nuevos favoritos. Quiero construir algo que se acerque lo más posible a la tranquilidad del “corazón”, pero que sea de mi propiedad y controle.

    ¿Cómo vamos a hacer eso? Me alegra que hayas preguntado.

    ¿Interesado en obtener el código? ¡ Puedes obtenerlo en Github o simplemente implementarlo directamente en Netlify desde ese repositorio! Eche un vistazo al producto terminado aquí .

    Nuestras tecnologías

    Funciones de hosting y sin servidor: Netlify

    Para funciones de alojamiento y sin servidor, utilizaremos Netlify. Como beneficio adicional, con la nueva colaboración mencionada anteriormente, la CLI de Netlify, “Netlify Dev”, se conectará automáticamente a FaunaDB y almacenará nuestras claves API como variables de entorno.

    Base de datos: FaunaDB

    FaunaDB es una base de datos NoSQL “sin servidor”. Lo usaremos para almacenar los datos de nuestros marcadores.

    Generador de sitios estáticos: 11ty

    Soy un gran creyente en HTML. Debido a esto, el tutorial no utilizará JavaScript frontal para representar nuestros marcadores. En su lugar, utilizaremos 11ty como generador de sitios estáticos. 11ty tiene una funcionalidad de datos incorporada que hace que obtener datos de una API sea tan fácil como escribir un par de funciones breves de JavaScript.

    Atajos de iOS

    Necesitaremos una manera fácil de publicar datos en nuestra base de datos. En este caso, usaremos la aplicación Atajos de iOS. Esto también se podría convertir en un bookmarklet JavaScript de Android o de escritorio.

    Configuración de FaunaDB a través de Netlify Dev

    Ya sea que ya se haya registrado en FaunaDB o necesite crear una nueva cuenta, la forma más sencilla de configurar un enlace entre FaunaDB y Netlify es a través de la CLI de Netlify: Netlify Dev. Puede encontrar instrucciones completas de FaunaDB aquí o seguirlas a continuación.

    Netlify Dev ejecutándose en el proyecto final con los nombres de nuestras variables de entorno mostrados ( vista previa grande )

    Si aún no lo tiene instalado, puede ejecutar el siguiente comando en la Terminal:

    npm install netlify-cli -g

    Desde el directorio de su proyecto, ejecute los siguientes comandos:

    netlify init // This will connect your project to a Netlify projectnetlify addons:create fauna // This will install the FaunaDB "addon"netlify addons:auth fauna // This command will run you through connecting your account or setting up an account

    Una vez que todo esto esté conectado, puede ejecutar netlify devsu proyecto. Esto ejecutará cualquier script de compilación que configuremos, pero también se conectará a los servicios Netlify y FaunaDB y tomará las variables de entorno necesarias. ¡Práctico!

    Creando nuestros primeros datos

    Desde aquí, iniciaremos sesión en FaunaDB y crearemos nuestro primer conjunto de datos. Comenzaremos creando una nueva base de datos llamada "marcadores". Dentro de una Base de Datos tenemos Colecciones, Documentos e Índices.

    Una captura de pantalla de la consola de FaunaDB con datos ( vista previa grande )

    Una colección es un grupo categorizado de datos. Cada dato toma la forma de un Documento. Un documento es un "registro único y modificable dentro de una base de datos de FaunaDB", según la documentación de Fauna. Puede pensar en Colecciones como una tabla de base de datos tradicional y en un Documento como una fila.

    Para nuestra aplicación, necesitamos una Colección, a la que llamaremos "enlaces". Cada documento dentro de la colección "enlaces" será un objeto JSON simple con tres propiedades. Para comenzar, agregaremos un nuevo documento que usaremos para crear nuestra primera búsqueda de datos.

    {"url": "https://css-irl.info/debugging-css-grid-part-2-what-the-fraction/","pageTitle": "CSS { In Real Life } | Debugging CSS Grid – Part 2: What the Fr(action)?","description": "CSS In Real Life is a blog covering CSS topics and useful snippets on the web’s most beautiful language. Published by Michelle Barker, front end developer at Ordoo and CSS superfan."}

    Esto crea la base para la información que necesitaremos extraer de nuestros marcadores y también nos proporciona nuestro primer conjunto de datos para incorporar a nuestra plantilla.

    Si eres como yo, querrás ver los frutos de tu trabajo de inmediato. ¡Pongamos algo en la página!

    Instalar 11ty y extraer datos en una plantilla

    Como queremos que los marcadores se representen en HTML y que el navegador no los recupere, necesitaremos algo para realizar la representación. Hay muchas formas excelentes de hacerlo, pero por su facilidad y potencia, me encanta usar el generador de sitios estáticos 11ty.

     

    Dado que 11ty es un generador de sitios estáticos de JavaScript, podemos instalarlo a través de NPM.

    npm install --save @11ty/eleventy

    Desde esa instalación, podemos ejecutar eleventyo eleventy --serveen nuestro proyecto ponernos en marcha.

    Netlify Dev a menudo detectará 11ty como un requisito y ejecutará el comando por nosotros. Para que esto funcione y asegurarnos de que estamos listos para la implementación, también podemos crear comandos "servir" y "compilar" en nuestro archivo package.json.

    "scripts": { "build": "npx eleventy", "serve": "npx eleventy --serve" }

    Archivos de datos de 11ty

    La mayoría de los generadores de sitios estáticos tienen incorporada la idea de un "archivo de datos". Por lo general, estos archivos serán archivos JSON o YAML que le permitirán agregar información adicional a su sitio.

    En 11ty, puede utilizar archivos de datos JSON o archivos de datos JavaScript. Al utilizar un archivo JavaScript, podemos realizar nuestras llamadas API y devolver los datos directamente a una plantilla.

    De forma predeterminada, 11ty quiere que los archivos de datos se almacenen en un _datadirectorio. Luego puede acceder a los datos utilizando el nombre del archivo como variable en sus plantillas. En nuestro caso, crearemos un archivo en _data/bookmarks.jsy accederemos a él a través del {{ bookmarks }}nombre de la variable.

    Si desea profundizar en la configuración de archivos de datos, puede leer ejemplos en la documentación de 11ty o consultar este tutorial sobre el uso de archivos de datos de 11ty con la API de Meetup .

    El archivo será un módulo JavaScript. Entonces, para que algo funcione, necesitamos exportar nuestros datos o una función. En nuestro caso, exportaremos una función.

    module.exports = async function() { const data = mapBookmarks(await getBookmarks()); return data.reverse()}

    Analicemos eso. Tenemos dos funciones haciendo nuestro trabajo principal aquí: mapBookmarks()y getBookmarks().

    La getBookmarks()función buscará nuestros datos de nuestra base de datos FaunaDB y mapBookmarks()tomará una serie de marcadores y los reestructurará para que funcionen mejor en nuestra plantilla.

    Profundicemos en getBookmarks().

    getBookmarks()

    Primero, necesitaremos instalar e inicializar una instancia del controlador JavaScript de FaunaDB.

    npm install --save faunadb

    Ahora que lo hemos instalado, agreguémoslo al principio de nuestro archivo de datos. Este código proviene directamente de los documentos de Fauna .

    // Requires the Fauna module and sets up the query module, which we can use to create custom queries.const faunadb = require('faunadb'), q = faunadb.query;// Once required, we need a new instance with our secretvar adminClient = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET});

    Después de eso, podemos crear nuestra función. Comenzaremos creando nuestra primera consulta utilizando métodos integrados en el controlador. Este primer fragmento de código devolverá las referencias de la base de datos que podemos usar para obtener datos completos de todos nuestros enlaces marcados. Usamos el Paginatemétodo como ayuda para administrar el estado del cursor en caso de que decidamos paginar los datos antes de entregárselos a 11ty. En nuestro caso, simplemente devolveremos todas las referencias.

     

    En este ejemplo, supongo que instaló y conectó FaunaDB a través de Netlify Dev CLI. Con este proceso, obtienes variables de entorno local de los secretos de FaunaDB. Si no lo instaló de esta manera o no lo está ejecutando netlify deven su proyecto, necesitará un paquete como dotenvpara crear las variables de entorno. También deberá agregar sus variables de entorno a la configuración de su sitio Netlify para que las implementaciones funcionen más adelante.

    adminClient.query(q.Paginate(q.Match( // Match the reference belowq.Ref("indexes/all_links") // Reference to match, in this case, our all_links index))).then( response = { ... })

    Este código devolverá una matriz de todos nuestros enlaces en forma de referencia. Ahora podemos crear una lista de consultas para enviar a nuestra base de datos.

    adminClient.query(...) .then((response) = { const linkRefs = response.data; // Get just the references for the links from the response const getAllLinksDataQuery = linkRefs.map((ref) = { return q.Get(ref) // Return a Get query based on the reference passed in})return adminClient.query(getAllLinksDataQuery).then(ret = { return ret // Return an array of all the links with full data})}).catch(...)

    A partir de aquí, sólo necesitamos limpiar los datos devueltos. ¡ Ahí es donde mapBookmarks()entra en juego! Cine de Calidad gratis

    mapBookmarks()

    En esta función, nos ocupamos de dos aspectos de los datos.

    Primero, obtenemos una fecha y hora gratuita en FaunaDB. Para cualquier dato creado, existe una tspropiedad de marca de tiempo (). No está formateado de una manera que haga feliz al filtro de fecha predeterminado de Liquid, así que solucionemos eso.

    function mapBookmarks(data) { return data.map(bookmark = { const dateTime = new Date(bookmark.ts / 1000); ... })}

    Una vez aclarado esto, podemos crear un nuevo objeto para nuestros datos. En este caso, tendrá una timepropiedad y usaremos el operador Spread para desestructurar nuestro dataobjeto y hacer que todos vivan en un nivel.

    function mapBookmarks(data) { return data.map(bookmark = { const dateTime = new Date(bookmark.ts / 1000); return { time: dateTime, ...bookmark.data } })}

    Aquí están nuestros datos antes de nuestra función:

    { ref: Ref(Collection("links"), "244778237839802888"), ts: 1569697568650000, data: { url: 'https://sample.com', pageTitle: 'Sample title', description: 'An escaped description goes here' }}

    Aquí están nuestros datos después de nuestra función:

    { time: 1569697568650, url: 'https://sample.com', pageTitle: 'Sample title' description: 'An escaped description goes here'}

    ¡Ahora tenemos datos bien formateados que están listos para nuestra plantilla!

     

    Escribamos una plantilla simple. Revisaremos nuestros marcadores y validaremos que cada uno tenga un pageTitley un urlpara no parecer tontos.

    div {% for link in bookmarks %} {% if link.url and link.pageTitle %} // confirms there’s both title AND url for safety div h2a href="{{ link.url }}"{{ link.pageTitle }}/a/h2 pSaved on {{ link.time | date: "%b %d, %Y" }}/p {% if link.description != "" %} p{{ link.description }}/p {% endif %} /div {% endif %} {% endfor %}/div

    Ahora estamos ingiriendo y mostrando datos de FaunaDB. ¡Tomemos un momento y pensemos en lo bueno que es que esto genere HTML puro y no haya necesidad de recuperar datos en el lado del cliente!

    Pero eso no es suficiente para que esta sea una aplicación útil para nosotros. Descubramos una manera mejor que agregar un marcador en la consola de FaunaDB.

    Ingrese a las funciones de Netlify

    El complemento Funciones de Netlify es una de las formas más sencillas de implementar funciones lambda de AWS. Como no hay ningún paso de configuración, es perfecto para proyectos de bricolaje en los que solo deseas escribir el código.

    Esta función residirá en una URL de su proyecto que se ve así: https://myproject.com/.netlify/functions/bookmarksasumiendo que el archivo que creamos en nuestra carpeta de funciones es bookmarks.js.

    Flujo Básico

    1. Pase una URL como parámetro de consulta a nuestra función URL.
    2. Utilice la función para cargar la URL y eliminar el título y la descripción de la página, si están disponibles.
    3. Formatee los detalles de FaunaDB.
    4. Envíe los detalles a nuestra colección FaunaDB.
    5. Reconstruir el sitio.

    Requisitos

    Tenemos algunos paquetes que necesitaremos mientras desarrollamos esto. Usaremos la CLI netlify-lambda para construir nuestras funciones localmente. request-promisees el paquete que usaremos para realizar solicitudes. Cheerio.js es el paquete que usaremos para extraer elementos específicos de nuestra página solicitada (piense en jQuery para Node). Y finalmente, necesitaremos FaunaDb (que ya debería estar instalado).

    npm install --save netlify-lambda request-promise cheerio

    Una vez instalado, configuremos nuestro proyecto para construir y servir las funciones localmente.

    Modificaremos nuestros scripts de “construcción” y “servicio” en nuestro package.jsonpara que se vean así:

    "scripts": { "build": "npx netlify-lambda build lambda --config ./webpack.functions.js npx eleventy", "serve": "npx netlify-lambda build lambda --config ./webpack.functions.js npx eleventy --serve"}

    Advertencia: hay un error con el controlador NodeJS de Fauna al compilar con Webpack, que las funciones de Netlify utilizan para compilar. Para solucionar esto , necesitamos definir un archivo de configuración para Webpack. Puede guardar el siguiente código en un archivo webpack.config.js.

    const webpack = require('webpack');module.exports = { plugins: [ new webpack.DefinePlugin({ "global.GENTLY": false }) ]};

    Una vez que exista este archivo, cuando usemos el netlify-lambdacomando, necesitaremos decirle que se ejecute desde esta configuración. Es por eso que nuestros scripts de “servicio” y “compilación” usan el --configvalor para ese comando.

     

    Función de limpieza

    Para mantener nuestro archivo de función principal lo más limpio posible, crearemos nuestras funciones en un bookmarksdirectorio separado y las importaremos a nuestro archivo de función principal.

    import { getDetails, saveBookmark } from "./bookmarks/create";

    getDetails(url)

    La getDetails()función tomará una URL, pasada desde nuestro controlador exportado. Desde allí, accederemos al sitio en esa URL y tomaremos partes relevantes de la página para almacenarlas como datos para nuestro marcador.

    Comenzamos solicitando los paquetes de NPM que necesitamos:

    const rp = require('request-promise');const cheerio = require('cheerio');

    Luego, usaremos el request-promisemódulo para devolver una cadena HTML para la página solicitada y la pasaremos cheeriopara brindarnos una interfaz muy similar a jQuery.

    const getDetails = async function(url) { const data = rp(url).then(function(htmlString) { const $ = cheerio.load(htmlString); ...}

    Desde aquí, necesitamos obtener el título de la página y una meta descripción. Para hacer eso, usaremos selectores como lo haría en jQuery.

    Nota: En este código, utilizamos 'head title' como selector para obtener el título de la página. Si no especifica esto, puede terminar obteniendo title etiquetas dentro de todos los SVG de la página, lo cual no es nada ideal.

    const getDetails = async function(url) { const data = rp(url).then(function(htmlString) { const $ = cheerio.load(htmlString); const title = $('head title').text(); // Get the text inside the tag const description = $('meta[name="description"]').attr('content'); // Get the text of the content attribute// Return out the data in the structure we expect return { pageTitle: title, description: description }; }); return data //return to our main function}

    Con los datos en la mano, ¡es hora de enviar nuestro marcador a nuestra Colección en FaunaDB!

    saveBookmark(details)

    Para nuestra función de guardar, queremos pasar los detalles que obtuvimos getDetailsasí como la URL como un objeto singular. ¡El operador Spread ataca de nuevo!

    const savedResponse = await saveBookmark({url, ...details});

    En nuestro create.jsarchivo, también necesitamos solicitar y configurar nuestro controlador FaunaDB. Esto debería resultarle muy familiar en nuestro archivo de datos 11ty.

    const faunadb = require('faunadb'), q = faunadb.query;const adminClient = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET});

    Una vez que hayamos eliminado eso, podemos codificar.

    Primero, necesitamos formatear nuestros detalles en una estructura de datos que Fauna espera para nuestra consulta. Fauna espera un objeto con una propiedad de datos que contenga los datos que deseamos almacenar.

     

    const saveBookmark = async function(details) {const data = {data: details};...}

    Luego abriremos una nueva consulta para agregar a nuestra Colección. En este caso, usaremos nuestro asistente de consulta y usaremos el método Create. Create() toma dos argumentos. El primero es la Colección en la que queremos almacenar nuestros datos y el segundo son los datos en sí.

    Después de guardar, devolvemos el éxito o el fracaso a nuestro controlador.

    const saveBookmark = async function(details) {const data = { data: details};return adminClient.query(q.Create(q.Collection("links"), data)) .then((response) = { /* Success! return the response with statusCode 200 */ return { statusCode: 200, body: JSON.stringify(response) } }).catch((error) = { /* Error! return the error with statusCode 400 */ return { statusCode: 400, body: JSON.stringify(error) } })}

    Echemos un vistazo al archivo de función completo.

    import { getDetails, saveBookmark } from "./bookmarks/create";import { rebuildSite } from "./utilities/rebuild"; // For rebuilding the site (more on that in a minute)exports.handler = async function(event, context) { try { const url = event.queryStringParameters.url; // Grab the URL const details = await getDetails(url); // Get the details of the page const savedResponse = await saveBookmark({url, ...details}); //Save the URL and the details to Fauna if (savedResponse.statusCode === 200) { // If successful, return success and trigger a Netlify build await rebuildSite(); return { statusCode: 200, body: savedResponse.body } } else { return savedResponse //or else return the error } } catch (err) { return { statusCode: 500, body: `Error: ${err}` }; }};

    rebuildSite()

    El ojo perspicaz notará que tenemos una función más importada a nuestro controlador: rebuildSite(). Esta función utilizará la funcionalidad Deploy Hook de Netlify para reconstruir nuestro sitio a partir de los nuevos datos cada vez que enviemos un nuevo guardado de marcador exitoso.

    En la configuración de su sitio en Netlify, puede acceder a la configuración de Build Deploy y crear un nuevo "Build Hook". Los ganchos tienen un nombre que aparece en la sección Implementar y una opción para implementar una rama no maestra si así lo desea. En nuestro caso, lo llamaremos "new_link" e implementaremos nuestra rama maestra.

    Una referencia visual para la configuración del enlace de compilación del administrador de Netlify ( vista previa grande )

    Desde allí, sólo necesitamos enviar una solicitud POST a la URL proporcionada.

    Necesitamos una forma de realizar solicitudes y, como ya lo hemos instalado request-promise, continuaremos usando ese paquete solicitándolo en la parte superior de nuestro archivo.

    const rp = require('request-promise');const rebuildSite = async function() { var options = { method: 'POST', uri: 'https://api.netlify.com/build_hooks/5d7fa6175504dfd43377688c', body: {}, json: true }; const returned = await rp(options).then(function(res) { console.log('Successfully hit webhook', res); }).catch(function(err) { console.log('Error:', err); }); return returned}
    Una referencia visual para la configuración de nuestra función de acceso directo ( vista previa grande )

    La aplicación Atajos de Apple permite crear elementos personalizados para incluirlos en su hoja para compartir. Dentro de estos accesos directos, podemos enviar varios tipos de solicitudes de datos recopilados en el proceso de compartir.

    Aquí está el atajo paso a paso:

    1. Acepte cualquier artículo y guárdelo en un bloque de "texto".
    2. Pase ese texto a un bloque de "Scripting" para codificar la URL (por si acaso).
    3. Pase esa cadena a un bloque de URL con la URL de nuestra función Netlify y un parámetro de consulta de url.
    4. Desde "Red", use un bloque "Obtener contenido" para PUBLICAR en JSON en nuestra URL.
    5. Opcional: Desde “Scripting” “Mostrar” el contenido del último paso (para confirmar los datos que estamos enviando).

    Para acceder a esto desde el menú para compartir, abrimos la configuración de este acceso directo y activamos la opción "Mostrar en hoja para compartir".

    A partir de iOS13, estas “acciones” compartidas se pueden marcar como favoritas y mover a una posición alta en el cuadro de diálogo.

    ¡Ahora tenemos una “aplicación” funcional para compartir marcadores en múltiples plataformas!

    ¡Hacer un esfuerzo adicional!

    Si está inspirado para probar esto usted mismo, existen muchas otras posibilidades para agregar funcionalidad. Lo bueno de la web DIY es que puedes hacer que este tipo de aplicaciones funcionen para ti. Aqui hay algunas ideas:

    1. Utilice una “clave API” falsa para una autenticación rápida, de modo que otros usuarios no publiquen en su sitio (el mío usa una clave API, ¡así que no intente publicar en él!).
    2. Agregue funcionalidad de etiquetas para organizar marcadores.
    3. Agregue una fuente RSS para su sitio para que otros puedan suscribirse.
    4. Envíe un correo electrónico de resumen semanal mediante programación para los enlaces que haya agregado.

    Realmente, el cielo es el límite, ¡así que empieza a experimentar!

    (dm, yk)Explora más en

    • javascript
    • Aplicaciones
    • once





    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Cree una aplicación de marcadores con FaunaDB, Netlify y 11ty

    Cree una aplicación de marcadores con FaunaDB, Netlify y 11ty

    Taller de diseño conductual, con Susan y Guthrie Weinschenk Patrones de diseño de interfaces inteligentes, vídeo de 10h + formación UX Índice

    programar

    es

    https://aprendeprogramando.es/static/images/programar-cree-una-aplicacion-de-marcadores-con-faunadb-1004-0.jpg

    2024-05-21

     

    Cree una aplicación de marcadores con FaunaDB, Netlify y 11ty
    Cree una aplicación de marcadores con FaunaDB, Netlify y 11ty

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20