Entradas relacionadas empleando la API v3.0 de Blogger
Al emplear el protocolo HTTPS en mi blog, me percaté que cierto elemento había desaparecido de mis entradas; se trataba de LinkWithin, un viejo, sencillo y popular servicio que te permite agregar un widget para vincular los artículos con otros similares. La razón se debe a que el script que emplea el sitio no es compatible con el mencionado protocolo.
En la web hay una variedad de alternativas, una de ellas es empleando la API de Blogger sin intermediarios, opción por la que opté; sin embargo, después de una búsqueda superficial, noté que la mayoría de las guías de la comunidad empleaban la versión 2.0, así que quise explorar un poco la versión 3.0, la cual resultó ser mucho más limpia.
Hay dos maneras de realizar este ejercicio, pero lo habitual consiste en vincular las entradas que compartan una misma etiqueta. Blogger permite combinar varias etiquetas en una sola consulta, pero solo te devuelve las entradas que las incluya todas, no una de ellas, limitando su alcance. La segunda opción es realizar una simple búsqueda, pero esta operación tiene sus limitaciones:
- Solo se puede obtener diez resultados como máximo. El parámetro MaxResult no funciona para esta operación.
- No proporciona imágenes, por lo que no se puede obtener las miniaturas. El parámetro fetchImages no funciona para esta operación.
Me conformé con solo apoyarme en la primera etiqueta de la entrada y acepté la imposición con respecto al número máximo de resultados de una búsqueda, puesto que, sin recurrir a más peticiones, es algo que está fuera de mi alcance; pero con el caso de las miniaturas, sí pude hacer algo, aunque no es eficiente: solicitar el contenido de las entradas para buscar la imagen.
El script permite relacionar las entradas por una de sus etiquetas o por una búsqueda basada en ellas y en el contenido marcado por <strong> y <em>. Además, brinda la posibilidad de mostrarlas con miniaturas o como una simple lista de enlaces.
Antes que nada, para hacer uso de la API de Blogger se debe poseer una clave y el ID del blog (blogId); la primera se obtiene en este sitio y la segunda en la barra de direcciones del navegador al visitar nuestro escritorio. Por otro lado, el script toma como referencia las clases post-title, post-body y post-labels para hacer su trabajo, por lo que su presencia en la plantilla del blog es imperativa.
Como primer paso, se hace necesario la ubicación del contenedor que albergará las entradas relacionadas, el cual bastará con colocarlo antes de <div class='post-footer'>:
<b:if cond='data:blog.pageType == "item"'>
<div class='related-posts'></div>
</b:if>
Por lo general, las entradas relacionadas aparecen en la página de la entrada, mas no en una lista de entradas, es de ahí el uso de la condición data:blog.pageType == "item"; sin embargo, el script debería funcionar en ambos escenarios.
Los estilos básicos deben ir antes de </head>:
<style>
.related-posts > div {
display: flex;
}
.related-posts a.thumbnail {
flex: 1 1 0;
padding: 8px;
max-width: 230px; /* Ancho máximo que puede adoptar cada entrada, el ancho de cada miniatura se ajusta a ello. */
text-align: center;
text-decoration: none;
}
.related-posts a.thumbnail:hover {
background: #f7f7f7;
}
.related-posts a.thumbnail + a.thumbnail {
border-left: 1px solid #cccccc; /* Separadores */
}
.related-posts a.no-image {
display: flex;
}
.related-posts a.no-image p {
align-self: center;
}
.related-posts img {
margin-bottom: 5px;
width: 100%;
height: 110px; /* Alto fijo de cada miniatura */
object-fit: cover;
border: 0;
}
.related-posts p {
margin: 0;
}
</style>
El script, el cual debe estar justo antes de </body>, es el siguiente:
<script>
//<![CDATA[
const search = true; //Si se asigna "true", emplea el método "search" en vez de listar las entradas por etiquetas.
const h4Text = "También te puede interesar:"; //Cabecera de las entradas relacionadas.
const maxItems = 5; //Número máximo de entradas a mostrar. Hasta 10 para el método "search".
const postImage = true; //Muestra una miniatura por cada entrada. Si se asigna "false", se mostrará una simple lista de enlaces.
const defaultImg = ""; //URL de la imagen miniatura por defeto.
const maxResults = 10; //Número de resultados por consulta. No funciona para el método "search".
const blogId = "ID-DEL-BLOG"; //Es el ID del blog.
const apiKey = "CLAVE-DE-LA-API"; //Es la clave necesaria para emplear la API de Blogger.
document.querySelectorAll(".post").forEach(post => {
const relatedPosts = post.querySelector(".related-posts");
if (relatedPosts) { //Verifica que el contenedor de las entradas relacionadas exista en la entrada.
const postTitle = post.querySelector(".post-title").textContent.trim(); //Obtiene el título de la entrada.
//Arma la consulta.
if (search) { //Verifica si el método empleado para la consulta es "search".
//Obtiene las palabras claves especificadas en la entrada.
const keyHTMLTags = Array.from(post.querySelectorAll(".post-body strong, .post-body em, .post-body .keyword"));
const keywords = keyHTMLTags.map(item => encodeURIComponent(item.textContent.trim()));
//Obtiene las etiquetas de la entrada.
const postLabels = Array.from(post.querySelectorAll(".post-labels a"));
const labels = postLabels.map(item => encodeURIComponent(item.textContent.trim()));
//Prepara la consulta.
var query = [...new Set([...keywords, ...labels])];
query = query.join("|");
} else {
var postLabel = post.querySelector(".post-labels a").textContent.trim(); //Obtiene la primera etiqueta de la entrada.
}
//Genera la URL de la API.
let apiUrl;
if (search) {
apiUrl = `https://www.googleapis.com/blogger/v3/blogs/${blogId}/posts/search?q=${query}&fields=items(title,${postImage ? "content," : ""}url)&key=${apiKey}`;
} else {
apiUrl = `https://www.googleapis.com/blogger/v3/blogs/${blogId}/posts?labels=${postLabel}&fetchImages=${postImage ? "true" : "false"}&fields=items(title,${postImage ? "images," : ""}url)&maxResults=${maxResults}&key=${apiKey}`;
}
fetch(apiUrl)
.then(response => response.json())
.then(data => {
let posts = data.items; //Obtiene las entradas relacionadas.
let htmlList = "";
if (posts) { //Verifica si se devolvió alguna entrada.
let counter = 0; //Cuenta las entradas añadidas a la lista de entradas relacionadas.
//Arma la lista, teniendo en cuenta el número de entradas solicitadas y el número de entradas devueltas.
while (counter < maxItems && posts.length > 0) {
//Elige una entrada relacionada aleatoria.
const index = Math.floor(Math.random() * posts.length);
const post = posts[index];
posts.splice(index, 1);
//Elabora la lista de las entradas relacionadas en formato HTML.
//La siguiente condición verifica que la entrada relacionada y la entrada madre no sean la misma.
//También se podría comparar sus ID o sus URL (más eficiente), pero con el título se mantiene el "script" simple.
if (post.title != postTitle) {
if (postImage) { //Verifica si se desea mostrar una miniatura.
let imageUrl;
if (search) {
//Busca la primera imagen en la entrada relacionada o emplea la que se definió por defecto.
const parser = new DOMParser();
const postBody = parser.parseFromString(post.content, "text/html");
const firstImage = postBody.querySelector("img");
imageUrl = (firstImage && firstImage.src) || defaultImg;
} else {
imageUrl = post.images ? post.images[0].url : defaultImg;
}
//Elabora el HTML de la entrada relacionada iterada.
htmlList += `<a href="${post.url}" title="${post.title}" class="thumbnail${!imageUrl ? " no-image" : ""}">`;
if (imageUrl) {
htmlList += `<img src="${imageUrl}" alt="thumbnail"/>`;
}
htmlList += `<p>${post.title}</p></a>`;
} else {
htmlList += `<li><a href="${post.url}">${item.title}</a></li>`;
}
} else {
continue;
}
counter++;
}
if (htmlList) {
//Elabora el HTML final.
let htmlDiv = h4Text ? `<h4>${h4Text}</h4>` : "";
htmlDiv += postImage ? `<div class="content">${htmlList}</div>` : `<ul>${htmlList}</ul>`;
relatedPosts.insertAdjacentHTML("afterbegin", htmlDiv);
}
}
});
}
});
//]]>
</script>
Lo que está resaltado en amarillo, el ID del blog y la clave de la API, son los datos requeridos para que el script haga lo que se espera de él. Una vez hecho todo, hay que tener en cuenta los siguientes aspectos:
- Independientemente del número especificado en maxItems, el número de entradas mostradas puede ser inferior, ya que esto estará sujeto a los resultados devueltos por la API.
- En caso de relacionar las entradas por una etiqueta, se debe procurar que la etiqueta con mayor entradas esté de primero en los artículos del blog.
- En caso de realizar una búsqueda para relacionar las entradas, la pesquisa solo se basará en el contenido de ellas; sus etiquetas no están incluidas.
- Google aplica una cuota a las peticiones realizadas con su API, es decir, impone un límite a la cantidad de veces que se puede consultar con ella. Para más información, sigue este enlace.
Publicar un comentario
Sé respetuoso, no publiques información personal (p. ej., direcciones de correos electrónicos) y evita el contenido publicitario o que esté fuera del tema tratado. El comentario estará visible después de ser aprobado por el administrador.