Formulaire de recherche
This commit is contained in:
496
assets/js/search-page.js
Normal file
496
assets/js/search-page.js
Normal file
@@ -0,0 +1,496 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Lit la configuration exposee par le template Hugo.
|
||||
* @param {HTMLElement} root Element principal de la page.
|
||||
* @returns {{endpoint: string, indexUid: string, apiKey: string, limit: number, queryParam: string}}
|
||||
*/
|
||||
function readSearchConfig(root) {
|
||||
const endpoint = root.dataset.searchEndpoint;
|
||||
const indexUid = root.dataset.searchIndex;
|
||||
const apiKey = root.dataset.searchApiKey;
|
||||
const limitValue = root.dataset.searchLimit;
|
||||
const queryParam = root.dataset.searchParam;
|
||||
|
||||
if (typeof endpoint !== "string" || endpoint.length === 0) {
|
||||
throw new Error("Missing search endpoint");
|
||||
}
|
||||
|
||||
if (typeof indexUid !== "string" || indexUid.length === 0) {
|
||||
throw new Error("Missing search index");
|
||||
}
|
||||
|
||||
if (typeof apiKey !== "string") {
|
||||
throw new Error("Invalid search api key");
|
||||
}
|
||||
|
||||
if (typeof limitValue !== "string" || limitValue.length === 0) {
|
||||
throw new Error("Missing search limit");
|
||||
}
|
||||
|
||||
if (typeof queryParam !== "string" || queryParam.length === 0) {
|
||||
throw new Error("Missing search query parameter");
|
||||
}
|
||||
|
||||
const limit = Number.parseInt(limitValue, 10);
|
||||
if (Number.isNaN(limit) || limit < 1) {
|
||||
throw new Error("Invalid search limit");
|
||||
}
|
||||
|
||||
return {
|
||||
endpoint,
|
||||
indexUid,
|
||||
apiKey,
|
||||
limit,
|
||||
queryParam,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit la requete de recherche depuis l'URL.
|
||||
* @param {string} queryParam Nom du parametre d'URL.
|
||||
* @returns {string}
|
||||
*/
|
||||
function readSearchQuery(queryParam) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const rawValue = params.get(queryParam);
|
||||
|
||||
if (rawValue === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return rawValue.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le payload envoye a Meilisearch.
|
||||
* @param {string} query Texte recherche.
|
||||
* @param {number} limit Nombre de resultats par lot.
|
||||
* @param {number} offset Position de depart.
|
||||
* @returns {{q: string, limit: number, offset: number, attributesToRetrieve: string[]}}
|
||||
*/
|
||||
function buildSearchPayload(query, limit, offset) {
|
||||
return {
|
||||
q: query,
|
||||
limit,
|
||||
offset,
|
||||
attributesToRetrieve: ["title", "path", "published_at"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une requete HTTP a Meilisearch pour un lot de resultats.
|
||||
* @param {{endpoint: string, indexUid: string, apiKey: string, limit: number}} config Configuration de recherche.
|
||||
* @param {string} query Texte recherche.
|
||||
* @param {number} offset Position de depart.
|
||||
* @returns {Promise<{ok: boolean, status: number, data: any}>}
|
||||
*/
|
||||
async function fetchSearchChunk(config, query, offset) {
|
||||
const endpoint = config.endpoint.replace(/\/+$/, "");
|
||||
const requestURL = endpoint + "/indexes/" + encodeURIComponent(config.indexUid) + "/search";
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (config.apiKey.length > 0) {
|
||||
headers.Authorization = "Bearer " + config.apiKey;
|
||||
}
|
||||
|
||||
const response = await fetch(requestURL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(buildSearchPayload(query, config.limit, offset)),
|
||||
});
|
||||
|
||||
if (response.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide et normalise le payload brut de Meilisearch.
|
||||
* @param {any} payload Donnees brutes.
|
||||
* @returns {{hits: any[], total: number}}
|
||||
*/
|
||||
function parseSearchPayload(payload) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
throw new Error("Invalid search payload");
|
||||
}
|
||||
|
||||
if (!Array.isArray(payload.hits)) {
|
||||
throw new Error("Invalid hits payload");
|
||||
}
|
||||
|
||||
const total = Number.parseInt(String(payload.estimatedTotalHits), 10);
|
||||
if (Number.isNaN(total)) {
|
||||
throw new Error("Invalid total hits value");
|
||||
}
|
||||
|
||||
return {
|
||||
hits: payload.hits,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tous les enfants d'un noeud.
|
||||
* @param {HTMLElement} node Noeud cible.
|
||||
*/
|
||||
function clearNode(node) {
|
||||
while (node.firstChild) {
|
||||
node.firstChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met a jour le message de statut.
|
||||
* @param {HTMLElement} statusNode Zone de statut.
|
||||
* @param {string} message Message a afficher.
|
||||
*/
|
||||
function updateStatus(statusNode, message) {
|
||||
statusNode.textContent = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche ou masque une section.
|
||||
* @param {HTMLElement} section Section cible.
|
||||
* @param {boolean} visible Etat de visibilite.
|
||||
*/
|
||||
function setSectionVisibility(section, visible) {
|
||||
if (visible) {
|
||||
section.removeAttribute("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
section.setAttribute("hidden", "hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un hit brut en resultat exploitable.
|
||||
* @param {any} hit Resultat brut.
|
||||
* @returns {{title: string, path: string, published_at: string} | null}
|
||||
*/
|
||||
function normalizeHit(hit) {
|
||||
if (typeof hit !== "object" || hit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof hit.path !== "string" || hit.path.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let title = hit.path.trim();
|
||||
if (typeof hit.title === "string" && hit.title.trim().length > 0) {
|
||||
title = hit.title.trim();
|
||||
}
|
||||
|
||||
let publishedAt = "";
|
||||
if (typeof hit.published_at === "string" && hit.published_at.trim().length > 0) {
|
||||
publishedAt = hit.published_at.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
path: hit.path.trim(),
|
||||
published_at: publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate la date en ignorant l'heure de publication.
|
||||
* @param {unknown} rawDate Date brute.
|
||||
* @returns {{datetime: string, display: string}}
|
||||
*/
|
||||
function buildDateInfo(rawDate) {
|
||||
if (typeof rawDate !== "string") {
|
||||
return {
|
||||
datetime: "",
|
||||
display: "Date inconnue",
|
||||
};
|
||||
}
|
||||
|
||||
const match = rawDate.trim().match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+\d{2}:\d{2}(?::\d{2})?)?$/);
|
||||
if (match === null) {
|
||||
return {
|
||||
datetime: "",
|
||||
display: "Date inconnue",
|
||||
};
|
||||
}
|
||||
|
||||
const year = match[1];
|
||||
const month = match[2];
|
||||
const day = match[3];
|
||||
|
||||
return {
|
||||
datetime: year + "-" + month + "-" + day,
|
||||
display: day + "/" + month + "/" + year,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait un libelle de section a partir du chemin Hugo.
|
||||
* @param {string} path Chemin relatif de l'article.
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractSectionLabel(path) {
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
if (segments.length === 0) {
|
||||
return "Section inconnue";
|
||||
}
|
||||
|
||||
const firstSegment = segments[0];
|
||||
if (firstSegment === "collections") {
|
||||
return "Collections";
|
||||
}
|
||||
|
||||
if (firstSegment === "critiques") {
|
||||
return "Critiques";
|
||||
}
|
||||
|
||||
if (firstSegment === "interets") {
|
||||
return "Intérêts";
|
||||
}
|
||||
|
||||
if (firstSegment === "stats") {
|
||||
return "Statistiques";
|
||||
}
|
||||
|
||||
if (firstSegment === "taxonomies") {
|
||||
return "Taxonomies";
|
||||
}
|
||||
|
||||
if (firstSegment === "contact") {
|
||||
return "Contact";
|
||||
}
|
||||
|
||||
if (firstSegment === "manifeste") {
|
||||
return "Manifeste";
|
||||
}
|
||||
|
||||
if (firstSegment === "liens-morts") {
|
||||
return "Liens morts";
|
||||
}
|
||||
|
||||
if (firstSegment === "revue-de-presse") {
|
||||
return "Revue de presse";
|
||||
}
|
||||
|
||||
return firstSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit un article de resultat simple.
|
||||
* @param {{title: string, path: string, published_at?: string}} hit Resultat.
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function createResultArticle(hit) {
|
||||
const article = document.createElement("article");
|
||||
|
||||
const title = document.createElement("h3");
|
||||
const link = document.createElement("a");
|
||||
link.href = hit.path;
|
||||
link.textContent = hit.title;
|
||||
title.append(link);
|
||||
article.append(title);
|
||||
|
||||
const meta = document.createElement("p");
|
||||
meta.className = "search-result-meta";
|
||||
|
||||
const dateInfo = buildDateInfo(hit.published_at);
|
||||
if (dateInfo.datetime.length > 0) {
|
||||
const time = document.createElement("time");
|
||||
time.dateTime = dateInfo.datetime;
|
||||
time.textContent = dateInfo.display;
|
||||
meta.append(time);
|
||||
} else {
|
||||
const dateText = document.createElement("span");
|
||||
dateText.textContent = dateInfo.display;
|
||||
meta.append(dateText);
|
||||
}
|
||||
|
||||
const separator = document.createElement("span");
|
||||
separator.textContent = "·";
|
||||
meta.append(separator);
|
||||
|
||||
const section = document.createElement("span");
|
||||
section.textContent = extractSectionLabel(hit.path);
|
||||
meta.append(section);
|
||||
|
||||
article.append(meta);
|
||||
|
||||
const path = document.createElement("p");
|
||||
path.className = "search-result-path";
|
||||
path.textContent = hit.path;
|
||||
article.append(path);
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rend la liste des resultats.
|
||||
* @param {HTMLOListElement} listNode Liste cible.
|
||||
* @param {Array<{title: string, path: string, published_at?: string}>} hits Resultats a afficher.
|
||||
*/
|
||||
function renderListing(listNode, hits) {
|
||||
clearNode(listNode);
|
||||
|
||||
for (const hit of hits) {
|
||||
const item = document.createElement("li");
|
||||
item.append(createResultArticle(hit));
|
||||
listNode.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupere tous les resultats disponibles en enchainant les pages.
|
||||
* @param {{endpoint: string, indexUid: string, apiKey: string, limit: number}} config Configuration de recherche.
|
||||
* @param {string} query Texte recherche.
|
||||
* @returns {Promise<{ok: boolean, status: number, data: {hits: Array<{title: string, path: string, published_at: string}>, estimatedTotalHits: number} | null}>}
|
||||
*/
|
||||
async function fetchAllSearchResults(config, query) {
|
||||
const allHits = [];
|
||||
const seenPaths = new Set();
|
||||
let expectedTotal = 0;
|
||||
let offset = 0;
|
||||
let lastStatus = 200;
|
||||
|
||||
while (true) {
|
||||
const chunkResponse = await fetchSearchChunk(config, query, offset);
|
||||
lastStatus = chunkResponse.status;
|
||||
|
||||
if (chunkResponse.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
status: chunkResponse.status,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseSearchPayload(chunkResponse.data);
|
||||
expectedTotal = parsed.total;
|
||||
|
||||
if (parsed.hits.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const rawHit of parsed.hits) {
|
||||
const normalized = normalizeHit(rawHit);
|
||||
if (normalized === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seenPaths.has(normalized.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenPaths.add(normalized.path);
|
||||
allHits.push(normalized);
|
||||
}
|
||||
|
||||
offset += parsed.hits.length;
|
||||
|
||||
if (offset >= expectedTotal) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (parsed.hits.length < config.limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: lastStatus,
|
||||
data: {
|
||||
hits: allHits,
|
||||
estimatedTotalHits: expectedTotal,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance la recherche au chargement de la page.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initSearchPage() {
|
||||
const root = document.querySelector("main.search-page");
|
||||
if (!(root instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = root.querySelector("[data-search-status]");
|
||||
const listingSection = root.querySelector("[data-search-listing]");
|
||||
const results = root.querySelector("[data-search-results]");
|
||||
|
||||
if (!(status instanceof HTMLElement)) {
|
||||
throw new Error("Missing search status");
|
||||
}
|
||||
|
||||
if (!(listingSection instanceof HTMLElement)) {
|
||||
throw new Error("Missing listing section");
|
||||
}
|
||||
|
||||
if (!(results instanceof HTMLOListElement)) {
|
||||
throw new Error("Missing results list");
|
||||
}
|
||||
|
||||
const config = readSearchConfig(root);
|
||||
const query = readSearchQuery(config.queryParam);
|
||||
|
||||
const headerInput = document.getElementById("header-search-input");
|
||||
if (headerInput instanceof HTMLInputElement) {
|
||||
headerInput.value = query;
|
||||
}
|
||||
|
||||
if (query.length === 0) {
|
||||
updateStatus(status, "Utilisez le champ de recherche dans l'en-tete.");
|
||||
setSectionVisibility(listingSection, false);
|
||||
clearNode(results);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(status, "Recherche en cours...");
|
||||
|
||||
const response = await fetchAllSearchResults(config, query);
|
||||
if (response.ok === false) {
|
||||
updateStatus(status, "Erreur Meilisearch (HTTP " + String(response.status) + ").");
|
||||
setSectionVisibility(listingSection, false);
|
||||
clearNode(results);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = parseSearchPayload(response.data);
|
||||
const totalHits = payload.total;
|
||||
const displayedHits = payload.hits.length;
|
||||
|
||||
if (totalHits === 0 || displayedHits === 0) {
|
||||
updateStatus(status, "Aucun resultat.");
|
||||
setSectionVisibility(listingSection, false);
|
||||
clearNode(results);
|
||||
return;
|
||||
}
|
||||
|
||||
renderListing(results, payload.hits);
|
||||
setSectionVisibility(listingSection, true);
|
||||
|
||||
if (totalHits > displayedHits) {
|
||||
const message = String(totalHits) + " resultats trouves. " + String(displayedHits) + " affiches. Affinez votre recherche.";
|
||||
updateStatus(status, message);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(status, String(totalHits) + " resultats.");
|
||||
}
|
||||
|
||||
void initSearchPage();
|
||||
@@ -3,3 +3,10 @@ favicon: /favicon.png
|
||||
# Et les placer dans /assets et non dans /static
|
||||
logo: logo-large.png
|
||||
description: "et ses opinions impopulaires"
|
||||
search:
|
||||
action: /recherche/
|
||||
param: q
|
||||
meilisearch:
|
||||
endpoint: /api/search
|
||||
indexUid: blog_posts
|
||||
hitsPerPage: 20
|
||||
|
||||
3
content/recherche/index.md
Normal file
3
content/recherche/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Recherche
|
||||
---
|
||||
44
layouts/recherche/single.html
Normal file
44
layouts/recherche/single.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{{ define "main" }}
|
||||
{{- $search := .Site.Params.search -}}
|
||||
{{- $meili := $search.meilisearch -}}
|
||||
<header class="article-header">
|
||||
{{ partialCached "header-brand.html" .Site .Site.Title (.Site.Params.logo | default "logo-large.png") }}
|
||||
<h1>{{ .Title }}</h1>
|
||||
</header>
|
||||
<main
|
||||
class="search-page"
|
||||
data-search-endpoint="{{ $meili.endpoint }}"
|
||||
data-search-index="{{ $meili.indexUid }}"
|
||||
data-search-api-key="{{ $meili.apiKey }}"
|
||||
data-search-limit="{{ $meili.hitsPerPage }}"
|
||||
data-search-param="{{ $search.param }}"
|
||||
>
|
||||
<section class="listing-search">
|
||||
<p class="search-page-status" data-search-status aria-live="polite"></p>
|
||||
</section>
|
||||
|
||||
<section class="listing-articles" data-search-listing hidden>
|
||||
<header>
|
||||
<h2>Résultats</h2>
|
||||
</header>
|
||||
<nav class="search-results" aria-label="Résultats de recherche">
|
||||
<ol data-search-results></ol>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<noscript>
|
||||
<section class="listing-articles">
|
||||
<p>Cette page nécessite JavaScript pour afficher les résultats.</p>
|
||||
</section>
|
||||
</noscript>
|
||||
</main>
|
||||
|
||||
{{- $searchJS := resources.Get "js/search-page.js" -}}
|
||||
{{- if eq hugo.Environment "development" -}}
|
||||
<script src="{{ $searchJS.RelPermalink }}" defer></script>
|
||||
{{- else -}}
|
||||
{{- with $searchJS | minify | fingerprint -}}
|
||||
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous" defer></script>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{ end }}
|
||||
@@ -45,6 +45,44 @@ body > header > section:first-of-type > a img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > input[type="search"] {
|
||||
width: min(22rem, 36vw);
|
||||
min-width: 12rem;
|
||||
min-height: 2.35rem;
|
||||
padding: 0.45rem 0.65rem;
|
||||
border: 1px solid var(--color-border-strong);
|
||||
background-color: color-mix(in srgb, var(--color-background-alt) 88%, #000000 12%);
|
||||
color: var(--color-heading);
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > input[type="search"]:focus-visible {
|
||||
border-color: var(--color-accent-1);
|
||||
outline: 2px solid color-mix(in srgb, var(--color-accent-1) 78%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > button.ui-button {
|
||||
width: 2.35rem;
|
||||
min-height: 2.35rem;
|
||||
min-width: 2.35rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > button.ui-button > svg {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
body > header > section.site-stats {
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@import "article-header.css";
|
||||
@import "content.css";
|
||||
@import "list.css";
|
||||
@import "search.css";
|
||||
@import "home.css";
|
||||
@import "footer.css";
|
||||
@import "responsive.css";
|
||||
|
||||
@@ -136,6 +136,17 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search {
|
||||
flex: 1 1 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > input[type="search"] {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
main nav.articles-list > ol {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -356,6 +367,28 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search {
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > input[type="search"] {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.4rem 0.55rem;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > button.ui-button {
|
||||
width: 2.2rem;
|
||||
min-height: 2.2rem;
|
||||
min-width: 2.2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body > header > section:first-of-type > form.site-search > button.ui-button > svg {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
body > header:has(> h1) > h1 {
|
||||
font-size: clamp(1.6rem, 8.2vw, 2.25rem);
|
||||
}
|
||||
|
||||
72
themes/2026/assets/css/search.css
Normal file
72
themes/2026/assets/css/search.css
Normal file
@@ -0,0 +1,72 @@
|
||||
body > main.search-page {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-search {
|
||||
border-top: var(--border-width-regular) solid var(--color-accent-1);
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
body > main.search-page .search-page-status {
|
||||
margin-top: 0;
|
||||
min-height: 1.4rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results > ol {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results > ol > li {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: var(--space-3);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results > ol > li:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article {
|
||||
margin-top: 0;
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article h3 {
|
||||
font-size: clamp(1.14rem, 1.9vw, 1.55rem);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article h3 > a {
|
||||
color: var(--color-heading);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article h3 > a:is(:hover, :focus-visible) {
|
||||
color: var(--color-link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article p.search-result-meta {
|
||||
margin-top: var(--space-1);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
font-size: 0.86rem;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
body > main.search-page > section.listing-articles > nav.search-results article p.search-result-path {
|
||||
margin-top: var(--space-1);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<section>
|
||||
{{ partialCached "site-title.html" (dict "Site" .) .Title (.Params.logo | default "logo-large.png") }}
|
||||
{{ partialCached "site-title.html" (dict "Site" .) .Title (.Params.logo | default "logo-large.png") .Params.search.action .Params.search.param }}
|
||||
</section>
|
||||
|
||||
@@ -11,3 +11,11 @@
|
||||
{{- end -}}
|
||||
<strong>{{ $site.Title }}</strong>
|
||||
</a>
|
||||
<form class="site-search" role="search" method="get" action="{{ $site.Params.search.action | relURL }}" aria-label="Recherche">
|
||||
<input id="header-search-input" type="search" name="{{ $site.Params.search.param }}" required aria-label="Recherche">
|
||||
<button type="submit" class="ui-button" aria-label="Lancer la recherche" title="Lancer la recherche">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M11 4a7 7 0 1 0 4.9 12l4.5 4.5 1.4-1.4-4.5-4.5A7 7 0 0 0 11 4Zm0 2a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user