mirror of
https://github.com/CaiJimmy/hugo-theme-stack.git
synced 2025-02-06 20:03:31 +08:00
feat: new archives layout (#989)
* feat(archives): add search bar to archives page * fix: error `file is nil; wrap it in if or with` from Hugo 0.123.0 * style: rename `search.ts` to `archives.ts` * refactor: remove `search` page layout * doc: remove `search` from `exampleSite` * fix: generate JSON output for archives page in exampleSite * feat: put archives search form under categories list
This commit is contained in:
parent
9df2641193
commit
5d100a7f99
165
assets/ts/archives.ts
Normal file
165
assets/ts/archives.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
interface PageData {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Search {
|
||||||
|
private data: PageData[];
|
||||||
|
private form: HTMLFormElement;
|
||||||
|
private input: HTMLInputElement;
|
||||||
|
private resultTitle: HTMLHeadElement;
|
||||||
|
private resultTitleTemplate: string;
|
||||||
|
|
||||||
|
constructor({ form, input, resultTitle, resultTitleTemplate }) {
|
||||||
|
this.form = form;
|
||||||
|
this.input = input;
|
||||||
|
this.resultTitle = resultTitle;
|
||||||
|
this.resultTitleTemplate = resultTitleTemplate;
|
||||||
|
|
||||||
|
this.handleQueryString();
|
||||||
|
this.bindQueryStringChange();
|
||||||
|
this.bindSearchForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getIndex() {
|
||||||
|
if (!this.data) {
|
||||||
|
const jsonURL = this.form.dataset.json;
|
||||||
|
this.data = await fetch(jsonURL).then(res => res.json());
|
||||||
|
}
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async searchKeywords(keywords: string[]) {
|
||||||
|
const index = await this.getIndex();
|
||||||
|
/// Return an set of ids that match the keywords
|
||||||
|
return new Set(index.filter(item => {
|
||||||
|
return keywords.every(keyword => {
|
||||||
|
return item.title.includes(keyword) || item.content.includes(keyword);
|
||||||
|
});
|
||||||
|
}).map(item => item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doSearch(keywords: string[]) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const results = await this.searchKeywords(keywords);
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
/// Hide all articles except the ones that are in the results
|
||||||
|
const archiveGroups = document.querySelectorAll('.archives-group') as NodeListOf<HTMLDivElement>;
|
||||||
|
|
||||||
|
archiveGroups.forEach(group => {
|
||||||
|
const articles = Array.from(group.querySelectorAll('article'));
|
||||||
|
articles.map(article => {
|
||||||
|
article.style.display = 'none';
|
||||||
|
article.style.removeProperty('border-bottom');
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchingArticles = articles.filter(article => results.has(article.id));
|
||||||
|
|
||||||
|
const hasResults = matchingArticles.length > 0;
|
||||||
|
if (!hasResults) return group.style.display = 'none';
|
||||||
|
|
||||||
|
matchingArticles.map(article => article.style.removeProperty('display'));
|
||||||
|
matchingArticles[matchingArticles.length - 1].style.borderBottom = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
this.resultTitle.innerText = this.generateResultTitle(results.size, ((endTime - startTime) / 1000).toPrecision(1));
|
||||||
|
this.resultTitle.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateResultTitle(resultLen, time) {
|
||||||
|
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindSearchForm() {
|
||||||
|
let lastSearch = '';
|
||||||
|
|
||||||
|
const eventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const keywords = this.input.value.trim();
|
||||||
|
|
||||||
|
Search.updateQueryString(keywords, true);
|
||||||
|
|
||||||
|
if (keywords === '') {
|
||||||
|
lastSearch = '';
|
||||||
|
return this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSearch === keywords) return;
|
||||||
|
lastSearch = keywords;
|
||||||
|
|
||||||
|
this.doSearch(keywords.split(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.input.addEventListener('input', eventHandler);
|
||||||
|
this.input.addEventListener('compositionend', eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
this.resultTitle.style.display = 'none';
|
||||||
|
document.querySelectorAll('.archives-group, .archives-group article').forEach(el => el.removeAttribute('style'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindQueryStringChange() {
|
||||||
|
window.addEventListener('popstate', (e) => {
|
||||||
|
this.handleQueryString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleQueryString() {
|
||||||
|
const pageURL = new URL(window.location.toString());
|
||||||
|
const keywords = pageURL.searchParams.get('keyword');
|
||||||
|
this.input.value = keywords;
|
||||||
|
|
||||||
|
if (keywords) {
|
||||||
|
this.doSearch(keywords.split(' '));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static updateQueryString(keywords: string, replaceState = false) {
|
||||||
|
const pageURL = new URL(window.location.toString());
|
||||||
|
|
||||||
|
if (keywords === '') {
|
||||||
|
pageURL.searchParams.delete('keyword')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pageURL.searchParams.set('keyword', keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceState) {
|
||||||
|
window.history.replaceState('', '', pageURL.toString());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.history.pushState('', '', pageURL.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
searchResultTitleTemplate: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
setTimeout(function () {
|
||||||
|
const searchForm = document.getElementById('search-form') as HTMLFormElement,
|
||||||
|
searchInput = searchForm.querySelector('input') as HTMLInputElement,
|
||||||
|
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
|
||||||
|
|
||||||
|
new Search({
|
||||||
|
form: searchForm,
|
||||||
|
input: searchInput,
|
||||||
|
resultTitle: searchResultTitle,
|
||||||
|
resultTitleTemplate: window.searchResultTitleTemplate
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Search;
|
@ -1,326 +0,0 @@
|
|||||||
interface pageData {
|
|
||||||
title: string,
|
|
||||||
date: string,
|
|
||||||
permalink: string,
|
|
||||||
content: string,
|
|
||||||
image?: string,
|
|
||||||
preview: string,
|
|
||||||
matchCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface match {
|
|
||||||
start: number,
|
|
||||||
end: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape HTML tags as HTML entities
|
|
||||||
* Edited from:
|
|
||||||
* @link https://stackoverflow.com/a/5499821
|
|
||||||
*/
|
|
||||||
const tagsToReplace = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
'…': '…'
|
|
||||||
};
|
|
||||||
|
|
||||||
function replaceTag(tag) {
|
|
||||||
return tagsToReplace[tag] || tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceHTMLEnt(str) {
|
|
||||||
return str.replace(/[&<>"]/g, replaceTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(string) {
|
|
||||||
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
class Search {
|
|
||||||
private data: pageData[];
|
|
||||||
private form: HTMLFormElement;
|
|
||||||
private input: HTMLInputElement;
|
|
||||||
private list: HTMLDivElement;
|
|
||||||
private resultTitle: HTMLHeadElement;
|
|
||||||
private resultTitleTemplate: string;
|
|
||||||
|
|
||||||
constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
|
|
||||||
this.form = form;
|
|
||||||
this.input = input;
|
|
||||||
this.list = list;
|
|
||||||
this.resultTitle = resultTitle;
|
|
||||||
this.resultTitleTemplate = resultTitleTemplate;
|
|
||||||
|
|
||||||
this.handleQueryString();
|
|
||||||
this.bindQueryStringChange();
|
|
||||||
this.bindSearchForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes search matches
|
|
||||||
* @param str original text
|
|
||||||
* @param matches array of matches
|
|
||||||
* @param ellipsis whether to add ellipsis to the end of each match
|
|
||||||
* @param charLimit max length of preview string
|
|
||||||
* @param offset how many characters before and after the match to include in preview
|
|
||||||
* @returns preview string
|
|
||||||
*/
|
|
||||||
private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
|
|
||||||
matches.sort((a, b) => {
|
|
||||||
return a.start - b.start;
|
|
||||||
});
|
|
||||||
|
|
||||||
let i = 0,
|
|
||||||
lastIndex = 0,
|
|
||||||
charCount = 0;
|
|
||||||
|
|
||||||
const resultArray: string[] = [];
|
|
||||||
|
|
||||||
while (i < matches.length) {
|
|
||||||
const item = matches[i];
|
|
||||||
|
|
||||||
/// item.start >= lastIndex (equal only for the first iteration)
|
|
||||||
/// because of the while loop that comes after, iterating over variable j
|
|
||||||
|
|
||||||
if (ellipsis && item.start - offset > lastIndex) {
|
|
||||||
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
|
|
||||||
resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
|
|
||||||
charCount += offset * 2;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/// If the match is too close to the end of last match, don't add ellipsis
|
|
||||||
resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
|
|
||||||
charCount += item.start - lastIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
let j = i + 1,
|
|
||||||
end = item.end;
|
|
||||||
|
|
||||||
/// Include as many matches as possible
|
|
||||||
/// [item.start, end] is the range of the match
|
|
||||||
while (j < matches.length && matches[j].start <= end) {
|
|
||||||
end = Math.max(matches[j].end, end);
|
|
||||||
++j;
|
|
||||||
}
|
|
||||||
|
|
||||||
resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
|
|
||||||
charCount += end - item.start;
|
|
||||||
|
|
||||||
i = j;
|
|
||||||
lastIndex = end;
|
|
||||||
|
|
||||||
if (ellipsis && charCount > charLimit) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the rest of the string
|
|
||||||
if (lastIndex < str.length) {
|
|
||||||
let end = str.length;
|
|
||||||
if (ellipsis) end = Math.min(end, lastIndex + offset);
|
|
||||||
|
|
||||||
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
|
|
||||||
|
|
||||||
if (ellipsis && end != str.length) {
|
|
||||||
resultArray.push(` [...]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultArray.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async searchKeywords(keywords: string[]) {
|
|
||||||
const rawData = await this.getData();
|
|
||||||
const results: pageData[] = [];
|
|
||||||
|
|
||||||
const regex = new RegExp(keywords.filter((v, index, arr) => {
|
|
||||||
arr[index] = escapeRegExp(v);
|
|
||||||
return v.trim() !== '';
|
|
||||||
}).join('|'), 'gi');
|
|
||||||
|
|
||||||
for (const item of rawData) {
|
|
||||||
const titleMatches: match[] = [],
|
|
||||||
contentMatches: match[] = [];
|
|
||||||
|
|
||||||
let result = {
|
|
||||||
...item,
|
|
||||||
preview: '',
|
|
||||||
matchCount: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentMatchAll = item.content.matchAll(regex);
|
|
||||||
for (const match of Array.from(contentMatchAll)) {
|
|
||||||
contentMatches.push({
|
|
||||||
start: match.index,
|
|
||||||
end: match.index + match[0].length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleMatchAll = item.title.matchAll(regex);
|
|
||||||
for (const match of Array.from(titleMatchAll)) {
|
|
||||||
titleMatches.push({
|
|
||||||
start: match.index,
|
|
||||||
end: match.index + match[0].length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
|
|
||||||
if (contentMatches.length > 0) {
|
|
||||||
result.preview = Search.processMatches(result.content, contentMatches);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/// If there are no matches in the content, use the first 140 characters as preview
|
|
||||||
result.preview = replaceHTMLEnt(result.content.substring(0, 140));
|
|
||||||
}
|
|
||||||
|
|
||||||
result.matchCount = titleMatches.length + contentMatches.length;
|
|
||||||
if (result.matchCount > 0) results.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result with more matches appears first
|
|
||||||
return results.sort((a, b) => {
|
|
||||||
return b.matchCount - a.matchCount;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async doSearch(keywords: string[]) {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
const results = await this.searchKeywords(keywords);
|
|
||||||
this.clear();
|
|
||||||
|
|
||||||
for (const item of results) {
|
|
||||||
this.list.append(Search.render(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateResultTitle(resultLen, time) {
|
|
||||||
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getData() {
|
|
||||||
if (!this.data) {
|
|
||||||
/// Not fetched yet
|
|
||||||
const jsonURL = this.form.dataset.json;
|
|
||||||
this.data = await fetch(jsonURL).then(res => res.json());
|
|
||||||
const parser = new DOMParser();
|
|
||||||
|
|
||||||
for (const item of this.data) {
|
|
||||||
item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bindSearchForm() {
|
|
||||||
let lastSearch = '';
|
|
||||||
|
|
||||||
const eventHandler = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const keywords = this.input.value.trim();
|
|
||||||
|
|
||||||
Search.updateQueryString(keywords, true);
|
|
||||||
|
|
||||||
if (keywords === '') {
|
|
||||||
lastSearch = '';
|
|
||||||
return this.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastSearch === keywords) return;
|
|
||||||
lastSearch = keywords;
|
|
||||||
|
|
||||||
this.doSearch(keywords.split(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.input.addEventListener('input', eventHandler);
|
|
||||||
this.input.addEventListener('compositionend', eventHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clear() {
|
|
||||||
this.list.innerHTML = '';
|
|
||||||
this.resultTitle.innerText = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private bindQueryStringChange() {
|
|
||||||
window.addEventListener('popstate', (e) => {
|
|
||||||
this.handleQueryString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleQueryString() {
|
|
||||||
const pageURL = new URL(window.location.toString());
|
|
||||||
const keywords = pageURL.searchParams.get('keyword');
|
|
||||||
this.input.value = keywords;
|
|
||||||
|
|
||||||
if (keywords) {
|
|
||||||
this.doSearch(keywords.split(' '));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static updateQueryString(keywords: string, replaceState = false) {
|
|
||||||
const pageURL = new URL(window.location.toString());
|
|
||||||
|
|
||||||
if (keywords === '') {
|
|
||||||
pageURL.searchParams.delete('keyword')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
pageURL.searchParams.set('keyword', keywords);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replaceState) {
|
|
||||||
window.history.replaceState('', '', pageURL.toString());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.history.pushState('', '', pageURL.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static render(item: pageData) {
|
|
||||||
return <article>
|
|
||||||
<a href={item.permalink}>
|
|
||||||
<div class="article-details">
|
|
||||||
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
|
|
||||||
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
|
|
||||||
</div>
|
|
||||||
{item.image &&
|
|
||||||
<div class="article-image">
|
|
||||||
<img src={item.image} loading="lazy" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</article>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
searchResultTitleTemplate: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
setTimeout(function () {
|
|
||||||
const searchForm = document.querySelector('.search-form') as HTMLFormElement,
|
|
||||||
searchInput = searchForm.querySelector('input') as HTMLInputElement,
|
|
||||||
searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
|
|
||||||
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
|
|
||||||
|
|
||||||
new Search({
|
|
||||||
form: searchForm,
|
|
||||||
input: searchInput,
|
|
||||||
list: searchResultList,
|
|
||||||
resultTitle: searchResultTitle,
|
|
||||||
resultTitleTemplate: window.searchResultTitleTemplate
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Search;
|
|
@ -3,6 +3,9 @@ title: "Archives"
|
|||||||
date: 2019-05-28
|
date: 2019-05-28
|
||||||
layout: "archives"
|
layout: "archives"
|
||||||
slug: "archives"
|
slug: "archives"
|
||||||
|
outputs:
|
||||||
|
- html
|
||||||
|
- json
|
||||||
menu:
|
menu:
|
||||||
main:
|
main:
|
||||||
weight: -70
|
weight: -70
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Search"
|
|
||||||
slug: "search"
|
|
||||||
layout: "search"
|
|
||||||
outputs:
|
|
||||||
- html
|
|
||||||
- json
|
|
||||||
menu:
|
|
||||||
main:
|
|
||||||
weight: -60
|
|
||||||
params:
|
|
||||||
icon: search
|
|
||||||
---
|
|
@ -1,9 +1,14 @@
|
|||||||
{{ define "body-class" }}template-archives{{ end }}
|
{{ define "body-class" }}template-archives template-search{{ end }}
|
||||||
|
{{ define "head" }}
|
||||||
|
{{- with .OutputFormats.Get "json" -}}
|
||||||
|
<link rel="preload" href="{{ .RelPermalink }}" as="fetch" crossorigin="anonymous">
|
||||||
|
{{- end -}}
|
||||||
|
{{ end }}
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
|
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
|
||||||
|
{{- $terms := $taxonomy.Pages -}}
|
||||||
|
{{ if $terms }}
|
||||||
<header>
|
<header>
|
||||||
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
|
|
||||||
{{- $terms := $taxonomy.Pages -}}
|
|
||||||
{{ if $terms }}
|
|
||||||
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
|
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
|
||||||
<div class="subsection-list">
|
<div class="subsection-list">
|
||||||
<div class="article-list--tile">
|
<div class="article-list--tile">
|
||||||
@ -12,8 +17,21 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
|
||||||
</header>
|
</header>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<form id="search-form" action="{{ .RelPermalink }}" class="search-form"{{ with .OutputFormats.Get "json" -}} data-json="{{ .RelPermalink }}"{{- end }}>
|
||||||
|
<p>
|
||||||
|
<label>{{ T "search.title" }}</label>
|
||||||
|
<input name="keyword" placeholder="{{ T `search.placeholder` }}" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button title="{{ T `search.title` }}">
|
||||||
|
{{ partial "helper/icon" "search" }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3 class="search-result--title section-title"></h3>
|
||||||
|
|
||||||
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
||||||
|
|
||||||
@ -30,4 +48,16 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ partialCached "footer/footer" . }}
|
{{ partialCached "footer/footer" . }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{- $opts := dict "minify" hugo.IsProduction -}}
|
||||||
|
{{- $archivesScript := resources.Get "ts/archives.ts" | js.Build $opts -}}
|
||||||
|
<script type="text/javascript" src="{{ $archivesScript.RelPermalink }}" defer></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "right-sidebar" }}
|
||||||
|
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
|
||||||
{{ end }}
|
{{ end }}
|
13
layouts/_default/archives.json
Normal file
13
layouts/_default/archives.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
|
||||||
|
{{- $result := slice -}}
|
||||||
|
|
||||||
|
{{- range $pages -}}
|
||||||
|
{{- $data := dict
|
||||||
|
"title" .Title
|
||||||
|
"content" (.Plain)
|
||||||
|
"id" .File.UniqueID
|
||||||
|
-}}
|
||||||
|
{{- $result = $result | append $data -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{ jsonify $result }}
|
@ -1,33 +0,0 @@
|
|||||||
{{ define "body-class" }}template-search{{ end }}
|
|
||||||
{{ define "head" }}
|
|
||||||
{{- with .OutputFormats.Get "json" -}}
|
|
||||||
<link rel="preload" href="{{ .RelPermalink }}" as="fetch" crossorigin="anonymous">
|
|
||||||
{{- end -}}
|
|
||||||
{{ end }}
|
|
||||||
{{ define "main" }}
|
|
||||||
<form action="{{ .RelPermalink }}" class="search-form"{{ with .OutputFormats.Get "json" -}} data-json="{{ .RelPermalink }}"{{- end }}>
|
|
||||||
<p>
|
|
||||||
<label>{{ T "search.title" }}</label>
|
|
||||||
<input name="keyword" placeholder="{{ T `search.placeholder` }}" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button title="{{ T `search.title` }}">
|
|
||||||
{{ partial "helper/icon" "search" }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="search-result">
|
|
||||||
<h3 class="search-result--title section-title"></h3>
|
|
||||||
<div class="search-result--list article-list--compact"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}}
|
|
||||||
{{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}}
|
|
||||||
<script type="text/javascript" src="{{ $searchScript.RelPermalink }}" defer></script>
|
|
||||||
|
|
||||||
{{ partialCached "footer/footer" . }}
|
|
||||||
{{ end }}
|
|
@ -1,23 +0,0 @@
|
|||||||
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
|
|
||||||
{{- $result := slice -}}
|
|
||||||
|
|
||||||
{{- range $pages -}}
|
|
||||||
{{- $data := dict "title" .Title "date" .Date "permalink" .Permalink "content" (.Plain) -}}
|
|
||||||
|
|
||||||
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}}
|
|
||||||
{{- if $image.exists -}}
|
|
||||||
{{- $imagePermalink := "" -}}
|
|
||||||
{{- if and $image.resource .Page.Site.Params.imageProcessing.cover.enabled -}}
|
|
||||||
{{- $thumbnail := $image.resource.Fill "120x120" -}}
|
|
||||||
{{- $imagePermalink = (absURL $thumbnail.Permalink) -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- $imagePermalink = $image.permalink -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- $data = merge $data (dict "image" (absURL $imagePermalink)) -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- $result = $result | append $data -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{ jsonify $result }}
|
|
@ -1,4 +1,4 @@
|
|||||||
<article>
|
<article id="{{ with .File }}{{ .UniqueID }}{{ end }}">
|
||||||
<a href="{{ .RelPermalink }}">
|
<a href="{{ .RelPermalink }}">
|
||||||
<div class="article-details">
|
<div class="article-details">
|
||||||
<h2 class="article-title">
|
<h2 class="article-title">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}}
|
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}}
|
||||||
{{- if $query -}}
|
{{- if $query -}}
|
||||||
{{- $searchPage := index $query 0 -}}
|
{{- $searchPage := index $query 0 -}}
|
||||||
<form action="{{ $searchPage.RelPermalink }}" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>
|
<form action="{{ $searchPage.RelPermalink }}" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>
|
||||||
@ -12,5 +12,5 @@
|
|||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{- warnf "Search page not found. Create a page with layout: search." -}}
|
{{- warnf "Archives page not found. Create a page with layout: archives." -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
Loading…
Reference in New Issue
Block a user