feat: re-designed search UI and new FAQ and Q&A blocks

- Add search-hero block with ==highlight== syntax and gradient mesh backgrounds
- Add knowledge-categories block with dynamic Hugo taxonomy counts
- Add trending-questions block with content-driven trending system
- Add FAQ/questions layouts with Alpine.js accordions and search integration
- Redesign search modal using Pagefind headless API to create custom UI design
- Add gradient mesh background system (orbs, waves, dots, grid) to framework
- Add i18n support for new blocks
- Fix video backgrounds regression in parse_block_v3.html
- Add QAPage/FAQPage structured data for SEO optimization
This commit is contained in:
George Cushen committed 2025-10-23 21:07:30 +01:00
1 parent 3eef2cba11
commit 692dc8c62e
40 files changed
+2856 -122

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: false
# Summary for SEO
summary: ""
# Mark this as an FAQ page to enable FAQPage structured data
faq_page: true
# Categories and tags
categories: []
tags: []
# Pagefind search metadata (automatically indexed)
# type: faq (auto-added by layout)
# category: first category from categories array above
# Option 1: Define FAQs inline
faqs:
- question: "What is your question here?"
answer: "Your answer here. Can use **Markdown** formatting."
- question: "Another question?"
answer: "Another answer with more details."
# Add more Q&As as needed
# Option 2: Alternatively, create child pages under this FAQ section
# and they will automatically be included
# SEO settings
seo:
title: ""
description: ""
# Show breadcrumb navigation
show_breadcrumb: true
---
Add an introductory text here that will appear before the FAQ accordion.
This is a great place to provide context about the FAQ section.
@@ -0,0 +1,62 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: false
# The question (can also use title)
question: "{{ replace .Name "-" " " | title }}"
# Short answer (optional - can also use content below)
answer: ""
# Summary for SEO and previews
summary: ""
# Difficulty level (optional)
difficulty: "" # e.g., "Beginner", "Intermediate", "Advanced"
# Categories and tags for organization
categories: []
tags: []
# Pagefind search metadata (automatically indexed)
# type: questions (auto-added by layout)
# category: first category from categories array above
# difficulty: value from difficulty field below
# Vote counts (optional - for display and structured data)
upvote_count: 0
downvote_count: 0
# Additional/suggested answers (optional)
# suggested_answer:
# - text: "Alternative answer text here..."
# author: "Author Name"
# date: 2024-01-15
# upvote_count: 5
# Related questions (optional - can also be auto-generated)
# related:
# - link-to-related-question
# SEO settings
seo:
title: ""
description: ""
# Show breadcrumb navigation
show_breadcrumb: true
---
Write your detailed answer here using Markdown.
You can include:
- Code blocks
- Images
- Lists
- Links
- And more!
## Additional Details
Add sections as needed to provide comprehensive information.
@@ -3,7 +3,8 @@
/* This ensures Tailwind generates these classes even if not found in templates */
/* Inline safelist for classes used in dynamic content and YAML configuration */
@source inline("<div class='block hidden dark:block dark:hidden'></div><div class='task-list'></div><div class='pl-4 pl-8 pl-12'></div><div class='min-h-screen'></div><div class='md:grid-cols-1 md:grid-cols-2 md:grid-cols-3 md:grid-cols-4 md:grid-cols-5 md:grid-cols-6'></div><div class='bg-white bg-gray-50 bg-gray-100 bg-gray-200 bg-gray-300 bg-gray-400 bg-gray-500 bg-gray-600 bg-gray-700 bg-gray-800 bg-gray-900'></div><div class='bg-neutral-50 bg-neutral-100 bg-neutral-200 bg-neutral-300 bg-neutral-400 bg-neutral-500 bg-neutral-600 bg-neutral-700 bg-neutral-800 bg-neutral-900'></div><div class='dark:bg-neutral-700 dark:bg-neutral-800 dark:bg-neutral-900'></div><div class='bg-primary-50 bg-primary-100 bg-primary-200 bg-primary-300 bg-primary-400 bg-primary-500 bg-primary-600 bg-primary-700 bg-primary-800 bg-primary-900'></div><div class='bg-secondary-50 bg-secondary-100 bg-secondary-200 bg-secondary-300 bg-secondary-400 bg-secondary-500 bg-secondary-600 bg-secondary-700 bg-secondary-800 bg-secondary-900'></div><div class='text-primary-50 text-primary-100 text-primary-200 text-primary-300 text-primary-400 text-primary-500 text-primary-600 text-primary-700 text-primary-800 text-primary-900'></div><div class='text-secondary-50 text-secondary-100 text-secondary-200 text-secondary-300 text-secondary-400 text-secondary-500 text-secondary-600 text-secondary-700 text-secondary-800 text-secondary-900'></div><div class='text-neutral-50 text-neutral-100 text-neutral-200 text-neutral-300 text-neutral-400 text-neutral-500 text-neutral-600 text-neutral-700 text-neutral-800 text-neutral-900'></div><div class='text-white text-black dark:text-white'></div><div class='dark:text-neutral-300 dark:text-neutral-400'></div><div class='border-primary-50 border-primary-100 border-primary-200 border-primary-300 border-primary-400 border-primary-500 border-primary-600 border-primary-700 border-primary-800 border-primary-900'></div><div class='border-secondary-50 border-secondary-100 border-secondary-200 border-secondary-300 border-secondary-400 border-secondary-500 border-secondary-600 border-secondary-700 border-secondary-800 border-secondary-900'></div><div class='border-neutral-50 border-neutral-100 border-neutral-200 border-neutral-300 border-neutral-400 border-neutral-500 border-neutral-600 border-neutral-700 border-neutral-800 border-neutral-900'></div><div class='bg-gradient-to-t bg-gradient-to-tr bg-gradient-to-r bg-gradient-to-br bg-gradient-to-b bg-gradient-to-bl bg-gradient-to-l bg-gradient-to-tl'></div><div class='from-primary-50 from-primary-100 from-primary-200 from-primary-300 from-primary-400 from-primary-500 from-primary-600 from-primary-700 from-primary-800 from-primary-900'></div><div class='from-secondary-50 from-secondary-100 from-secondary-200 from-secondary-300 from-secondary-400 from-secondary-500 from-secondary-600 from-secondary-700 from-secondary-800 from-secondary-900'></div><div class='to-primary-50 to-primary-100 to-primary-200 to-primary-300 to-primary-400 to-primary-500 to-primary-600 to-primary-700 to-primary-800 to-primary-900'></div><div class='to-secondary-50 to-secondary-100 to-secondary-200 to-secondary-300 to-secondary-400 to-secondary-500 to-secondary-600 to-secondary-700 to-secondary-800 to-secondary-900'></div><div class='hover:from-primary-500 hover:from-secondary-500 hover:to-primary-500 hover:to-secondary-500'></div><div class='hover:bg-primary-50 hover:bg-primary-100 hover:bg-primary-200 hover:bg-primary-300 hover:bg-primary-400 hover:bg-primary-500 hover:bg-primary-600 hover:bg-primary-700 hover:bg-primary-800 hover:bg-primary-900'></div><div class='hover:bg-neutral-50 hover:bg-neutral-100 hover:bg-neutral-200 hover:bg-neutral-300 hover:bg-neutral-400 hover:bg-neutral-500 hover:bg-neutral-600 hover:bg-neutral-700 hover:bg-neutral-800 hover:bg-neutral-900'></div><div class='hover:text-primary hover:text-primary-50 hover:text-primary-100 hover:text-primary-200 hover:text-primary-300 hover:text-primary-400 hover:text-primary-500 hover:text-primary-600 hover:text-primary-700 hover:text-primary-800 hover:text-primary-900'></div><div class='hover:text-neutral-50 hover:text-neutral-100 hover:text-neutral-200 hover:text-neutral-300 hover:text-neutral-400 hover:text-neutral-500 hover:text-neutral-600 hover:text-neutral-700 hover:text-neutral-800 hover:text-neutral-900'></div><div class='dark:hover:bg-primary-50 dark:hover:bg-primary-100 dark:hover:bg-primary-200 dark:hover:bg-primary-300 dark:hover:bg-primary-400 dark:hover:bg-primary-500 dark:hover:bg-primary-600 dark:hover:bg-primary-700 dark:hover:bg-primary-800 dark:hover:bg-primary-900'></div><div class='dark:hover:text-neutral-50 dark:hover:text-neutral-100 dark:hover:text-neutral-200 dark:hover:text-neutral-300 dark:hover:text-neutral-400 dark:hover:text-neutral-500 dark:hover:text-neutral-600 dark:hover:text-neutral-700 dark:hover:text-neutral-800 dark:hover:text-neutral-900'></div><div class='bg-indigo-50 bg-indigo-100 bg-indigo-200 bg-Line truncated
/* Includes search modal Quick Actions dynamic color classes */
@source inline("<div class='block hidden dark:block dark:hidden'></div><div class='task-list'></div><div class='pl-4 pl-8 pl-12'></div><div class='min-h-screen'></div><div class='border-l-2 border-primary-600 bg-primary-50 dark:bg-primary-900/20'></div><div class='overflow-hidden animate-pulse animate-float animate-rotate-slow'></div><div class='blur-2xl blur-3xl -z-10'></div><div class='top-0 bottom-0 left-0 right-0 left-1/4 right-1/4 left-1/2 right-1/2 left-1/3 right-1/3'></div><div class='w-64 w-72 w-80 w-96 h-64 h-72 h-80 h-96 h-48 h-56 rounded-full'></div><div class='skew-y-2 -skew-y-2 skew-y-3 -skew-y-3'></div><div class='md:grid-cols-1 md:grid-cols-2 md:grid-cols-3 md:grid-cols-4 md:grid-cols-5 md:grid-cols-6'></div><div class='bg-white bg-gray-50 bg-gray-100 bg-gray-200 bg-gray-300 bg-gray-400 bg-gray-500 bg-gray-600 bg-gray-700 bg-gray-800 bg-gray-900'></div><div class='bg-neutral-50 bg-neutral-100 bg-neutral-200 bg-neutral-300 bg-neutral-400 bg-neutral-500 bg-neutral-600 bg-neutral-700 bg-neutral-800 bg-neutral-900'></div><div class='dark:bg-neutral-700 dark:bg-neutral-800 dark:bg-neutral-900'></div><div class='bg-primary-50 bg-primary-100 bg-primary-200 bg-primary-300 bg-primary-400 bg-primary-500 bg-primary-600 bg-primary-700 bg-primary-800 bg-primary-900'></div><div class='bg-primary-500/10 bg-primary-500/20 bg-primary-500/30 bg-primary-500/40 bg-primary-600/10 bg-primary-600/20 bg-primary-600/30 bg-primary-600/40'></div><div class='via-transparent from-primary-500/20 to-primary-600/20'></div><div class='bg-secondary-50 bg-secondary-100 bg-secondary-200 bg-secondary-300 bg-secondary-400 bg-secondary-500 bg-secondary-600 bg-secondary-700 bg-secondary-800 bg-secondary-900'></div><div class='text-primary-50 text-primary-100 text-primary-200 text-primary-300 text-primary-400 text-primary-500 text-primary-600 text-primary-700 text-primary-800 text-primary-900'></div><div class='text-secondary-50 text-secondary-100 text-secondary-200 text-secondary-300 text-secondary-400 text-secondary-500 text-secondary-600 text-secondary-700 text-secondary-800 text-secondary-900'></div><div class='text-neutral-50 text-neutral-100 text-neutral-200 text-neutral-300 text-neutral-400 text-neutral-500 text-neutral-600 text-neutral-700 text-neutral-800 text-neutral-900'></div><div class='text-white text-black dark:text-white'></div><div class='dark:text-neutral-300 dark:text-neutral-400'></div><div class='border-primary-50 border-primary-100 border-primary-200 border-primary-300 border-primary-400 border-primary-500 border-primary-600 border-primary-700 border-primary-800 border-primary-900'></div><div class='border-secondary-50 border-secondary-100 border-secondary-200 border-secondary-300 border-secondary-400 border-secondary-500 border-secondary-600 border-secondary-700 border-secondary-800 border-secondary-900'></div><div class='border-neutral-50 border-neutral-100 border-neutral-200 border-neutral-300 border-neutral-400 border-neutral-500 border-neutral-600 border-neutral-700 border-neutral-800 border-neutral-900'></div><div class='bg-gradient-to-t bg-gradient-to-tr bg-gradient-to-r bg-gradient-to-br bg-gradient-to-b bg-gradient-to-bl bg-gradient-to-l bg-gradient-to-tl'></div><div class='from-primary-50 from-primary-100 from-primary-200 from-primary-300 from-primary-400 from-primary-500 from-primary-600 from-primary-700 from-primary-800 from-primary-900'></div><div class='from-secondary-50 from-secondary-100 from-secondary-200 from-secondary-300 from-secondary-400 from-secondary-500 from-secondary-600 from-secondary-700 from-secondary-800 from-secondary-900'></div><div class='to-primary-50 to-primary-100 to-primary-200 to-primary-300 to-primary-400 to-primary-500 to-primary-600 to-primary-700 to-primary-800 to-primary-900'></div><div class='to-secondary-50 to-secondary-100 to-secondary-200 to-secondary-300 to-secondary-400 to-secondary-500 to-secondary-600 to-secondary-700 to-secondary-800 to-secondary-900'></div><div class='hover:from-primary-500 hover:from-secondary-500 hover:to-primary-500 hover:to-secondary-500'></div><div class='hover:bg-primary-50 hover:bg-primary-100 hover:bg-primary-200 hover:bg-primary-300 hover:bg-primary-400 hover:bg-primary-500 hover:bg-primary-600 hover:bg-primary-700 hover:bg-primary-800 hover:bg-primary-900'></div><div class='hover:bg-neutral-50 hover:bg-neutral-100 hover:bg-neutral-200 hover:bg-neutral-300 hover:bg-neutral-400 hover:bg-neutral-500 hover:bg-neutral-600 hover:bg-neutral-700 hover:bg-neutral-800 hover:bg-neutral-900'></div><div class='hover:text-primary hover:text-primary-50 hover:text-primary-100 hover:text-primary-200 hover:text-primary-300 hover:text-primary-400 hover:text-primary-500 hover:text-primary-600 hover:text-primary-700 hover:text-primary-800 hover:text-primary-900'></div><div class='hover:text-neutral-50 hover:text-neutral-100 hover:text-neutral-200 hover:text-neutral-300 hover:text-neutral-400 hover:text-neutral-500 hover:text-neutral-600 hover:text-neutLine truncated
/* Force generation of all CSS custom properties for dynamic usage in YAML front matter */
/* Hidden utility class that forces all color variables to be generated */
@@ -11,6 +11,39 @@
}
}
/* Gradient Mesh Animations */
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-20px) translateX(10px);
}
50% {
transform: translateY(-10px) translateX(-10px);
}
75% {
transform: translateY(-30px) translateX(5px);
}
}
@keyframes rotate-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-float {
animation: float 8s ease-in-out infinite;
}
.animate-rotate-slow {
animation: rotate-slow 20s linear infinite;
}
/* Custom utilities and components */
@utility task-list {
list-style: none;
@@ -0,0 +1,88 @@
/* Hugo Blox Search - Custom Pagefind Implementation */
/* Animations */
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Search result highlights */
.search-result mark {
background-color: rgb(254 240 138);
color: rgb(17 24 39);
font-weight: 600;
padding: 0.05rem 0.2rem;
border-radius: 0.125rem;
}
.dark .search-result mark {
background-color: rgb(133 77 14);
color: rgb(254 240 138);
}
/* Result hover state */
.search-result:hover mark {
background-color: rgb(253 224 71);
}
.dark .search-result:hover mark {
background-color: rgb(161 98 7);
}
/* Smooth transitions */
.search-modal-enter {
animation: slideInUp 0.2s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(1rem) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Loading skeleton */
.search-skeleton {
background: linear-gradient(
90deg,
rgb(229 231 235) 25%,
rgb(243 244 246) 50%,
rgb(229 231 235) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.dark .search-skeleton {
background: linear-gradient(
90deg,
rgb(55 65 81) 25%,
rgb(75 85 99) 50%,
rgb(55 65 81) 75%
);
background-size: 200% 100%;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Keyboard navigation highlight - handled by Alpine :class binding */
/* Smooth transition for selection state */
.search-result {
transition: all 0.15s ease;
}
/* Additional hover effects for keyboard-selected items */
.search-result:focus-visible {
outline: 2px solid rgb(59 130 246);
outline-offset: -2px;
}
@@ -35,5 +35,8 @@
@import "views/all.css";
@import "chroma.css";
/* Hugo Blox Search */
@import "hb-search.css";
/* Safelist for dynamic classes */
@import "config/safelist.css";
+30 -67
View File
@@ -1,69 +1,32 @@
// Hugo Blox Search functionality
// Initialize search wrapper element globally
let searchWrapper = null;
// Hugo Blox Search - Custom Pagefind API Implementation
// Search is handled by Alpine.js component in search-modal.html
window.addEventListener("DOMContentLoaded", () => {
const configScript = document.getElementById("search-config");
if (!configScript) {
// No search on this page.
return;
}
const searchConfig = JSON.parse(configScript.textContent);
if (!searchConfig || typeof searchConfig.baseUrl === "undefined") {
console.error("Pagefind search config is missing or invalid.", searchConfig);
return;
}
try {
new PagefindUI({
element: "#search",
showSubResults: true,
baseUrl: searchConfig.baseUrl,
bundlePath: searchConfig.baseUrl + "pagefind/",
// Additional keyboard shortcuts and utilities
document.addEventListener('DOMContentLoaded', () => {
console.log('✓ Hugo Blox Search initialized (Pagefind Headless API)');
// Handle search toggle buttons
const searchButtons = document.querySelectorAll('[data-search-toggle]');
searchButtons.forEach(button => {
button.addEventListener('click', (e) => {
const prefilledQuery = button.dataset.searchQuery;
if (window.Alpine && Alpine.store('search')) {
Alpine.store('search').open = true;
// Pre-fill query if specified
if (prefilledQuery) {
setTimeout(() => {
const searchInput = document.querySelector('[x-ref="searchInput"]');
if (searchInput) {
searchInput.value = prefilledQuery;
searchInput.dispatchEvent(new Event('input'));
}
}, 100);
}
}
});
} catch (e) {
console.error("Failed to initialize PagefindUI:", e);
return;
}
// Get search wrapper element
searchWrapper = document.getElementById("search-wrapper");
// Add click handlers to triggers
let triggers = document.querySelectorAll("[data-search-toggle]");
triggers.forEach((trigger) => trigger.addEventListener("click", handleSearchToggle));
});
function handleSearchToggle() {
if (!searchWrapper) return;
const isHidden = searchWrapper.classList.contains("hidden");
searchWrapper.classList.toggle("hidden");
document.body.style.overflow = isHidden ? "hidden" : "";
const searchInput = searchWrapper.querySelector("input");
if (searchInput) {
searchInput.value = "";
searchInput.focus();
}
if (!searchWrapper.classList.contains("hidden")) {
let clearTrigger = document.querySelector(".pagefind-ui__search-clear");
if (clearTrigger && !clearTrigger.hasAttribute("listenerOnClick")) {
clearTrigger.setAttribute("listenerOnClick", "true");
clearTrigger.addEventListener("click", () => {
searchInput.focus();
});
}
}
}
// Add keyboard shortcut to close search modal with Escape key
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && searchWrapper && !searchWrapper.classList.contains("hidden")) {
searchWrapper.classList.add("hidden");
}
});
});
// Note: Keyboard navigation (arrow keys, Enter) is handled by Alpine.js in search-modal.html
});
+58
View File
@@ -0,0 +1,58 @@
# FAQ Block
Display frequently asked questions in an interactive accordion format with automatic FAQPage structured data for SEO.
## Features
- ✅ Interactive accordion with Alpine.js
- ✅ FAQPage Schema.org structured data for Google rich results
- ✅ Responsive design with Tailwind CSS
- ✅ Dark mode support
- ✅ Markdown support in answers
## Usage
Add to your page's front matter:
```yaml
sections:
- block: faq
content:
title: Frequently Asked Questions
subtitle: Find answers to common questions
text: Can't find what you're looking for? [Contact us](/contact)
items:
- question: What is Hugo Blox Builder?
answer: Hugo Blox Builder is a no-code website builder framework powered by Hugo and Tailwind CSS.
- question: Is it free to use?
answer: Yes! Hugo Blox Builder is open source and free to use under the MIT license.
- question: How do I get started?
answer: |
Getting started is easy:
1. Choose a template
2. Click "Use Template"
3. Customize your content
4. Deploy to GitHub Pages or Netlify
button:
text: View All FAQs
url: /faq/
icon: arrow-right
design:
spacing:
padding: ["6rem", "0", "6rem", "0"]
```
## Structured Data
This block automatically generates FAQPage structured data for better SEO and eligibility for Google's FAQ rich results.
## Customization
- **Accordion behavior**: One item open at a time (default)
- **Colors**: Automatically adapts to your theme
- **Spacing**: Customizable via `design.spacing`
- **Background**: Customizable via `design.background.color`
+97
View File
@@ -0,0 +1,97 @@
{{- $wcPage := .wcPage -}}
{{- $wcBlock := .wcBlock -}}
{{- $wcIdentifier := .wcIdentifier -}}
{{/* Enable Alpine.js for interactive accordion */}}
{{ $wcPage.Page.Store.Set "has_alpine" true }}
{{- $st := $wcBlock.design.spacing | default dict -}}
{{- $pad := $st.padding | default (slice "20px" "0" "20px" "0") -}}
{{- $pad_sm := slice "3rem" "0" -}}
{{- $mar := $st.margin | default (slice "0" "0" "0" "0") -}}
{{- $bg := $wcBlock.design.background | default dict -}}
{{- $bg_color_val := $bg.color | default "" -}}
<section id="{{ $wcIdentifier }}" class="hb-section {{ $bg_color_val }}" style="padding: {{ delimit $pad " " }}; padding-top: clamp({{ index $pad_sm 0 }}, {{ index $pad 0 }}, {{ index $pad 0 }}); padding-bottom: clamp({{ index $pad_sm 1 }}, {{ index $pad 2 }}, {{ index $pad 2 }}); margin: {{ delimit $mar " " }};">
<div class="mx-auto max-w-screen-xl px-6 lg:px-8">
{{- with $wcBlock.content.title -}}
<div class="mb-8 text-center">
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
{{ . | markdownify | emojify }}
</h2>
{{- with $wcBlock.content.subtitle -}}
<p class="mt-4 text-lg text-gray-600 dark:text-gray-300">
{{ . | markdownify | emojify }}
</p>
{{- end -}}
</div>
{{- end -}}
{{- with $wcBlock.content.text -}}
<div class="prose prose-slate dark:prose-invert max-w-3xl mx-auto mb-8">
{{ . | markdownify | emojify }}
</div>
{{- end -}}
{{- $items := $wcBlock.content.items | default slice -}}
{{- if $items -}}
<div class="max-w-3xl mx-auto" x-data="{ openItem: null }">
<div class="space-y-4">
{{- range $index, $item := $items -}}
<div class="faq-item border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm">
<button
type="button"
class="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="openItem = openItem === {{ $index }} ? null : {{ $index }}"
:aria-expanded="openItem === {{ $index }}"
>
<span class="font-semibold text-gray-900 dark:text-gray-100 pr-8">
{{ $item.question | markdownify | emojify }}
</span>
<svg
class="w-5 h-5 text-gray-500 transition-transform flex-shrink-0"
:class="{ 'rotate-180': openItem === {{ $index }} }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div
x-show="openItem === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
>
<div class="prose prose-slate dark:prose-invert max-w-none">
{{ $item.answer | markdownify | emojify }}
</div>
</div>
</div>
{{- end -}}
</div>
</div>
{{- end -}}
{{- with $wcBlock.content.button -}}
<div class="mt-10 text-center">
<a href="{{ .url }}" class="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-primary-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600">
{{ .text }}
{{- with .icon -}}
<span class="ml-2">{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"inline-block h-5 w-5\"") }}</span>
{{- end -}}
</a>
</div>
{{- end -}}
</div>
</section>
@@ -0,0 +1,9 @@
{
"name": "faq",
"version": "1",
"title": "FAQ",
"description": "Display frequently asked questions in an accordion format with SEO-optimized structured data",
"categories": ["content", "interactive"],
"preview": "preview.svg"
}
@@ -0,0 +1,32 @@
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f8fafc;stop-opacity:1" />
<stop offset="100%" style="stop-color:#e2e8f0;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="400" height="300" fill="url(#bg)"/>
<!-- Title -->
<text x="200" y="40" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="#1e293b" text-anchor="middle">FAQ</text>
<!-- FAQ Item 1 -->
<rect x="50" y="70" width="300" height="50" rx="8" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text x="70" y="95" font-family="Arial, sans-serif" font-size="14" font-weight="600" fill="#334155">What is this feature?</text>
<path d="M320 93 l5 5 l5 -5" stroke="#64748b" stroke-width="2" fill="none"/>
<!-- FAQ Item 2 -->
<rect x="50" y="130" width="300" height="50" rx="8" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text x="70" y="155" font-family="Arial, sans-serif" font-size="14" font-weight="600" fill="#334155">How does it work?</text>
<path d="M320 153 l5 5 l5 -5" stroke="#64748b" stroke-width="2" fill="none"/>
<!-- FAQ Item 3 Expanded -->
<rect x="50" y="190" width="300" height="80" rx="8" fill="#ffffff" stroke="#3b82f6" stroke-width="2"/>
<text x="70" y="215" font-family="Arial, sans-serif" font-size="14" font-weight="600" fill="#334155">Where can I learn more?</text>
<path d="M320 213 l5 -5 l5 5" stroke="#3b82f6" stroke-width="2" fill="none"/>
<text x="70" y="240" font-family="Arial, sans-serif" font-size="12" fill="#64748b">Check our documentation</text>
<text x="70" y="255" font-family="Arial, sans-serif" font-size="12" fill="#64748b">for detailed guides...</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,95 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "FAQ Block",
"description": "Display frequently asked questions in an accordion format with FAQPage structured data",
"properties": {
"content": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Block title"
},
"subtitle": {
"type": "string",
"description": "Block subtitle"
},
"text": {
"type": "string",
"description": "Introductory text (Markdown supported)"
},
"items": {
"type": "array",
"description": "FAQ items",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question"
},
"answer": {
"type": "string",
"description": "The answer (Markdown supported)"
}
},
"required": ["question", "answer"]
}
},
"button": {
"type": "object",
"description": "Call-to-action button",
"properties": {
"text": {
"type": "string",
"description": "Button text"
},
"url": {
"type": "string",
"description": "Button URL"
},
"icon": {
"type": "string",
"description": "Button icon name"
}
}
}
}
},
"design": {
"type": "object",
"properties": {
"spacing": {
"type": "object",
"properties": {
"padding": {
"type": "array",
"items": {
"type": "string"
},
"description": "Padding [top, right, bottom, left]"
},
"margin": {
"type": "array",
"items": {
"type": "string"
},
"description": "Margin [top, right, bottom, left]"
}
}
},
"background": {
"type": "object",
"properties": {
"color": {
"type": "string",
"description": "Background color CSS class"
}
}
}
}
}
}
}
@@ -0,0 +1,97 @@
{{- $wcPage := .wcPage -}}
{{- $wcBlock := .wcBlock -}}
{{- $wcIdentifier := .wcIdentifier -}}
{{- $st := $wcBlock.design.spacing | default dict -}}
{{- $pad := $st.padding | default (slice "4rem" "0" "4rem" "0") -}}
<section id="{{ $wcIdentifier }}" class="relative" style="padding: {{ delimit $pad " " }};">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
{{/* Section header */}}
{{ if or $wcBlock.content.title $wcBlock.content.subtitle }}
<div class="mx-auto max-w-2xl text-center mb-12">
{{ with $wcBlock.content.title }}
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
{{ . | markdownify }}
</h2>
{{ end }}
{{ with $wcBlock.content.subtitle }}
<p class="mt-4 text-lg text-gray-600 dark:text-gray-300">
{{ . | markdownify }}
</p>
{{ end }}
</div>
{{ end }}
{{/* Categories grid - with dynamic counts from Hugo taxonomy */}}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ range $wcBlock.content.categories }}
{{ $category_name := .category | default .title }}
{{/* Get actual count from Hugo taxonomy - proper Hugo syntax */}}
{{ $category_url := "" }}
{{ $actual_count := 0 }}
{{ with site.Taxonomies.categories.Get $category_name }}
{{ $actual_count = .Count }}
{{ $category_url = .Page.RelPermalink }}
{{ end }}
{{/* Only show category if it has content OR if count is explicitly set */}}
{{ if or (gt $actual_count 0) (.count) }}
<a href="{{ $category_url }}" class="group relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-sm ring-1 ring-gray-900/5 transition-all duration-300 hover:shadow-xl hover:ring-primary-500/50 hover:-translate-y-1">
{{/* Icon */}}
{{ with .icon }}
<div class="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl {{ .bg_color | default "bg-primary-100 dark:bg-primary-900/50" }} {{ .text_color | default "text-primary-600 dark:text-primary-400" }} group-hover:scale-110 transition-transform">
{{ partial "functions/get_icon" (dict "name" .name "attributes" "class=\"h-6 w-6\"") }}
</div>
{{ end }}
{{/* Title */}}
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{{ .title }}
</h3>
{{/* Description */}}
{{ with .description }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{{ . }}
</p>
{{ end }}
{{/* Stats - use actual count from taxonomy, fallback to manual count */}}
<div class="flex items-center justify-between text-sm">
{{ $display_count := default .count $actual_count }}
{{ if $display_count }}
<span class="text-gray-500 dark:text-gray-400">
{{ $display_count }} {{ cond (eq $display_count 1) (T "article" | default "article") (T "articles" | default "articles") }}
</span>
{{ end }}
<span class="text-primary-600 dark:text-primary-400 group-hover:translate-x-1 transition-transform">
{{ T "browse" | default "Browse" }} →
</span>
</div>
{{/* Hover gradient effect */}}
<div class="absolute inset-0 -z-10 bg-gradient-to-br from-primary-500/0 via-primary-500/0 to-primary-500/0 opacity-0 transition-opacity duration-300 group-hover:opacity-10"></div>
</a>
{{ end }}
{{ end }}
</div>
{{/* View all link */}}
{{ with $wcBlock.content.view_all }}
<div class="mt-10 text-center">
<a href="{{ .link }}" class="inline-flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
{{ .text }}
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
{{ end }}
</div>
</section>
@@ -0,0 +1,8 @@
{
"name": "knowledge-categories",
"version": "1",
"title": "Knowledge Categories",
"description": "Grid of knowledge base category cards",
"categories": ["content", "navigation"],
"preview": "preview.svg"
}
@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Knowledge Categories Block",
"description": "Grid of knowledge base category cards",
"properties": {
"content": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Category name (e.g., 'Getting Started', 'ChatGPT') - will be auto-slugified for URL"
},
"title": {
"type": "string",
"description": "Display title (optional - defaults to category name)"
},
"description": {
"type": "string"
},
"icon": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"bg_color": {
"type": "string"
},
"text_color": {
"type": "string"
}
}
},
"count": {
"type": "number"
}
}
}
},
"view_all": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"link": {
"type": "string"
}
}
}
}
},
"design": {
"type": "object",
"properties": {
"spacing": {
"type": "object"
}
}
}
}
}
@@ -0,0 +1,265 @@
# Search Hero Block
A modern hero section with prominent search functionality, perfect for knowledge bases and documentation sites.
## Features
- 🔍 **Integrated Search** - Opens Hugo Blox search modal
- 💎 **Gradient Text Highlighting** - Eye-catching emphasized text
- 🎨 **Animated Backgrounds** - Gradient mesh with pulsing orbs
- 📊 **Stats Display** - Showcase key metrics
- 🏷️ **Badge Support** - With optional pulsing indicator
- 🔥 **Popular Searches** - Quick suggestion pills
- 📱 **Fully Responsive** - Beautiful on all devices
## Basic Usage
```yaml
sections:
- block: search-hero
content:
title: "Your Knowledge Hub"
subtitle: "Find answers to all your questions"
```
## Title Highlighting
Use `==text==` syntax to apply gradient highlighting to any words or phrases:
### Single Highlight
```yaml
title: "Master ==AI Tools=="
```
**Result**: "Master" in regular text, "AI Tools" with gradient
### Multiple Highlights
```yaml
title: "Master ==AI Tools==. ==Boost Your Productivity.=="
```
**Result**: Both "AI Tools" and "Boost Your Productivity" highlighted
### Mid-Sentence Highlighting
```yaml
title: "Build ==amazing== websites with ==Hugo Blox=="
```
**Result**: "amazing" and "Hugo Blox" highlighted within the sentence
### i18n-Friendly
```yaml
# en.yaml
- id: hero_title
translation: "Master ==AI Tools==. ==Boost Productivity.=="
# es.yaml
- id: hero_title
translation: "Domina ==Herramientas IA==. ==Aumenta Productividad.=="
```
Each language can highlight naturally!
## Complete Example
```yaml
sections:
- block: search-hero
content:
badge:
text: "500+ AI answers added this week"
show_pulse: true # Shows pulsing dot indicator
title: "Master ==AI Tools==. ==Boost Your Productivity.=="
subtitle: "Get instant answers to your AI questions from our comprehensive knowledge base."
search_placeholder: "Ask anything about AI tools, prompts, or workflows..."
suggestions:
- "ChatGPT prompts"
- "Midjourney tips"
- "AI automation"
- "Claude vs GPT-4"
stats:
- value: "2,500+"
label: "Expert Answers"
- value: "50K+"
label: "Monthly Users"
- value: "4.9/5"
label: "User Rating"
design:
background:
gradient_mesh:
enable: true
style: "orbs" # orbs, waves, dots, grid
animation: "pulse" # pulse, float, rotate, none
intensity: "subtle" # subtle, medium, bold
colors:
- "primary-500/20"
- "primary-600/20"
spacing:
padding: ["8rem", "0", "6rem", "0"]
```
## Properties
### Content
#### `badge`
- **Type**: Object
- **Optional**
- **Properties**:
- `text` (string): Badge text
- `show_pulse` (boolean): Show pulsing dot indicator
#### `title`
- **Type**: String
- **Required**
- **Description**: Main heading. Use `==text==` for gradient highlighting
- **Example**: `"Build ==Amazing== Websites"`
#### `subtitle`
- **Type**: String
- **Optional**
- **Description**: Supporting text below title
#### `search_placeholder`
- **Type**: String
- **Optional**
- **Default**: "Search for answers..."
- **Description**: Placeholder text in search box
#### `suggestions`
- **Type**: Array of strings
- **Optional**
- **Description**: Quick search suggestion pills
- **Example**: `["Topic 1", "Topic 2"]`
#### `stats`
- **Type**: Array of objects
- **Optional**
- **Description**: Metrics displayed below search
- **Properties**:
- `value` (string): Metric value (e.g., "2,500+")
- `label` (string): Metric description
### Design
See [Gradient Mesh Guide](../../../../GRADIENT_MESH_GUIDE.md) for complete background options.
## Styling Tips
### Gradient Colors
The `==text==` highlighting uses:
```css
background: linear-gradient(to-right, primary-600, primary-500);
background-clip: text;
```
This automatically adapts to your site's primary color theme.
### Stat Layout
Stats automatically:
- Display in 3-column grid
- Center-aligned
- Responsive on mobile
### Search Integration
The search button automatically:
- Opens Hugo Blox search modal
- Works with Cmd+K shortcut
- Supports pre-filled queries from suggestions
## Examples
### Knowledge Base (Current)
```yaml
title: "Master ==AI Tools==. ==Boost Your Productivity.=="
```
### Documentation Site
```yaml
title: "==Comprehensive== Documentation for ==Developers=="
```
### Product Landing
```yaml
title: "Build ==Faster==. Ship ==Better==. Scale ==Effortlessly.=="
```
### Academic/Research
```yaml
title: "Advancing ==Science== Through ==Collaboration=="
```
## Best Practices
### 1. Highlight Key Value Props
✅ **Good**: Highlight unique value or action words
```yaml
title: "Learn ==Faster== with AI"
```
❌ **Bad**: Highlight common words
```yaml
title: "Learn Faster ==with== AI"
```
### 2. Don't Over-Highlight
✅ **Good**: 1-3 highlighted phrases max
```yaml
title: "Master ==AI Tools==. Boost ==Productivity==."
```
❌ **Bad**: Everything highlighted
```yaml
title: "==Master== ==AI== ==Tools==. ==Boost== ==Your== ==Productivity==."
```
### 3. Keep Highlights Meaningful
Highlighted text should be:
- **Scannable** - Key phrases users notice first
- **Actionable** - Words that convey benefit
- **Brief** - 1-4 words per highlight
## Advanced Examples
### Corporate Landing
```yaml
title: "Transform ==Your Business== with AI"
```
### Educational Platform
```yaml
title: "Learn ==Smarter==, Not ==Harder=="
```
## Technical Notes
- **Regex Pattern**: `==([^=]+)==` matches text between double equals
- **Processing**: Applied before markdownify, so Markdown works inside highlights
- **Output**: `<span class="bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent">text</span>`
- **Performance**: No runtime cost, processed at build time
---
**Part of Hugo Blox Builder** - https://hugoblox.com
@@ -0,0 +1,98 @@
{{- $wcPage := .wcPage -}}
{{- $wcBlock := .wcBlock -}}
{{- $wcIdentifier := .wcIdentifier -}}
{{/* Enable Alpine.js for interactivity */}}
{{ $wcPage.Page.Store.Set "has_alpine" true }}
{{/* Background is now handled by parse_block_v3.html gradient_mesh system */}}
{{/* Content only - wrapper and styling applied by framework */}}
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-3xl text-center">
{{/* Badge */}}
{{ with $wcBlock.content.badge }}
<div class="mb-8 inline-flex items-center gap-2 rounded-full bg-primary-100 dark:bg-primary-900/30 px-4 py-2 text-sm font-medium text-primary-800 dark:text-primary-200">
{{ with .show_pulse }}
<span class="flex h-2 w-2 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-primary-500"></span>
</span>
{{ end }}
{{ .text | markdownify }}
</div>
{{ end }}
{{/* Title - supports ==highlight== syntax for gradient text */}}
{{ $title := $wcBlock.content.title }}
{{/* Replace ==text== with gradient-highlighted spans - supports multiple highlights */}}
{{ $title = replaceRE "==([^=]+)==" "<span class=\"bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent\">$1</span>" $title }}
<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl mb-6">
{{ $title | markdownify | emojify | safeHTML }}
</h1>
{{/* Subtitle */}}
{{ with $wcBlock.content.subtitle }}
<p class="text-lg leading-8 text-gray-600 dark:text-gray-300 mb-12">
{{ . | markdownify | emojify }}
</p>
{{ end }}
{{/* Search Box - Integrates with Pagefind */}}
<div class="relative" x-data="{ searchFocus: false }">
<button
data-search-toggle
type="button"
class="w-full group cursor-pointer"
aria-label="Open search">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-xl ring-1 ring-gray-900/5 transition-all duration-300 hover:ring-2 hover:ring-primary-500 hover:scale-105"
:class="{ 'ring-2 ring-primary-500 scale-105': searchFocus }">
<div class="w-full px-6 py-5 pl-14 pr-32 text-left">
<span class="text-gray-400 dark:text-gray-500 text-lg">{{ $wcBlock.content.search_placeholder | default "Search for answers..." }}</span>
</div>
<div class="absolute inset-y-0 left-0 flex items-center pl-5 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<div class="absolute inset-y-2 right-2 flex items-center px-4 py-2 bg-primary-600 group-hover:bg-primary-700 text-white rounded-xl font-medium transition-colors pointer-events-none">
Search
<svg class="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</div>
</div>
</button>
{{/* Quick search suggestions - Now trigger Pagefind with pre-filled query */}}
{{ with $wcBlock.content.suggestions }}
<div class="mt-4 flex flex-wrap items-center justify-center gap-2 text-sm">
<span class="text-gray-500 dark:text-gray-400">Popular:</span>
{{ range . }}
<button
data-search-toggle
data-search-query="{{ . }}"
type="button"
class="px-3 py-1 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors cursor-pointer">
{{ . }}
</button>
{{ end }}
</div>
{{ end }}
</div>
{{/* Stats */}}
{{ if $wcBlock.content.stats }}
<div class="mt-16 grid grid-cols-3 gap-8 text-center">
{{ range $wcBlock.content.stats }}
<div>
<div class="text-3xl font-bold text-gray-900 dark:text-white">{{ .value }}</div>
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ .label }}</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
@@ -0,0 +1,8 @@
{
"name": "search-hero",
"version": "1",
"title": "Search Hero",
"description": "Hero section with prominent search for knowledge bases",
"categories": ["hero", "search"],
"preview": "preview.svg"
}
@@ -0,0 +1,114 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Search Hero Block",
"description": "A hero section with prominent search functionality for knowledge bases",
"properties": {
"content": {
"type": "object",
"properties": {
"badge": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Badge text"
},
"show_pulse": {
"type": "boolean",
"description": "Show pulsing dot indicator"
}
}
},
"title": {
"type": "string",
"description": "Main title. Use ==text== syntax to apply gradient highlighting to any words or phrases. Example: 'Master ==AI Tools==. ==Boost Your Productivity.=='"
},
"subtitle": {
"type": "string",
"description": "Subtitle text"
},
"search_placeholder": {
"type": "string",
"description": "Search input placeholder"
},
"suggestions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Quick search suggestions"
},
"stats": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "Stat value"
},
"label": {
"type": "string",
"description": "Stat label"
}
}
}
}
}
},
"design": {
"type": "object",
"properties": {
"spacing": {
"type": "object"
},
"background": {
"type": "object",
"properties": {
"color": {
"type": "string",
"description": "Background color (Tailwind class)"
},
"gradient_mesh": {
"type": "object",
"description": "Animated gradient mesh background",
"properties": {
"enable": {
"type": "boolean",
"description": "Enable animated gradient mesh"
},
"style": {
"type": "string",
"enum": ["orbs", "waves", "dots", "grid"],
"description": "Visual style of gradient mesh",
"default": "orbs"
},
"animation": {
"type": "string",
"enum": ["pulse", "float", "rotate", "none"],
"description": "Animation type",
"default": "pulse"
},
"intensity": {
"type": "string",
"enum": ["subtle", "medium", "bold"],
"description": "Visual intensity",
"default": "subtle"
},
"colors": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tailwind colors with opacity (e.g., 'primary-500/20')",
"default": ["primary-500/20", "primary-600/20"]
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,165 @@
{{- $wcPage := .wcPage -}}
{{- $wcBlock := .wcBlock -}}
{{- $wcIdentifier := .wcIdentifier -}}
{{- $st := $wcBlock.design.spacing | default dict -}}
{{- $pad := $st.padding | default (slice "4rem" "0" "4rem" "0") -}}
<section id="{{ $wcIdentifier }}" class="{{ $wcBlock.design.background.color }}" style="padding: {{ delimit $pad " " }};">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="flex items-center justify-between mb-8">
<div>
{{ with $wcBlock.content.title }}
<h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
{{ . | markdownify }}
</h2>
{{ end }}
{{ with $wcBlock.content.subtitle }}
<p class="mt-2 text-gray-600 dark:text-gray-300">
{{ . | markdownify }}
</p>
{{ end }}
</div>
{{ with $wcBlock.content.view_all }}
<a href="{{ .link }}" class="hidden sm:flex items-center gap-2 text-primary-600 dark:text-primary-400 font-medium hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
{{ .text }}
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
{{ end }}
</div>
{{/* Get trending questions dynamically or from static config */}}
{{ $questions := slice }}
{{/* Method 1: Dynamic - Pull from pages with trending: true */}}
{{ $count := $wcBlock.content.count | default 4 }}
{{ $trending_pages := where site.RegularPages "Params.trending" true }}
{{ $trending_pages = $trending_pages.ByParam "trending_weight" | default ($trending_pages.ByDate.Reverse) }}
{{ $trending_pages = first $count $trending_pages }}
{{ if $trending_pages }}
{{ range $trending_pages }}
{{ $question_item := dict }}
{{ $question_item = merge $question_item (dict "question" (.Params.question | default .Title)) }}
{{ $question_item = merge $question_item (dict "link" .RelPermalink) }}
{{ $question_item = merge $question_item (dict "answer_preview" (.Params.answer_preview | default .Params.answer | default .Summary | plainify | truncate 150)) }}
{{/* Optional metadata */}}
{{ with .Params.author }}
{{ $question_item = merge $question_item (dict "author" .) }}
{{ end }}
{{ with .Params.views }}
{{ $question_item = merge $question_item (dict "views" .) }}
{{ end }}
{{ with .Params.upvote_count }}
{{ $question_item = merge $question_item (dict "upvotes" .) }}
{{ end }}
{{ with .Params.badge }}
{{ $question_item = merge $question_item (dict "badge" .) }}
{{ else }}
{{/* Default badges based on metadata */}}
{{ if gt (.Params.upvote_count | default 0) 50 }}
{{ $question_item = merge $question_item (dict "badge" (dict "text" "🔥 Popular" "color" "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200")) }}
{{ else if .Params.featured }}
{{ $question_item = merge $question_item (dict "badge" (dict "text" "⭐ Featured" "color" "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200")) }}
{{ else if (time .Date).After (now.AddDate 0 0 -7) }}
{{ $question_item = merge $question_item (dict "badge" (dict "text" "✨ New" "color" "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200")) }}
{{ else }}
{{ $question_item = merge $question_item (dict "badge" (dict "text" "🔥 Trending" "color" "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200")) }}
{{ end }}
{{ end }}
{{ $questions = $questions | append $question_item }}
{{ end }}
{{ end }}
{{/* Method 2: Static fallback - Use config if no trending pages found */}}
{{ if not $questions }}
{{ $questions = $wcBlock.content.questions }}
{{ end }}
{{/* Questions list or carousel */}}
<div class="grid gap-4 lg:grid-cols-2">
{{ range $index, $question := $questions }}
<div class="group relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 transition-all duration-300 hover:border-primary-500 hover:shadow-lg">
{{/* Badge */}}
{{ with .badge }}
<div class="mb-3 inline-flex items-center rounded-full px-3 py-1 text-xs font-medium {{ .color | default "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200" }}">
{{ .text }}
</div>
{{ end }}
{{/* Question */}}
<h3 class="mb-3 text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
<a href="{{ .link }}" class="after:absolute after:inset-0">
{{ .question }}
</a>
</h3>
{{/* Answer preview */}}
{{ with .answer_preview }}
<p class="text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{{ . }}
</p>
{{ end }}
{{/* Meta info */}}
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-4 text-gray-500 dark:text-gray-400">
{{ with .author }}
<span class="flex items-center gap-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
{{ . }}
</span>
{{ end }}
{{ with .views }}
<span class="flex items-center gap-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
{{ . }}
</span>
{{ end }}
{{ with .upvotes }}
<span class="flex items-center gap-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ . }} helpful
</span>
{{ end }}
</div>
<span class="text-primary-600 dark:text-primary-400 opacity-0 group-hover:opacity-100 transition-opacity">
Read →
</span>
</div>
</div>
{{ end }}
</div>
{{/* Mobile view all link */}}
{{ with $wcBlock.content.view_all }}
<div class="mt-6 sm:hidden">
<a href="{{ .link }}" class="flex items-center justify-center gap-2 rounded-lg bg-primary-600 px-4 py-2 text-white font-medium hover:bg-primary-700 transition-colors">
{{ .text }}
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
{{ end }}
</div>
</section>
@@ -0,0 +1,8 @@
{
"name": "trending-questions",
"version": "1",
"title": "Trending Questions",
"description": "Display trending or featured questions",
"categories": ["content"],
"preview": "preview.svg"
}
@@ -0,0 +1,83 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Trending Questions Block",
"description": "Display trending or featured questions",
"properties": {
"content": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"questions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string"
},
"answer_preview": {
"type": "string"
},
"link": {
"type": "string"
},
"badge": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
"author": {
"type": "string"
},
"views": {
"type": "string"
},
"upvotes": {
"type": "number"
}
}
}
},
"view_all": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"link": {
"type": "string"
}
}
}
}
},
"design": {
"type": "object",
"properties": {
"spacing": {
"type": "object"
},
"background": {
"type": "object",
"properties": {
"color": {
"type": "string"
}
}
}
}
}
}
}
+57
View File
@@ -335,6 +335,48 @@
- id: authors
translation: Authors
- id: qa
translation: Q&A
- id: questions
translation: Questions
- id: faq
translation: FAQ
- id: faqs
translation: FAQs
- id: docs
translation: Documentation
- id: question
translation: Question
- id: answer
translation: Answer
- id: accepted_answer
translation: Accepted Answer
- id: other_answers
translation: Other Answers
- id: related_questions
translation: Related Questions
- id: browse_by_category
translation: Browse by Category
- id: search_questions
translation: Search questions...
- id: no_questions_yet
translation: No questions yet. Check back soon!
- id: questions_count
translation: questions
# Search
- id: search
@@ -409,3 +451,18 @@
# Card metadata
- id: trending
translation: Trending
- id: article
translation: article
- id: articles
translation: articles
- id: browse
translation: Browse
- id: all
translation: All
- id: helpful
translation: helpful
+2 -1
View File
@@ -1,5 +1,6 @@
{{- define "main" -}}
<div class="flex flex-col justify-center">
{{/* Exclude 404 page from search indexing */}}
<div data-pagefind-ignore class="flex flex-col justify-center">
<article class="container mx-auto prose prose-slate lg:prose-xl dark:prose-invert">
<h1 class="lg:text-6xl">{{ i18n "page_not_found" }}</h1>
@@ -142,7 +142,7 @@
{{ with $show_search }}
<button
aria-label="toggle search"
class="text-black hover:text-primary inline-block px-3 text-xl dark:text-white"
class="text-black hover:text-primary inline-block px-3 text-xl dark:text-white cursor-pointer"
data-search-toggle>
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512" fill="currentColor"><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
</button>
@@ -1,20 +1,523 @@
{{/* Modern Search Modal - Custom Pagefind API Implementation */}}
<div
id="search-wrapper"
class="hidden fixed inset-0 z-50 bg-white dark:bg-gray-900 flex flex-col overflow-hidden"
x-data="searchModal()"
x-show="$store.search.open"
@keydown.escape.window="$store.search.open = false"
@keydown.cmd.k.window.prevent="$store.search.open = !$store.search.open"
@keydown.ctrl.k.window.prevent="$store.search.open = !$store.search.open"
x-cloak
class="fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm"
style="display: none;"
>
<div class="flex justify-end p-3">
{{/* Backdrop */}}
<div class="absolute inset-0" @click="$store.search.open = false"></div>
{{/* Modal Container */}}
<div
class="relative mx-auto mt-[10vh] max-w-3xl"
x-show="$store.search.open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-4 scale-95"
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
x-transition:leave-end="opacity-0 translate-y-4 scale-95"
>
<div class="mx-4 overflow-hidden rounded-2xl bg-white dark:bg-gray-900 shadow-2xl ring-1 ring-gray-900/10 dark:ring-white/10">
{{/* Search Header */}}
<div class="border-b border-gray-200 dark:border-gray-800">
<div class="flex items-center gap-3 px-4 py-3">
{{/* Search Icon */}}
<svg class="h-5 w-5 flex-shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
{{/* Search Input */}}
<input
x-ref="searchInput"
x-model="query"
@input="loading = query.trim() !== ''"
@input.debounce.300ms="search()"
type="text"
placeholder="Search for answers..."
class="flex-1 bg-transparent text-lg outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400"
autocomplete="off"
autofocus
/>
{{/* Loading Indicator */}}
<div x-show="loading" class="flex-shrink-0">
<svg class="animate-spin h-5 w-5 text-primary-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
{{/* ESC hint */}}
<kbd class="hidden sm:block flex-shrink-0 rounded bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-400">ESC</kbd>
{{/* Close Button */}}
<button
aria-label="search"
class="search-close text-black hover:text-primary dark:text-white"
data-search-toggle
@click="$store.search.open = false"
class="flex-shrink-0 rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300 cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="search" class="flex-1 overflow-y-auto p-3">
<!-- Search content displayed here -->
{{/* Filters - Auto-show when search has results */}}
<div x-show="query && availableFilters.length > 0" x-transition class="border-t border-gray-100 dark:border-gray-800 px-4 py-2">
<div class="flex flex-wrap gap-2">
<button
@click="activeFilter = null; search()"
:class="activeFilter === null ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400'"
class="rounded-full px-3 py-1 text-xs font-medium transition-colors cursor-pointer"
>
All Results
</button>
<template x-for="filter in availableFilters" :key="filter.filterKey">
<button
@click="activeFilter = filter.filterKey; search()"
:class="activeFilter === filter.filterKey ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400'"
class="rounded-full px-3 py-1 text-xs font-medium transition-colors cursor-pointer"
x-text="filter.label + ' (' + filter.count + ')'"
></button>
</template>
</div>
</div>
</div>
{{/* Results Container */}}
<div class="max-h-[60vh] overflow-y-auto">
{{/* Empty State - Quick Actions */}}
<div x-show="!query && results.length === 0" class="p-6">
{{/* Quick Actions - Configurable via site params */}}
{{ $quick_actions := site.Params.features.search.quick_actions }}
{{ if $quick_actions }}
<div class="mb-8">
<h3 class="mb-4 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Quick Actions
</h3>
<div class="grid gap-3 sm:grid-cols-2">
{{ range $quick_actions }}
<a href="{{ .url }}" {{ if .external }}target="_blank" rel="noopener"{{ end }} class="group flex items-center gap-3 rounded-xl border border-gray-200 dark:border-gray-700 p-4 hover:border-{{ .color | default "primary" }}-500 hover:bg-{{ .color | default "primary" }}-50 dark:hover:bg-{{ .color | default "primary" }}-900/10 transition-all">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-{{ .color | default "primary" }}-100 text-{{ .color | default "primary" }}-600 dark:bg-{{ .color | default "primary" }}-900 dark:text-{{ .color | default "primary" }}-400">
{{ partial "functions/get_icon" (dict "name" (.icon | default "question-mark-circle") "attributes" "class=\"h-5 w-5\"") }}
</div>
<div class="flex-1 min-w-0">
<div class="font-semibold text-gray-900 dark:text-white truncate">{{ .title }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400 truncate">{{ .description }}</div>
</div>
{{ if .external }}
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
{{ end }}
</a>
{{ end }}
</div>
</div>
{{ end }}
{{/* Trending Searches - Configurable via site params */}}
{{ $trending := site.Params.features.search.trending_searches }}
{{ if $trending }}
<div>
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Trending Searches
</h3>
<div class="flex flex-wrap gap-2">
{{ range $trending }}
<button
@click="query = '{{ . }}'; search()"
class="rounded-full bg-gradient-to-r from-primary-100 to-blue-100 dark:from-primary-900/50 dark:to-blue-900/50 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:from-primary-200 hover:to-blue-200 dark:hover:from-primary-800/50 dark:hover:to-blue-800/50 transition-all cursor-pointer"
>
{{ . }}
</button>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{/* Search Results */}}
<div x-show="query && results.length > 0" class="divide-y divide-gray-200 dark:divide-gray-800">
<template x-for="(result, index) in results" :key="result.id">
<a
:href="result.url"
@mouseenter="selectedIndex = index"
@click="$store.search.open = false"
:class="selectedIndex === index ? 'bg-primary-50 dark:bg-primary-900/20 border-l-2 border-primary-600' : ''"
class="block px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors group search-result"
>
{{/* Result Badges */}}
<div class="flex items-center gap-2 mb-2 flex-wrap">
{{/* Type badge with i18n */}}
<span
x-show="result.filters && result.filters.type"
:class="{
'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300': result.filters.type && result.filters.type[0] === 'questions',
'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300': result.filters.type && result.filters.type[0] === 'faq',
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': result.filters.type && result.filters.type[0] === 'docs'
}"
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
x-text="result.filters.type ? (typeLabels[result.filters.type[0]] || result.filters.type[0]) : ''"
></span>
{{/* Category badge */}}
<span
x-show="result.filters && result.filters.category"
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
x-text="result.filters.category ? result.filters.category[0] : ''"
></span>
{{/* Difficulty badge */}}
<span
x-show="result.filters && result.filters.difficulty"
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300"
x-text="result.filters.difficulty ? result.filters.difficulty[0] : ''"
></span>
</div>
{{/* Title */}}
<h3 class="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors mb-2" x-html="result.meta.title || 'Untitled'"></h3>
{{/* Excerpt */}}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2" x-html="result.excerpt"></p>
</a>
</template>
</div>
{{/* Loading State */}}
<div x-show="query && loading" class="px-6 py-16 text-center">
<div class="inline-flex items-center gap-3">
<svg class="animate-spin h-8 w-8 text-primary-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<div class="text-left">
<p class="text-lg font-medium text-gray-900 dark:text-white">Searching...</p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="'Finding results for &quot;' + query + '&quot;'"></p>
</div>
</div>
</div>
{{/* No Results - Only show if search has completed at least once */}}
<div x-show="query && results.length === 0 && !loading && hasSearched" class="px-6 py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">No results found</h3>
<p class="mt-2 text-gray-600 dark:text-gray-400" x-text="'No results for &quot;' + query + '&quot;'"></p>
<button
@click="query = ''; $refs.searchInput.focus()"
class="mt-4 text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium"
>
Clear search
</button>
</div>
</div>
{{/* Footer */}}
<div class="border-t border-gray-200 dark:border-gray-800 px-4 py-3 bg-gray-50 dark:bg-gray-900/50">
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3">
<span class="flex items-center gap-1">
<kbd class="rounded bg-white dark:bg-gray-800 px-1.5 py-0.5 font-mono text-[10px] font-semibold border border-gray-300 dark:border-gray-700">↑↓</kbd>
Navigate
</span>
<span class="flex items-center gap-1">
<kbd class="rounded bg-white dark:bg-gray-800 px-1.5 py-0.5 font-mono text-[10px] font-semibold border border-gray-300 dark:border-gray-700">↵</kbd>
Select
</span>
<span x-show="availableFilters.length > 0" class="flex items-center gap-1">
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
</svg>
<span x-text="availableFilters.length + ' filters'"></span>
</span>
</div>
<a href="https://hugoblox.com" target="_blank" rel="noopener" class="flex items-center gap-1 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
Powered by Hugo Blox
</a>
</div>
</div>
</div>
</div>
</div>
<script>
// Alpine.js component for search modal
function searchModal() {
return {
query: '',
results: [],
loading: false,
hasSearched: false, // Track if at least one search has completed
selectedIndex: -1,
activeFilter: null,
availableFilters: [],
trendingSearches: {{ site.Params.features.search.trending_searches | default (slice "Search term 1" "Search term 2") | jsonify }},
pagefind: null,
// i18n translations for content types
typeLabels: {
'questions': {{ i18n "questions" | default "Questions" | jsonify }},
'faq': {{ i18n "faq" | default "FAQ" | jsonify }},
'docs': {{ i18n "docs" | default "Documentation" | jsonify }}
},
async init() {
// Initialize Pagefind
try {
this.pagefind = await import('/pagefind/pagefind.js');
await this.pagefind.init();
console.log('✓ Pagefind initialized');
// Load available filters
await this.loadFilters();
} catch (error) {
console.error('Failed to initialize Pagefind:', error);
}
// Keyboard navigation
this.$watch('$store.search.open', (open) => {
if (open) {
this.$nextTick(() => this.$refs.searchInput?.focus());
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
this.query = '';
this.results = [];
this.selectedIndex = -1;
this.hasSearched = false; // Reset search state when modal closes
}
});
// Arrow key and Enter navigation
window.addEventListener('keydown', (e) => {
if (!this.$store.search.open || this.results.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
this.selectedIndex = this.selectedIndex < this.results.length - 1 ? this.selectedIndex + 1 : 0;
this.scrollToSelected();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.results.length - 1;
this.scrollToSelected();
} else if (e.key === 'Enter' && this.selectedIndex >= 0) {
e.preventDefault();
const selected = this.results[this.selectedIndex];
if (selected) {
window.location.href = selected.url;
this.$store.search.open = false;
}
}
});
},
scrollToSelected() {
this.$nextTick(() => {
const selectedEl = document.querySelector('.search-result:nth-child(' + (this.selectedIndex + 1) + ')');
if (selectedEl) {
selectedEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
});
},
async search() {
if (!this.query.trim()) {
this.results = [];
this.hasSearched = false;
return;
}
this.loading = true;
this.hasSearched = false; // Reset until search completes
try {
const options = {};
// Apply active filter if selected
if (this.activeFilter) {
const [filterCategory, filterValue] = this.activeFilter.split(':');
options.filters = { [filterCategory]: filterValue };
}
const search = await this.pagefind.search(this.query, options);
console.log('Search results:', search);
console.log('Search filters:', search.filters);
// Process results
this.results = await Promise.all(
search.results.slice(0, 10).map(async (result) => {
const data = await result.data();
console.log('Result data:', data);
return {
id: data.url,
url: data.url,
meta: data.meta,
excerpt: data.excerpt,
filters: data.filters || {}
};
})
);
// Extract unique filters from results themselves
const filterCounts = { type: {}, category: {}, difficulty: {} };
this.results.forEach(result => {
if (result.filters) {
// Count type filters
if (result.filters.type) {
result.filters.type.forEach(val => {
filterCounts.type[val] = (filterCounts.type[val] || 0) + 1;
});
}
// Count category filters
if (result.filters.category) {
result.filters.category.forEach(val => {
filterCounts.category[val] = (filterCounts.category[val] || 0) + 1;
});
}
// Count difficulty filters
if (result.filters.difficulty) {
result.filters.difficulty.forEach(val => {
filterCounts.difficulty[val] = (filterCounts.difficulty[val] || 0) + 1;
});
}
}
});
console.log('Extracted filter counts from results:', filterCounts);
// Build filter list
const filters = [];
// Add type filters with i18n labels
Object.entries(filterCounts.type).forEach(([value, count]) => {
filters.push({
category: 'type',
value,
label: this.typeLabels[value] || value.charAt(0).toUpperCase() + value.slice(1),
count,
filterKey: `type:${value}`
});
});
// Add category filters
Object.entries(filterCounts.category).forEach(([value, count]) => {
filters.push({
category: 'category',
value,
label: value,
count,
filterKey: `category:${value}`
});
});
// Add difficulty filters
Object.entries(filterCounts.difficulty).forEach(([value, count]) => {
filters.push({
category: 'difficulty',
value,
label: value,
count,
filterKey: `difficulty:${value}`
});
});
this.availableFilters = filters;
console.log('Final availableFilters array:', this.availableFilters);
// Set first result as selected for keyboard navigation
if (this.results.length > 0) {
this.selectedIndex = 0;
}
} catch (error) {
console.error('Search error:', error);
this.results = [];
} finally {
this.loading = false;
this.hasSearched = true; // Mark that search has completed
}
},
async loadFilters() {
// Pre-load available filters by doing an empty search
try {
const search = await this.pagefind.search('');
console.log('Available Pagefind filters:', search.filters);
const filters = [];
// Load type filters (questions, faq, docs)
if (search.filters && search.filters.type) {
Object.entries(search.filters.type).forEach(([value, count]) => {
filters.push({
category: 'type',
value,
label: value.charAt(0).toUpperCase() + value.slice(1),
count,
filterKey: `type:${value}`
});
});
}
// Load category filters
if (search.filters && search.filters.category) {
Object.entries(search.filters.category).forEach(([value, count]) => {
filters.push({
category: 'category',
value,
label: value,
count,
filterKey: `category:${value}`
});
});
}
// Load difficulty filters (for questions)
if (search.filters && search.filters.difficulty) {
Object.entries(search.filters.difficulty).forEach(([value, count]) => {
filters.push({
category: 'difficulty',
value,
label: value,
count,
filterKey: `difficulty:${value}`
});
});
}
this.availableFilters = filters;
console.log('Processed filters:', this.availableFilters);
} catch (error) {
console.error('Failed to load filters:', error);
}
}
};
}
// Alpine store for global search state
document.addEventListener('alpine:init', () => {
Alpine.store('search', {
open: false
});
});
</script>
<style>
[x-cloak] { display: none !important; }
</style>
@@ -9,9 +9,16 @@
</div>
{{ end }}
<div class="content text-base">
{{/* Pagefind metadata for search filtering */}}
<div class="content" data-pagefind-body data-pagefind-meta="title:{{ .Title }}">
<h1>{{ .Title }}</h1>
{{ .Content }}
{{/* Use hidden spans for filter values - best practice per Pagefind docs */}}
<span data-pagefind-filter="type" style="display:none;">docs</span>
{{ with .Params.categories }}
<span data-pagefind-filter="category" style="display:none;">{{ index . 0 }}</span>
{{ end }}
</div>
{{ partial "components/last-edited.html" . }}
@@ -48,7 +48,110 @@
{{/* Process gradients and images */}}
{{ $background_layers := slice }}
{{ $gradient_layer := "" }}
{{ $has_gradient_mesh := false }}
{{ $gradient_mesh_html := "" }}
{{/* Enhanced Gradient Mesh System */}}
{{ $gradient_mesh := $bg.gradient_mesh | default dict }}
{{ if $gradient_mesh.enable }}
{{ $has_gradient_mesh = true }}
{{ $mesh_style := $gradient_mesh.style | default "orbs" }}
{{ $mesh_animation_raw := $gradient_mesh.animation | default "pulse" }}
{{ $mesh_intensity := $gradient_mesh.intensity | default "subtle" }}
{{ $mesh_colors := $gradient_mesh.colors | default (slice "primary-500/20" "primary-600/20") }}
{{/* Map animation names to CSS classes */}}
{{ $mesh_animation := "" }}
{{ if eq $mesh_animation_raw "pulse" }}
{{ $mesh_animation = "pulse" }}
{{ else if eq $mesh_animation_raw "float" }}
{{ $mesh_animation = "float" }}
{{ else if eq $mesh_animation_raw "rotate" }}
{{ $mesh_animation = "rotate-slow" }}
{{ else if ne $mesh_animation_raw "none" }}
{{ $mesh_animation = "pulse" }} {{/* Default fallback */}}
{{ end }}
{{/* Build gradient mesh HTML */}}
{{ $mesh_html := "<div class=\"absolute inset-0 -z-10\">" }}
{{/* Base gradient overlay */}}
{{ $base_gradient := printf "<div class=\"absolute inset-0 bg-gradient-to-br from-%s via-transparent to-%s\"></div>" (index $mesh_colors 0) (index $mesh_colors 1) }}
{{ $mesh_html = printf "%s%s" $mesh_html $base_gradient }}
{{/* Animated elements based on style */}}
{{ if eq $mesh_style "orbs" }}
{{ $orb_count := $gradient_mesh.orb_count | default 2 }}
{{ $orb_positions := $gradient_mesh.positions | default (slice "top-0 left-1/4" "bottom-0 right-1/4") }}
{{ $orb_sizes := $gradient_mesh.sizes | default (slice "w-96 h-96" "w-96 h-96") }}
{{ range $index, $position := first $orb_count $orb_positions }}
{{ $size := index $orb_sizes $index | default "w-96 h-96" }}
{{ $color := index $mesh_colors $index | default "primary-500/30" }}
{{ $blur_class := cond (eq $mesh_intensity "bold") "blur-2xl" (cond (eq $mesh_intensity "medium") "blur-3xl" "blur-3xl") }}
{{ $opacity := cond (eq $mesh_intensity "bold") "40" (cond (eq $mesh_intensity "medium") "30" "30") }}
{{/* Extract base color and adjust opacity for orbs */}}
{{ $orb_color := $color }}
{{ if not (in $color "/") }}
{{ $orb_color = printf "%s/%s" $color $opacity }}
{{ end }}
{{ $animation_delay := "" }}
{{ if gt $index 0 }}
{{ $animation_delay = printf " style=\"animation-delay: %ds;\"" (mul $index 2) }}
{{ end }}
{{/* Build orb with or without animation */}}
{{ $animate_class := cond (ne $mesh_animation "") (printf "animate-%s" $mesh_animation) "" }}
{{ $orb := printf "<div class=\"absolute %s %s bg-%s rounded-full filter %s %s\"%s></div>" $position $size $orb_color $blur_class $animate_class $animation_delay }}
{{ $mesh_html = printf "%s%s" $mesh_html $orb }}
{{ end }}
{{ else if eq $mesh_style "waves" }}
{{/* Flowing wave effect - multiple horizontal bands */}}
{{ $wave_count := $gradient_mesh.wave_count | default 3 }}
{{ $blur_class := cond (eq $mesh_intensity "bold") "blur-2xl" (cond (eq $mesh_intensity "medium") "blur-3xl" "blur-3xl") }}
{{ range $index := seq 0 (sub $wave_count 1) }}
{{ $color_index := mod $index (len $mesh_colors) }}
{{ $color := index $mesh_colors $color_index }}
{{ $top_pos := mul $index 33 }}
{{ $height := "h-64" }}
{{ $anim_delay := printf " style=\"animation-delay: %ds; animation-duration: %ds;\"" (mul $index 1) (add 8 (mul $index 2)) }}
{{/* Apply animation if not none */}}
{{ $animate_class := cond (ne $mesh_animation "") (printf "animate-%s" $mesh_animation) "" }}
{{ $wave := printf "<div class=\"absolute left-0 right-0 %s bg-%s filter %s %s\" style=\"top: %d%%; transform: skewY(-2deg);\"%s></div>" $height $color $blur_class $animate_class $top_pos $anim_delay }}
{{ $mesh_html = printf "%s%s" $mesh_html $wave }}
{{ end }}
{{ else if eq $mesh_style "dots" }}
{{/* Dotted grid pattern */}}
{{ $dot_size := $gradient_mesh.dot_size | default "w-4 h-4" }}
{{ $spacing := $gradient_mesh.spacing | default "16" }}
{{ $color := index $mesh_colors 0 | default "primary-500/20" }}
{{/* Create a dotted pattern background */}}
{{ $dots := printf "<div class=\"absolute inset-0\" style=\"background-image: radial-gradient(circle, rgb(59 130 246 / 0.2) 2px, transparent 2px); background-size: %spx %spx; background-position: 0 0, %spx %spx;\"></div>" $spacing $spacing (div (int $spacing) 2) (div (int $spacing) 2) }}
{{ $mesh_html = printf "%s%s" $mesh_html $dots }}
{{ else if eq $mesh_style "grid" }}
{{/* Grid lines pattern */}}
{{ $color := index $mesh_colors 0 | default "primary-500/10" }}
{{ $line_width := $gradient_mesh.line_width | default "1" }}
{{ $spacing := $gradient_mesh.spacing | default "32" }}
{{/* Create grid pattern */}}
{{ $grid := printf "<div class=\"absolute inset-0\" style=\"background-image: linear-gradient(rgb(59 130 246 / 0.1) %spx, transparent %spx), linear-gradient(90deg, rgb(59 130 246 / 0.1) %spx, transparent %spx); background-size: %spx %spx;\"></div>" $line_width $line_width $line_width $line_width $spacing $spacing }}
{{ $mesh_html = printf "%s%s" $mesh_html $grid }}
{{ end }}
{{ $mesh_html = printf "%s</div>" $mesh_html }}
{{ $gradient_mesh_html = $mesh_html | safeHTML }}
{{ end }}
{{/* Legacy simple gradient support */}}
{{ if and $bg.gradient.start $bg.gradient.end }}
{{ $angle := string $bg.gradient.direction | default "135" }}
{{ $start_color := $bg.gradient.start }}
@@ -56,6 +159,17 @@
{{ $gradient_layer = printf "linear-gradient(%sdeg, %s, %s)" $angle $start_color $end_color }}
{{ end }}
{{/* Process video backgrounds */}}
{{ $bg_video := "" }}
{{ $bg_video_class := "" }}
{{ if $bg.video.filename }}
{{ $bg_video = resources.Get (printf "media/%s" $bg.video.filename) }}
{{ if $bg.video.flip }}
{{ $bg_video_class = "flip" }}
{{ end }}
{{ end }}
{{/* Process image backgrounds */}}
{{ if $bg.image.filename }}
{{ $bg_img := resources.Get (printf "media/%s" $bg.image.filename) }}
{{ if $bg_img }}
@@ -94,8 +208,12 @@
{{/* Route to appropriate handler */}}
{{ if $is_preact }}
{{/* Render Preact block with styling wrapper */}}
<section id="{{$hash_id}}" class="relative hbb-section {{$widget_class}} {{if $bg.text_color_light}}dark{{else if (eq $bg.text_color_light false)}}light{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}}>
<div class="home-section-bg {{if $bg.image}} bg-image{{if ($bg.image.parallax | default true) }} parallax{{end}}{{end}}" {{with $style_bg}}style="{{. | safeCSS}}"{{end}}></div>
<section id="{{$hash_id}}" class="relative {{if $has_gradient_mesh}}overflow-hidden{{end}} hbb-section {{$widget_class}} {{if $bg.text_color_light}}dark{{else if (eq $bg.text_color_light false)}}light{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}}>
{{/* Gradient Mesh Background */}}
{{ if $has_gradient_mesh }}{{ $gradient_mesh_html }}{{ end }}
<div class="home-section-bg {{if $bg.image}} bg-image{{if ($bg.image.parallax | default true) }} parallax{{end}}{{end}}" {{with $style_bg}}style="{{. | safeCSS}}"{{end}}>
{{with $bg_video}}<video class="bg-video {{$bg_video_class}}" playsinline="" preload="auto" loop="" muted autoplay="" tabindex="-1" width="100%" height="100%" src="{{.RelPermalink}}" style="width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 1;"></video>{{end}}
</div>
{{/* Use generic Preact wrapper for content */}}
{{ partial "blox/preact-wrapper" (dict "wcPage" $page "wcBlock" $block "wcBlockType" $block_type "wcIdentifier" $hash_id) }}
</section>
@@ -118,8 +236,12 @@
{{ $widget_args := dict "wcPage" $page "wcBlock" $block "wcIdentifier" $hash_id }}
{{/* Render traditional block with wrapper */}}
<section id="{{$hash_id}}" class="relative hbb-section {{$widget_class}} {{if $bg.text_color_light}}dark{{else if (eq $bg.text_color_light false)}}light{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}}>
<div class="home-section-bg {{if $bg.image}} bg-image{{if ($bg.image.parallax | default true) }} parallax{{end}}{{end}}" {{with $style_bg}}style="{{. | safeCSS}}"{{end}}></div>
<section id="{{$hash_id}}" class="relative {{if $has_gradient_mesh}}overflow-hidden{{end}} hbb-section {{$widget_class}} {{if $bg.text_color_light}}dark{{else if (eq $bg.text_color_light false)}}light{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}}>
{{/* Gradient Mesh Background */}}
{{ if $has_gradient_mesh }}{{ $gradient_mesh_html }}{{ end }}
<div class="home-section-bg {{if $bg.image}} bg-image{{if ($bg.image.parallax | default true) }} parallax{{end}}{{end}}" {{with $style_bg}}style="{{. | safeCSS}}"{{end}}>
{{with $bg_video}}<video class="bg-video {{$bg_video_class}}" playsinline="" preload="auto" loop="" muted autoplay="" tabindex="-1" width="100%" height="100%" src="{{.RelPermalink}}" style="width: 100%; height: 100%; object-fit: cover; object-position: center center; opacity: 1;"></video>{{end}}
</div>
{{ partial $block_path $widget_args }}
</section>
{{ end }}
@@ -107,6 +107,17 @@
{{ end }}
{{ end }}
{{/* Detect FAQ blocks for JSON-LD structured data (avoid race conditions) */}}
{{ if eq $block_type "faq" }}
{{ if $block.content.items }}
{{ $faq_items := $page.Store.Get "faq_items" | default slice }}
{{ range $block.content.items }}
{{ $faq_items = $faq_items | append (dict "question" .question "answer" .answer) }}
{{ end }}
{{ $page.Store.Set "faq_items" $faq_items }}
{{ end }}
{{ end }}
{{ $widget_config_file := printf "blox/%s/config.html" $block_type }}
{{ if templates.Exists (printf "_partials/%s" $widget_config_file) }}
{{ $hash_id := $block.id | default (printf "section-%s" (replace $block_type "." "-")) }}
@@ -0,0 +1,48 @@
{{- $page := .page }}
{{ $summary := .summary }}
{{/* Get FAQ items - can be from params.faqs, Page.Store (from init.html), or child pages */}}
{{ $faq_items := slice }}
{{/* Method 1: FAQ items defined in front matter */}}
{{ if $page.Params.faqs }}
{{ $faq_items = $page.Params.faqs }}
{{ end }}
{{/* Method 2: Extract from Page.Store (populated by init.html to avoid race conditions) */}}
{{ if not $faq_items }}
{{ with $page.Store.Get "faq_items" }}
{{ $faq_items = . }}
{{ end }}
{{ end }}
{{/* Method 3: Auto-generate from child pages in a FAQ section */}}
{{ if and (not $faq_items) ($page.IsSection) }}
{{ range $page.RegularPages }}
{{ $faq_items = $faq_items | append (dict "question" (.Params.question | default .Title) "answer" (.Params.answer | default .Summary | default (.Content | plainify))) }}
{{ end }}
{{ end }}
{{/* Only output FAQ schema if we have FAQ items */}}
{{ if $faq_items }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{{- range $index, $item := $faq_items -}}
{{- if $index }},{{ end -}}
{
"@type": "Question",
"name": {{ $item.question | jsonify }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ $item.answer | plainify | jsonify }}
}
}
{{- end -}}
]
}
</script>
{{ end }}
@@ -20,4 +20,19 @@
{{ partial "jsonld/event.html" (dict "page" $page "summary" $summary) }}
{{ end }}
{{/* Q&A Page - individual question/answer page */}}
{{ if eq $page.Type "questions" }}
{{ partial "jsonld/qapage.html" (dict "page" $page "summary" $summary) }}
{{ end }}
{{/* FAQ Page type */}}
{{ if eq $page.Type "faq" }}
{{ partial "jsonld/faqpage.html" (dict "page" $page "summary" $summary) }}
{{ end }}
{{- end }}
{{/* FAQ content - can appear on any page type including landing pages and homepage */}}
{{ if or ($page.Params.faq_page) ($page.Params.faqs) }}
{{ partial "jsonld/faqpage.html" (dict "page" $page "summary" $summary) }}
{{ end }}
@@ -0,0 +1,88 @@
{{- $page := .page }}
{{ $summary := .summary }}
{{ $featured_image := ($page.Resources.ByType "image").GetMatch "*featured*" }}
{{/* Get author and publisher info */}}
{{ $author := partial "functions/get_author_name" $page }}
{{ $publisher := site.Params.marketing.seo.org_name | default site.Title }}
{{/* Extract question - prioritize explicit param, fallback to title */}}
{{ $question := $page.Params.question | default $page.Title }}
{{/* Extract answer - prioritize explicit param, fallback to summary, then content */}}
{{ $answer := "" }}
{{ if $page.Params.answer }}
{{ $answer = $page.Params.answer }}
{{ else if $page.Params.summary }}
{{ $answer = $page.Params.summary }}
{{ else }}
{{ $answer = $page.Content | plainify | truncate 500 }}
{{ end }}
{{/* Get upvote/downvote counts if available */}}
{{ $upvote_count := $page.Params.upvote_count | default 0 }}
{{ $downvote_count := $page.Params.downvote_count | default 0 }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "QAPage",
"mainEntity": {
"@type": "Question",
"name": {{ $question | jsonify }},
"text": {{ $question | jsonify }},
"answerCount": 1,
{{ if $upvote_count }}"upvoteCount": {{ $upvote_count }},{{ end }}
"dateCreated": {{ $page.Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
{{- with $author -}}
"author": {
"@type": "Person",
"name": {{ . | jsonify }}
},
{{- end -}}
"acceptedAnswer": {
"@type": "Answer",
"text": {{ $answer | plainify | jsonify }},
{{ if $upvote_count }}"upvoteCount": {{ $upvote_count }},{{ end }}
{{ if $downvote_count }}"downvoteCount": {{ $downvote_count }},{{ end }}
"dateCreated": {{ $page.Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
{{- with $author -}}
"author": {
"@type": "Person",
"name": {{ . | jsonify }}
}{{- if or $page.Params.url $featured_image -}},{{- end -}}
{{- end -}}
{{- with $page.Params.url -}}
"url": {{ . | jsonify }}{{ if $featured_image }},{{ end }}
{{- end -}}
{{ if $featured_image }}
"image": {{ $featured_image.Permalink | jsonify }}
{{ end }}
}{{ if or $page.Params.suggested_answer $page.Params.additional_answers }},{{ end }}
{{- with $page.Params.suggested_answer -}}
"suggestedAnswer": [
{{- range $index, $answer := . -}}
{{- if $index }},{{ end -}}
{
"@type": "Answer",
"text": {{ $answer.text | jsonify }},
{{- with $answer.author -}}
"author": {
"@type": "Person",
"name": {{ . | jsonify }}
}{{ if or $answer.upvote_count $answer.date }},{{ end }}
{{- end -}}
{{- with $answer.upvote_count -}}
"upvoteCount": {{ . }},
{{- end -}}
{{- with $answer.date -}}
"dateCreated": {{ (time .) | time.Format "2006-01-02T15:04:05Z07:00" | jsonify }}
{{- end -}}
}
{{- end -}}
]
{{- end -}}
}
}
</script>
@@ -29,48 +29,25 @@
{{ end }}
{{ end }}
{{/* Show site search? */}}
{{ $show_search := site.Params.header.navbar.show_search | default false }}
{{/* Load Alpine JS extension? */}}
{{ if (.Page.Store.Get "has_alpine") }}
{{/* Alpine is needed for search modal and other interactive components */}}
{{ if or (.Page.Store.Get "has_alpine") $show_search }}
<script>console.log('✓ Alpine.js loading on demand');</script>
{{ $alpine_js := resources.Get "dist/lib/alpinejs/cdn.min.js" }}
{{ $alpine_js = $alpine_js | resources.Fingerprint "sha256" }}
<script src="{{ $alpine_js.RelPermalink }}" integrity="{{ $alpine_js.Data.Integrity }}" defer></script>
{{ end }}
{{/* Show site search? */}}
{{ $show_search := site.Params.header.navbar.show_search | default false }}
{{/* Hugo Blox Search JavaScript (CSS is in main.css) */}}
{{ if $show_search }}
{{ $pf_css := resources.Get "dist/pagefind/pagefind-ui.css" }}
{{ $pf_css = $pf_css | resources.Fingerprint "sha256" }}
<link type="text/css" rel="stylesheet" href="{{ $pf_css.RelPermalink }}" integrity="{{ $pf_css.Data.Integrity }}" />
{{ $pf_js := resources.Get "dist/pagefind/pagefind-ui.js" }}
{{ $pf_js = $pf_js | resources.Fingerprint "sha256" }}
<script src="{{ $pf_js.RelPermalink }}" integrity="{{ $pf_js.Data.Integrity }}"></script>
{{ $search_config := dict "baseUrl" (relURL "") }}
<script id="search-config" type="application/json">{{ $search_config | jsonify | safeJS }}</script>
<style>
html.dark {
--pagefind-ui-primary: #eeeeee;
--pagefind-ui-text: #eeeeee;
--pagefind-ui-background: #152028;
--pagefind-ui-border: #152028;
--pagefind-ui-tag: #152028;
}
/* Make search modal close button match size of search input */
.search-close > svg {
height: calc(64px * var(--pagefind-ui-scale));
width: calc(64px * var(--pagefind-ui-scale));
}
</style>
{{ $search_js := resources.Get "js/hb-search.js" | resources.Minify }}
{{- if hugo.IsProduction -}}
{{ $search_js = $search_js | fingerprint "sha256" }}
{{- end -}}
<script defer src="{{ $search_js.RelPermalink }}"{{ if hugo.IsProduction }} integrity="{{ $search_js.Data.Integrity }}" crossorigin="anonymous"{{ end }}></script>
<script defer src="{{ $search_js.RelPermalink }}"{{ if hugo.IsProduction }} integrity="{{ $search_js.Data.Integrity }}"{{ end }}></script>
{{ end }}
{{/* Mermaid */}}
@@ -67,14 +67,16 @@
</a>
{{ end }}
{{ else }}
<!-- Fallback gradient with subtle pattern -->
<div class="w-full h-full bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center">
<div class="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<svg class="w-8 h-8 text-white/60" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
</svg>
<!-- Fallback gradient with subtle pattern - make clickable for consistency -->
<a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="block w-full h-full">
<div class="w-full h-full bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center hover:from-blue-500/30 hover:to-purple-500/30 transition-colors duration-300">
<div class="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<svg class="w-8 h-8 text-white/60" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
</svg>
</div>
</div>
</div>
</a>
{{end}}
<!-- Subtle overlay for better text contrast (allow clicks to pass through) -->
@@ -0,0 +1,65 @@
{{- define "main" -}}
<div class="mx-auto flex max-w-screen-xl">
{{ partial "components/sidebar.html" (dict "context" . "no_sidebar" false) }}
<article class="w-full min-w-0 min-h-[calc(100vh-var(--navbar-height))] px-6 pt-4 pb-8 md:px-12">
<div class="max-w-4xl mx-auto">
{{ if (.Params.show_breadcrumb | default true) }}
<div class="mb-4">
{{ partial "components/breadcrumb.html" . }}
</div>
{{ end }}
<div class="prose prose-slate lg:prose-xl dark:prose-invert mb-8">
<h1>{{ .Title }}</h1>
{{ with .Content }}
{{ . }}
{{ end }}
</div>
{{/* List all FAQ pages */}}
{{ $pages := .Pages.ByDate.Reverse }}
<div class="grid gap-6 md:grid-cols-2">
{{ range $pages }}
<a href="{{ .RelPermalink }}" class="block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-500 dark:hover:border-primary-500 transition-all hover:shadow-md">
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
{{ .Title }}
</h3>
{{ with .Summary }}
<p class="text-gray-600 dark:text-gray-400 mb-3">
{{ . }}
</p>
{{ end }}
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<span>
{{ if .Params.faqs }}
{{ len .Params.faqs }} {{ T "questions_count" | default "questions" }}
{{ else if .Pages }}
{{ len .Pages }} {{ T "questions_count" | default "questions" }}
{{ end }}
</span>
<span class="text-primary-600 dark:text-primary-400 font-medium">
{{ T "read_more" | default "View" }} →
</span>
</div>
</a>
{{ end }}
</div>
{{ if eq (len $pages) 0 }}
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
<p class="text-lg">{{ T "no_questions_yet" | default "No FAQ pages available yet." }}</p>
</div>
{{ end }}
{{ partial "components/paginator" . }}
</div>
</article>
</div>
{{- end -}}
@@ -0,0 +1,141 @@
{{- define "main" -}}
{{/* Enable Alpine.js for interactive FAQ accordion */}}
{{ .Page.Store.Set "has_alpine" true }}
{{/* Pagefind metadata for search filtering */}}
<div data-pagefind-body data-pagefind-meta="title:{{ .Title }}">
{{/* Use hidden spans for filter values - best practice per Pagefind docs */}}
<span data-pagefind-filter="type" style="display:none;">faq</span>
{{ with .Params.categories }}
<span data-pagefind-filter="category" style="display:none;">{{ index . 0 }}</span>
{{ end }}
<div class="mx-auto flex max-w-screen-xl">
{{ partial "components/sidebar.html" (dict "context" . "no_sidebar" false) }}
{{ partial "components/toc.html" . }}
<article class="flex w-full min-w-0 min-h-[calc(100vh-var(--navbar-height))] justify-center break-words pb-8 pr-[calc(env(safe-area-inset-right)-1.5rem)]">
<main class="prose prose-slate lg:prose-xl dark:prose-invert w-full min-w-0 max-w-6xl px-6 pt-4 md:px-12">
{{ if (.Params.show_breadcrumb | default true) }}
<div class="mb-1">
{{ partial "components/breadcrumb.html" . }}
</div>
{{ end }}
<h1>{{ .Title }}</h1>
{{ with .Content }}
<div class="lead mb-8">
{{ . }}
</div>
{{ end }}
{{/* FAQ Accordion */}}
{{ with .Params.faqs }}
<div class="faq-container space-y-4 not-prose" x-data="{ openItem: null }">
{{ range $index, $item := . }}
<div class="faq-item border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<button
type="button"
class="w-full px-6 py-4 text-left flex items-center justify-between bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="openItem = openItem === {{ $index }} ? null : {{ $index }}"
:aria-expanded="openItem === {{ $index }}"
>
<span class="font-semibold text-gray-900 dark:text-gray-100 pr-8">
{{ $item.question }}
</span>
<svg
class="w-5 h-5 text-gray-500 transition-transform flex-shrink-0"
:class="{ 'rotate-180': openItem === {{ $index }} }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div
x-show="openItem === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
>
<div class="prose prose-slate dark:prose-invert max-w-none">
{{ $item.answer | markdownify }}
</div>
</div>
</div>
{{ end }}
</div>
{{ end }}
{{/* If no explicit FAQs, show child pages as FAQ items */}}
{{ if not .Params.faqs }}
{{ $child_pages := .Pages }}
{{ if $child_pages }}
<div class="faq-container space-y-4 not-prose" x-data="{ openItem: null }">
{{ range $index, $page := $child_pages }}
<div class="faq-item border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<button
type="button"
class="w-full px-6 py-4 text-left flex items-center justify-between bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="openItem = openItem === {{ $index }} ? null : {{ $index }}"
:aria-expanded="openItem === {{ $index }}"
>
<span class="font-semibold text-gray-900 dark:text-gray-100 pr-8">
{{ $page.Params.question | default $page.Title }}
</span>
<svg
class="w-5 h-5 text-gray-500 transition-transform flex-shrink-0"
:class="{ 'rotate-180': openItem === {{ $index }} }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div
x-show="openItem === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
>
<div class="prose prose-slate dark:prose-invert max-w-none">
{{ if $page.Params.answer }}
{{ $page.Params.answer | markdownify }}
{{ else }}
{{ $page.Content }}
{{ end }}
</div>
{{ if not $page.Params.answer }}
<a href="{{ $page.RelPermalink }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 text-sm font-medium mt-2 inline-block">
{{ T "read_more" | default "Read more" }} →
</a>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ partial "components/last-edited.html" . }}
{{ partial "components/feedback.html" . }}
{{ partial "comments.html" . }}
</main>
</article>
</div>
</div>{{/* End Pagefind metadata wrapper */}}
{{- end -}}
@@ -0,0 +1,130 @@
{{- define "main" -}}
<div class="mx-auto flex max-w-screen-xl">
{{ partial "components/sidebar.html" (dict "context" . "no_sidebar" false) }}
<article class="w-full min-w-0 min-h-[calc(100vh-var(--navbar-height))] px-6 pt-4 pb-8 md:px-12">
<div class="max-w-4xl mx-auto">
{{ if (.Params.show_breadcrumb | default true) }}
<div class="mb-4">
{{ partial "components/breadcrumb.html" . }}
</div>
{{ end }}
<div class="prose prose-slate lg:prose-xl dark:prose-invert mb-8">
<h1>{{ .Title }}</h1>
{{ with .Content }}
{{ . }}
{{ end }}
</div>
{{/* Search box for Q&A - Integrated with Pagefind */}}
{{ if site.Params.features.search.provider }}
<div class="mb-8">
<button
data-search-toggle
type="button"
class="w-full group cursor-pointer"
aria-label="{{ T "search_questions" | default "Search questions" }}">
<div class="relative">
<div class="w-full px-4 py-3 pl-10 text-left text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all cursor-pointer">
{{ T "search_questions" | default "Search questions..." }}
</div>
<svg class="absolute left-3 top-3.5 w-5 h-5 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</button>
</div>
{{ end }}
{{/* Category filter */}}
{{ $categories := .Site.Taxonomies.categories }}
{{ if $categories }}
<div class="mb-8">
<h2 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">{{ T "browse_by_category" | default "Browse by Category" }}</h2>
<div class="flex flex-wrap gap-2">
<a href="{{ .RelPermalink }}" class="px-4 py-2 text-sm rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors">
{{ T "all" | default "All" }}
</a>
{{ range $name, $taxonomy := $categories }}
<a href="{{ "/categories/" | relLangURL }}{{ $name | urlize }}" class="px-4 py-2 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
{{ $name }} ({{ $taxonomy.Count }})
</a>
{{ end }}
</div>
</div>
{{ end }}
{{/* Q&A List */}}
{{ $pages := .Pages }}
{{ if .Params.sort_by }}
{{ if eq .Params.sort_by "date" }}
{{ $pages = .Pages.ByDate.Reverse }}
{{ else if eq .Params.sort_by "title" }}
{{ $pages = .Pages.ByTitle }}
{{ else if eq .Params.sort_by "weight" }}
{{ $pages = .Pages.ByWeight }}
{{ end }}
{{ else }}
{{ $pages = .Pages.ByDate.Reverse }}
{{ end }}
<div class="space-y-4">
{{ range $pages }}
<div class="p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-500 dark:hover:border-primary-500 transition-all hover:shadow-md">
<a href="{{ .RelPermalink }}" class="block">
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2 hover:text-primary-600 dark:hover:text-primary-400">
{{ .Params.question | default .Title }}
</h3>
{{ if .Params.answer }}
<p class="text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
{{ .Params.answer | plainify | truncate 200 }}
</p>
{{ else if .Summary }}
<p class="text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
{{ .Summary }}
</p>
{{ end }}
<div class="flex flex-wrap items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
{{ .Date.Format "Jan 2, 2006" }}
</span>
{{ range first 3 .Params.tags }}
<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
#{{ . }}
</span>
{{ end }}
{{ if .Params.difficulty }}
<span class="px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
{{ .Params.difficulty }}
</span>
{{ end }}
</div>
</a>
</div>
{{ end }}
</div>
{{ if eq (len $pages) 0 }}
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="text-lg">{{ T "no_questions_yet" | default "No questions yet. Check back soon!" }}</p>
</div>
{{ end }}
{{ partial "components/paginator" . }}
</div>
</article>
</div>
{{- end -}}
@@ -0,0 +1,151 @@
{{- define "main" -}}
{{/* Pagefind metadata for search filtering */}}
<div data-pagefind-body
{{ with .Params.question }}data-pagefind-meta="title:{{ . }}"{{ end }}>
{{/* Use hidden spans for filter values - best practice per Pagefind docs */}}
<span data-pagefind-filter="type" style="display:none;">questions</span>
{{ with .Params.categories }}
<span data-pagefind-filter="category" style="display:none;">{{ index . 0 }}</span>
{{ end }}
{{ with .Params.difficulty }}
<span data-pagefind-filter="difficulty" style="display:none;">{{ . }}</span>
{{ end }}
<div class="mx-auto flex max-w-screen-xl">
{{ partial "components/sidebar.html" (dict "context" . "no_sidebar" false) }}
{{ partial "components/toc.html" . }}
<article class="flex w-full min-w-0 min-h-[calc(100vh-var(--navbar-height))] justify-center break-words pb-8 pr-[calc(env(safe-area-inset-right)-1.5rem)]">
<main class="prose prose-slate lg:prose-xl dark:prose-invert w-full min-w-0 max-w-6xl px-6 pt-4 md:px-12">
{{ if (.Params.show_breadcrumb | default true) }}
<div class="mb-1">
{{ partial "components/breadcrumb.html" . }}
</div>
{{ end }}
{{/* Question as title */}}
<div class="qa-question mb-6">
<h1 class="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
{{ .Params.question | default .Title }}
</h1>
{{/* Metadata */}}
<div class="text-sm text-gray-600 dark:text-gray-400 flex flex-wrap gap-4 mb-4 not-prose">
{{ if .Date }}
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "January 2, 2006" }}
</time>
</div>
{{ end }}
{{ if .Lastmod }}
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>Updated {{ .Lastmod.Format "January 2, 2006" }}</span>
</div>
{{ end }}
{{ with .Params.difficulty }}
<span class="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
{{ . }}
</span>
{{ end }}
</div>
{{/* Tags and Categories */}}
<div class="flex flex-wrap gap-2 not-prose">
{{ range .Params.tags }}
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}" class="px-3 py-1 text-sm rounded-full bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
#{{ . }}
</a>
{{ end }}
{{ range .Params.categories }}
<a href="{{ "/categories/" | relLangURL }}{{ . | urlize }}" class="px-3 py-1 text-sm rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors">
{{ . }}
</a>
{{ end }}
</div>
</div>
{{/* Accepted Answer */}}
<div class="qa-answer">
{{ if .Params.answer }}
{{/* Explicit answer parameter */}}
<div class="mb-4 p-4 bg-green-50 dark:bg-green-900/20 border-l-4 border-green-500 rounded">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<span class="font-semibold text-green-800 dark:text-green-300 not-prose">{{ T "accepted_answer" | default "Accepted Answer" }}</span>
</div>
{{ .Params.answer | markdownify }}
</div>
{{ end }}
{{/* Full content */}}
<div class="content">
{{ .Content }}
</div>
</div>
{{/* Suggested/Additional Answers */}}
{{ with .Params.suggested_answer }}
<div class="mt-8 border-t pt-6">
<h2 class="text-2xl font-bold mb-4">{{ T "other_answers" | default "Other Answers" }}</h2>
{{ range $index, $answer := . }}
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
{{ with $answer.author }}
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ . }}
{{ with $answer.date }} · {{ (time .) | time.Format "January 2, 2006" }}{{ end }}
</div>
{{ end }}
<div class="text-gray-800 dark:text-gray-200">
{{ $answer.text | markdownify }}
</div>
{{ with $answer.upvote_count }}
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
👍 {{ . }} {{ T "helpful" | default "helpful" }}
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{/* Related Q&As */}}
{{ $related := .Site.RegularPages.Related . | first 5 }}
{{ with $related }}
<div class="mt-8 border-t pt-6">
<h2 class="text-2xl font-bold mb-4">{{ T "related_questions" | default "Related Questions" }}</h2>
<ul class="space-y-2 not-prose">
{{ range . }}
<li>
<a href="{{ .RelPermalink }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300">
{{ .Params.question | default .Title }}
</a>
</li>
{{ end }}
</ul>
</div>
{{ end }}
{{ partial "components/last-edited.html" . }}
{{ partial "components/next-in-series.html" . }}
{{ partial "components/feedback.html" . }}
{{ partial "comments.html" . }}
</main>
</article>
</div>
</div>{{/* End Pagefind metadata wrapper */}}
{{- end -}}
@@ -1,5 +1,8 @@
{{ define "main" }}
{{/* Exclude taxonomy pages from search indexing */}}
<div data-pagefind-ignore>
<div class="max-w-prose mx-auto flex justify-start">
<article class="prose prose-slate lg:prose-xl dark:prose-invert max-w-none">
<h1 class="lg:text-6xl !mb-4">{{ .Title }}</h1>
@@ -29,4 +32,6 @@
</div>
</div>{{/* End pagefind-ignore wrapper */}}
{{- end -}}
+5
View File
@@ -1,5 +1,8 @@
{{ define "main" }}
{{/* Exclude taxonomy term listing pages from search indexing */}}
<div data-pagefind-ignore>
<div class="max-w-prose mx-auto flex justify-start">
<article class="prose prose-slate lg:prose-xl dark:prose-invert max-w-none">
<h1 class="lg:text-6xl !mb-4">{{ .Title }}</h1>
@@ -29,4 +32,6 @@
</div>
</div>{{/* End pagefind-ignore wrapper */}}
{{- end -}}