mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-13 20:14:15 +00:00
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:
1 parent
3eef2cba11
commit
692dc8c62e
40 files changed
+2856
-122
No files matched your search
@@ -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";
|
||||
@@ -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
|
||||
});
|
||||
@@ -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`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 "' + query + '"'"></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 "' + query + '"'"></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 -}}
|
||||
@@ -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 -}}
|
||||
Reference in new issue
Block a user