search page #19 #20

Merged
fauno merged 50 commits from issue-19 into antifascista 2026-10-14 14:51:27 +00:00
33 changed files with 339 additions and 123 deletions
+2
View File
@@ -56,6 +56,8 @@ liquid:
- permalink
ignored_layouts:
- about
- menu_internal
- card_contact
images:
interlaced: true
activity_pub:
+1
View File
@@ -15,6 +15,7 @@
- "card"
- "title"
- "select"
- "responsive_img"
- "text"
- "checkbox"
- "radio"
+8 -1
View File
@@ -210,5 +210,12 @@ fields_recipe:
repository: "Repository"
versions: "Versions"
code: "Code"
sort:
last_modified_at/desc: "Newest first"
last_modified_at/asc: "Oldest first"
title/asc: "Alphabetical"
title/desc: "Reverse alphabetical"
score/asc: "Least revelant first"
score/desc: "Most relevant first"
previous: "Previous"
next: "Next"
next: "Next"
+1 -1
View File
@@ -36,7 +36,7 @@ image:
en: "Main image description"
help:
en: "Describe the image for accesibility purposes and for indexation in search engines"
category:
categories:
type: "new_predefined_array"
label:
en: "Category"
+13
View File
@@ -5,3 +5,16 @@ collections:
- "pages"
- "posts"
- "privacy_policies"
- "recipes"
- "documents"
rating:
- item: "★"
value: "1"
- item: "★★"
value: "2"
- item: "★★★"
value: "3"
- item: "★★★★"
value: "4"
- item: "★★★★★"
value: "5"
+2 -2
View File
@@ -4,7 +4,7 @@ description: "..."
image:
description: ''
path:
category: []
categories: []
topic:
- tech
status: pending
@@ -34,4 +34,4 @@ created_at: 2026-06-30 14:40:08.608656827 -06:00
<li>
<p style="text-align:start">-drddr</p>
</li>
</ul>
</ul>
+2 -2
View File
@@ -4,7 +4,7 @@ description: CoopCloud is a community of people with shared values and objective
image:
description: ''
path:
category: []
categories: []
topic:
- community
status: in_progress
@@ -33,4 +33,4 @@ created_at: 2026-06-30 14:29:08.943282163 -06:00
<li>
<p style="text-align:start">aoeu</p>
</li>
</ul>
</ul>
+15
View File
@@ -0,0 +1,15 @@
---
title: Search
placeholder: Search for...
searching: Searching...
empty: There are no results for these search terms.
locales: []
draft: false
order: 35
permalink: "/search/"
layout: search
uuid: fe2a3b3d-4a01-4ede-8589-080590145392
render_with_liquid: false
created_at: 2026-01-12 09:28:11.094422954 -06:00
last_modified_at: 2026-01-16 15:26:57.676094110 +00:00
---
+2 -2
View File
@@ -14,9 +14,9 @@ url:
<div class="row g-0">
<div class="col-md-4">
{%
render 'sutty/picture.html',
render 'responsive_img.html',
image:,
img_class: 'w-100 rounded-3'
class: 'w-100 rounded-3'
%}
</div>
<div class="col-md-8 ps-3">
+1
View File
@@ -10,6 +10,7 @@ btn_text:
default: 'join chats'
btn_link:
type: string
default: 'https://coopcloud.tech/'
---
<div class="border rounded border-primary p-3 d-flex flex-column">
<h2 class="text-primary fw-bold">{{ title }}</h2>
+6 -2
View File
@@ -5,13 +5,16 @@ class:
items:
type: 'array'
default: []
search:
type: 'hash'
default: {}
coopcloud_url:
type: 'string'
default: '?#'
color:
type: 'string'
default: 'primary'
uuid:
uuid:
type: 'string'
default: ''
---
@@ -42,7 +45,8 @@ uuid:
render 'menu.html',
items:,
id: 'menu',
coopcloud_url: '/#'
coopcloud_url: '/#',
search:
-%}
</div>
</div>
+8 -4
View File
@@ -9,15 +9,19 @@ star:
type: 'integer'
url:
type: 'string'
grid_breakpoints:
type: 'array'
default:
- 100
---
<div class="card mb-3">
<div class="d-flex align-items-center justify-content-center g-0">
<div class="d-flex">
{%
render 'sutty/picture.html',
render 'responsive_img.html',
image:,
img_class: 'w-100px w-md-150px border border-3 border-secondary rounded-3',
width: 100
class: 'w-100px w-md-150px border border-3 border-secondary rounded-3',
grid_breakpoints:
%}
</div>
<div class="d-flex w-100 ps-4">
@@ -58,4 +62,4 @@ url:
</div>
</div>
</div>
</div>
</div>
+6 -4
View File
@@ -13,9 +13,9 @@ coopcloud_url:
color:
type: 'string'
default: 'white'
title_search:
type: 'string'
default: ''
search:
type: 'hash'
default: {}
---
{%- block 'sutty/toggler/toggler_label.html', id: , class: 'py-3 ps-3' -%}
<div class="d-flex d-lg-none">
@@ -37,7 +37,9 @@ title_search:
</div>
</a>
<div>
{% render 'search.html', title: title_search, %}
{% unless search == empty %}
{% render 'search.html', **search %}
{% endunless %}
</div>
<ul class="px-0 py-5">
{% for item in items reversed %}
+4 -4
View File
@@ -18,16 +18,16 @@ tags:
<p class="fst-italic pt-3 m-0">{{ created_at | date: "%B %d, %Y" }}</p>
<small class="fst-italic pb-3">
Reading time: {{ words | divided_by: 180.0 | round | at_least: 1 }} minutes
</small>
</small>
<div class="d-flex align-items-center justify-content-start pb-2 w-100">
{% for tag in tags %}
<span class="card-link bg-body-color rounded-3 px-2 py-1 text-white me-2">
{{- tag -}}
</span>
{% endfor %}
{% endfor %}
</div>
{% render 'sutty/picture.html',
{% render 'responsive_img.html',
image:,
img_class: 'w-100 rounded-3'
class: 'w-100 rounded-3'
%}
</div>
+26
View File
@@ -0,0 +1,26 @@
---
image:
type: 'hash'
class:
type: 'string'
default: 'img-fluid'
grid_breakpoints:
type: 'array'
default:
- 576
- 768
- 992
- 1200
- 1400
---
<img
alt="{{ image.description | escape_once }}"
src="{{ image.path | thumbnail: grid_breakpoints.last | component_escape }}"
class="{{ class }}"
loading="lazy"
srcset="
{%- for grid_breakpoint in grid_breakpoints -%}
{{- image.path | thumbnail: grid_breakpoint | component_escape }} {{ grid_breakpoint }}w{{ forloop.last | value_unless: ',' -}}
{%- endfor -%}
"
>
+4 -2
View File
@@ -1,6 +1,8 @@
---
title:
type: 'string'
url:
type: 'string'
placeholder:
type: 'string'
default: ''
@@ -14,8 +16,8 @@ placeholder:
<div class="d-flex w-40px p-2 bg-light">
{% render 'svg/search.svg', class: 'purple w-35px' %}
</div>
<form class="w-100" action="/buscador/" method="get" data-pagefind-target="form">
<input type="search" class="form-control w-100 p-2" placeholder="{{ placeholder }}" name="q">
<form class="w-100" action="{{ url | component_escape }}" method="get" data-pagefind-target="form">
<input type="search" class="form-control w-100 p-2" placeholder="{{ placeholder | escape_once }}" name="q">
</form>
</div>
</div>
+43
View File
@@ -0,0 +1,43 @@
---
url:
type: 'string'
placeholder:
type: 'hash'
default:
es: 'Buscar'
en: 'Search'
pt_BR: 'Pesquisar'
categories:
type: 'array'
tags:
type: 'array'
sort:
type: 'array'
---
<form
class="w-100 mr-4"
action="{{ url | component_escape }}"
method="get"
data-pagefind-target="form"
data-action="pagefind#search:prevent"
>
<div class="row row-cols-1 row-cols-lg-5">
<div class="col mb-3">
{% render 'forms/select.html', id: 'categories', items: categories %}
</div>
<div class="col mb-3">
{% render 'forms/select.html', id: 'tags', items: tags %}
</div>
<div class="col mb-3">
<input type="search" class="form-control w-100" placeholder="{{ placeholder[locale] | escape_once }}" name="q">
</div>
<div class="col mb-3">
{% render 'forms/select.html', id: 'sort', items: sort %}
</div>
<div class="col mb-3">
<input type="submit" class="btn btn-primary" value="{{ placeholder[locale] | escape_once }}">
</div>
</form>
-34
View File
@@ -1,34 +0,0 @@
---
image:
type: 'hash'
class:
type: 'string'
default: ''
img_class:
type: 'string'
default: 'w-100'
width:
type: 'integer'
default: 560
pixel_densities:
type: 'array'
default:
- 1
- 1.5
- 2
---
<picture class="{{ class }}">
<source
srcset="
{%- for pixel_density in pixel_densities %}
{%- assign w = width | times: pixel_density -%}
{{- image.path | thumbnail: w | component_escape }} {{ pixel_density }}x,
{% endfor -%}
"
>
<img
class="{{ img_class }}"
src="{{ image.path | thumbnail: width | component_escape }}"
alt="{{ image.description | escape_once }}"
>
</picture>
+4 -1
View File
@@ -4,9 +4,12 @@ controller:
context_class:
type: "string"
default: "col"
target:
type: "string"
default: "_top"
---
<template data-{{ controller }}-target="placeholder">
<turbo-frame loading="lazy">
<turbo-frame loading="lazy" target="{{ target }}">
<div class="{{ context_class }}">
<span class="placeholder w-100" aria-hidden="true"></span>
</div>
+1 -1
View File
@@ -11,4 +11,4 @@ i18n:
text: 'Visita nuestra web',
btn_text: 'sutty',
btn_link: 'https://sutty.coop.ar'
%}
%}
-11
View File
@@ -1,11 +0,0 @@
---
data:
type: hash
locale:
type: string
i18n:
type: hash
---
{% assign image = i18n.lorem_ipsum.image %}
{% render 'sutty/picture.html', image: %}
+9
View File
@@ -0,0 +1,9 @@
---
data:
type: hash
locale:
type: string
i18n:
type: hash
---
{% render 'responsive_img.html', image: i18n.lorem_ipsum.image %}
+22
View File
@@ -57,7 +57,29 @@
{% if page.indexed -%}
<meta data-pagefind-meta="indexed[content]" content="{{ page.indexed.url | component_escape }}">
<meta data-pagefind-meta="uuid[content]" content="{{ page.indexed.uuid | escape_once }}">
{% if page.indexed_recipe %}
<meta data-pagefind-meta="indexed_recipe[content]" content="{{ page.indexed_recipe.url | component_escape }}">
<meta data-pagefind-filter="rating[content]" content="{{ page.metadata.features.status | default: '0' }}">
{% endif %}
<meta data-pagefind-filter="layout[content]" content="{{ page.layout | escape_once }}">
{% for category in page.categories %}
<meta data-pagefind-filter="categories[content]" content="{{ category | escape_once }}">
{% endfor %}
{% for tag in page.tags %}
<meta data-pagefind-filter="tags[content]" content="{{ tag | escape_once }}">
{% endfor %}
<meta
data-pagefind-sort="last_modified_at[content]"
content="{{ page.last_modified_at | default: page.created_at | default: page.date | default: site.time | date: "%F %T" }}"
>
<meta data-pagefind-sort="title[content]" content="{{ page.title | escape_once }}">
{% endif %}
{{ content }}
</main>
</body>
+11 -9
View File
@@ -2,27 +2,27 @@
layout: default
---
{% assign about = site.abouts | first %}
{% assign search = site.searches | first %}
<section class="row align-items-center justify-content-center w-100 mx-0 h-100">
<div class="col-12 col-md-6 bg-white bg-md-primary h-100vh text-white g-0">
<div class="d-flex flex-column justify-content-between h-100">
{% render 'bootstrap/navbar.html',
{%
render 'bootstrap/navbar.html',
class: 'container-lg',
items: site.menu_internals,
color: 'white',
uuid: uuid,
cache: false
uuid:,
search:
%}
<div class="d-block d-md-none">
{% render 'svg/cloud_srv.svg' %}
</div>
<div class="fw-bold fst-italic text-monospace px-4 fs-4">
{{ about.content | replace: '<a','<a class="text-white"' }}
<div class="fw-bold fst-italic text-monospace px-4 fs-4">
{{ about.content | replace: '<a', '<a class="text-white"' }}
</div>
{% render 'footer.html',
social_networks: about.social_networks
%}
{% render 'footer.html', social_networks: about.social_networks %}
</div>
</div>
<div class="col-12 col-md-6 d-flex flex-column justify-content-between m-0 h-100vh g-0">
@@ -30,7 +30,9 @@ layout: default
<div class="d-md-flex d-none w-75 m-auto">
{% render 'svg/cloud_srv.svg' %}
</div>
{% render 'search.html', title: site.i18n.search_title, placeholder: site.i18n.search_placeholder %}
{% unless search == empty %}
{% render 'search.html', **search %}
{% endunless %}
</div>
</div>
</section>
+2 -4
View File
@@ -1,9 +1,7 @@
---
---
<turbo-frame id="{{ page.uuid }}">
<turbo-frame id="{{ page.uuid }}" target="{{ page.target | default: "_top" }}">
<div class="col mb-3">
<a href="{{ page.page.url | component_escape }}">
{{- page.page.title -}}
</a>
{% render 'card_search.html', **page.page %}
</div>
</turbo-frame>
+10
View File
@@ -0,0 +1,10 @@
---
---
{% assign icon = page.page.metadata.icon | default: 'public/59c6b7ef75334ab1dd8d9a1c9bf/default.png' %}
{% assign image = 'path,description' | create_hash: icon, recipe.description %}
<turbo-frame id="{{ page.page.indexed.uuid }}" target="{{ page.target | default: "_top" }}">
<div class="col mb-3">
{% render 'card_recipe.html', title: page.page.title, description: page.page.description, image:, star: page.page.metadata.features.status, url: page.page.url %}
</div>
</turbo-frame>
+42 -4
View File
@@ -1,14 +1,42 @@
---
layout: "default"
---
<div class="container">
<div class="container" data-controller="pagefind" data-pagefind-indexed-value="indexed_recipe">
{% render 'sutty/title.html', title: page.title, class: 'pb-3', h: '1', description: page.description %}
{% if page.recipes == empty %}
{% if site.recipes == empty %}
<h1>Error: No recipes were processed</h1>
{% else %}
<div class="row row-cols-1 row-cols-md-2 g-0 w-100">
{% for recipe in page.recipes %}
<div class="mb-3">
<form
class="w-100 mr-4"
action="{{ page.url | component_escape }}"
method="get"
data-pagefind-target="form"
data-action="pagefind#search:prevent"
>
{% render 'forms/hidden.html', id: 'layout', value: 'recipe' %}
<div class="row row-cols-1 row-cols-lg-5">
<div class="col mb-3">
{% render 'forms/select.html', id: 'categories', items: site.data.pagefind.app_categories %}
</div>
<div class="col mb-3">
{% render 'forms/select.html', id: 'rating', items: site.data.pagefind.rating %}
</div>
<div class="col mb-3">
<input type="search" class="form-control w-100" placeholder="" name="q">
</div>
<div class="col mb-3">
{% render 'forms/select.html', id: 'sort', items: site.data.pagefind.sort %}
</div>
</div>
</form>
</div>
<div
class="row row-cols-1 row-cols-lg-2 g-0 w-100"
data-pagefind-target="container"
>
{% for recipe in site.recipes %}
{% assign icon = recipe.metadata.icon | default: 'public/59c6b7ef75334ab1dd8d9a1c9bf/default.png' %}
{% assign image = 'path,description' | create_hash: icon, recipe.description %}
<div class="col">
@@ -17,4 +45,14 @@ layout: "default"
{% endfor %}
</div>
{% endif %}
{% render 'sutty/placeholder.html', controller: 'pagefind' %}
<section data-pagefind-target="empty" class="mx-auto" hidden>
<h2>{{ page.empty }}</h2>
</section>
<section data-pagefind-target="searching" class="mx-auto" hidden>
<h2>{{ page.searching }}</h2>
</section>
</div>
+8 -3
View File
@@ -3,7 +3,12 @@ layout: default
---
<div class="container py-5 mb-100" data-controller="pagefind">
<div class="mb-3">
{% render 'search_box.html', url: page.url %}
{% render 'search_box_with_filters.html',
url: page.url,
categories: site.data.pagefind.categories,
tags: site.data.pagefind.tags,
sort: site.data.pagefind.sort
%}
</div>
<section
@@ -13,11 +18,11 @@ layout: default
{% render 'sutty/placeholder.html', controller: 'pagefind' %}
<section data-pagefind-target="empty" class="d-none mx-auto">
<section data-pagefind-target="empty" class="mx-auto" hidden>
<h2>{{ page.empty }}</h2>
</section>
<section data-pagefind-target="searching" class="mx-auto">
<section data-pagefind-target="searching" class="mx-auto" hidden>
<h2>{{ page.searching }}</h2>
</section>
</div>
+42 -17
View File
@@ -14,9 +14,9 @@ export default class extends Controller {
];
static values = {
q: String,
maxResults: {
type: Number,
default: 100,
indexed: {
type: String,
default: "indexed",
},
};
@@ -28,6 +28,7 @@ export default class extends Controller {
});
await window.pagefind.init();
this.filters = await window.pagefind.filters();
if (window.location.search) {
const searchParams = new URLSearchParams(window.location.search);
@@ -50,24 +51,48 @@ export default class extends Controller {
const formData = new FormData(form);
const q = formData.get("q") || null;
const filters = {};
const sort = {};
for (const key of formData.keys()) {
if (key === "q") continue;
if (key === "sort") {
formData.getAll(key).forEach((x) => {
const [sortBy, direction] = x.split("/");
sort[sortBy] = direction || "asc";
});
} else if (!this.filters[key]) {
console.error(
`Search form contains an element not configured as Pagefind filter. Add the corresponding data-pagefind-filter to _layouts/default.html`,
key,
);
} else {
const any = formData.getAll(key).filter((x) => x);
if (any.length !== 0) filters[key] = { any };
}
}
this.qValue = q;
this.formDisable(form, true);
this.searchingTargets.forEach((x) => x.classList.remove("d-none"));
const searchResults = (await window.pagefind.search(q)).results.slice(
0,
this.maxResultsValue,
);
this.emptyTargets.forEach((x) => (x.hidden = true));
this.searchingTargets.forEach((x) => (x.hidden = false));
this.containerTarget.innerHTML = "";
this.emptyTargets.forEach((x) => x.classList.add("d-none"));
this.searchingTargets.forEach((x) => x.classList.add("d-none"));
const searchResults = (await window.pagefind.search(q, { filters, sort }))
.results;
this.searchingTargets.forEach((x) => (x.hidden = true));
if (searchResults.length) {
this.renderResults(searchResults);
if (sort["score"] === "asc") {
this.renderResults(searchResults.reverse());
} else {
this.renderResults(searchResults);
}
} else {
this.emptyTargets.forEach((x) => x.classList.remove("d-none"));
this.emptyTargets.forEach((x) => (x.hidden = false));
}
this.formDisable(form, false);
@@ -91,8 +116,8 @@ export default class extends Controller {
}
renderResult(result) {
if (!result.meta.indexed) {
console.error("Result is missing indexed meta", result);
if (!result.meta[this.indexedValue]) {
console.error("Result is missing meta", this.indexedValue, result);
return;
}
@@ -100,7 +125,7 @@ export default class extends Controller {
this.placeholderTarget.content.firstElementChild.cloneNode(true);
placeholder.setAttribute("id", result.meta.uuid);
placeholder.setAttribute("src", result.meta.indexed);
placeholder.setAttribute("src", result.meta[this.indexedValue]);
this.containerTarget.appendChild(placeholder);
}
+3 -7
View File
@@ -1,19 +1,15 @@
# frozen_string_literal: true
# Renders images in content as picture
# Renders images in content as responsive images
Jekyll::Hooks.register :documents, :post_convert do |doc|
max_width = (doc.site.data.dig(*%(bootstrap variables container_max_widths xxl)) || '920px').to_i
template = Liquid::Template.parse("{% render 'sutty/picture.html', image:, class:, img_class:, width: %}")
template = Liquid::Template.parse("{% render 'responsive_img.html', image: %}")
doc.to_html.css('img').each do |img|
context = {
'image' => {
'path' => img['src'],
'description' => img['alt']
},
'class' => 'd-block w-100',
'img_class' => 'img-fluid',
'width' => max_width
}
}
img.replace(template.render(context))
+15
View File
@@ -2,6 +2,21 @@
require 'open3'
# This hook keeps the array of categories updated from the schema
# declaration.
Jekyll::Hooks.register :site, :post_read, priority: :low do |site|
site.data['pagefind']['categories'] = site.data['layouts']['document']['categories']['values'].map do |value, item|
{ value:, item: item[site.locale] }.transform_keys(&:to_s)
end
site.data['pagefind']['sort'] = site.data[site.locale]['sort'].map do |value, item|
{ item:, value: }.transform_keys(&:to_s)
end
site.data['pagefind']['tags'] = site.documents.map { _1['tags'] }.flatten.uniq.compact.sort
site.data['pagefind']['app_categories'] = site.collections['recipes'].docs.map { _1['categories'] }.flatten.uniq.compact.sort
end
Jekyll::Hooks.register :site, :post_write, priority: :low do |site|
cmd = ['_bin/pagefind', '--force-language', site.config['locale'], '--site', site.dest, '--root-selector', 'main']
Open3.popen2e(*cmd, unsetenv_others: true) do |_, o, t|
+4 -4
View File
@@ -171,7 +171,7 @@ module RenderWithSchema
@include_file ||= unquote(@file)
else
# Fix params retroactively
@include_file = Liquid::Template.parse("{{ #{@file} }}").render(context)
@include_file = Liquid::Template.parse("{{ #{@file} }}").render(context, strict_variables: strict_variables?)
@params[@include_file] = @params[@file].dup
end
@@ -194,8 +194,8 @@ module RenderWithSchema
def find_template_file!
@template_file = find_template_file @include_file
report_error("doesn't exist or is outside the include dirs", fatal: true) unless @template_file
report_error("doesn't exist!", fatal: true) unless File.file?(@template_file)
report_error("#{@include_file} doesn't exist or is outside the include dirs", fatal: true) unless @template_file
report_error("#{@include_file} doesn't exist!", fatal: true) unless File.file?(@template_file)
end
def find_liquid_file!
@@ -369,7 +369,7 @@ module RenderWithSchema
Jekyll.logger.error "#{self.class.name}:", error
exit if fatal || strict_variables?
exit 1 if fatal || strict_variables?
end
def quoted?(string)
+22 -4
View File
@@ -5,8 +5,16 @@ Jekyll::Router.draw do |router|
page.data['metadata'] = site.data.dig('recipes', recipe)
page.data['title'] = recipe
page.data['description'] = page['metadata']['description']
page.data['categories'] = [ page['metadata']['category'] ]
page.data['uuid'] = recipe
unless page['metadata']['category'].empty?
page.data['categories'] = [ page['metadata']['category'] ]
page.data['tags'] = [ page['metadata']['category'] ]
end
collection = site.collections['recipes'] ||= Jekyll::Collection.new(site, 'recipes')
collection.docs << page
site.pages.delete page
end
get '/recipes/' do |page|
@@ -14,9 +22,6 @@ Jekyll::Router.draw do |router|
page.data['description'] = "You can use these recipes (What's a recipe?)
with Co-op Cloud."
page.data['uuid'] = SecureRandom.uuid
page.data['recipes'] = site.pages.select do |p|
p['layout'] == 'recipe'
end
end
# Finds every Jekyll::Document whose collection marks it as indexable
@@ -33,4 +38,17 @@ with Co-op Cloud."
page.data['page'] = indexed_docs[indexed]
indexed_docs[indexed].data['indexed'] = page
end
indexed_recipes = site.collections['recipes'].docs.to_h do |doc|
[ doc['uuid'], doc ]
end
# Generates a new page representing every indexed recipe and
# establishes a bidirectional relationship.
get '/indexed_recipes/:indexed_recipe.html', indexed_recipe: indexed_recipes.keys do |page, indexed_recipe|
page.data['uuid'] = SecureRandom.uuid
page.data['page'] = indexed_recipes[indexed_recipe]
indexed_recipes[indexed_recipe].data['indexed_recipe'] = page
end
end