Merge pull request #19 from CaiJimmy/template-search
feat: local search
This commit is contained in:
commit
148f0ff8fe
7
assets/icons/search.svg
Normal file
7
assets/icons/search.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z"/>
|
||||||
|
<circle cx="10" cy="10" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="15" y2="15" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 355 B |
@ -199,6 +199,13 @@
|
|||||||
.article-time {
|
.article-time {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-preview{
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: var(--card-text-color-tertiary);
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
82
assets/scss/partials/layout/search.scss
Normal file
82
assets/scss/partials/layout/search.scss
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
.search-form {
|
||||||
|
margin-bottom: var(--section-separation);
|
||||||
|
position: relative;
|
||||||
|
--button-size: 80px;
|
||||||
|
|
||||||
|
&.widget {
|
||||||
|
--button-size: 60px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 30px 20px 15px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 20px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: var(--card-text-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
border-radius: var(--card-border-radius);
|
||||||
|
background-color: var(--card-background);
|
||||||
|
box-shadow: var(--shadow-l1);
|
||||||
|
color: var(--card-text-color-main);
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
|
||||||
|
font-size: 1.8rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: var(--shadow-l2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--button-size);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke-width: 2;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--card-text-color-secondary);
|
||||||
|
stroke-width: 1.33;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
@import "partials/layout/article.scss";
|
@import "partials/layout/article.scss";
|
||||||
@import "partials/layout/taxonomy.scss";
|
@import "partials/layout/taxonomy.scss";
|
||||||
@import "partials/layout/404.scss";
|
@import "partials/layout/404.scss";
|
||||||
|
@import "partials/layout/search.scss";
|
||||||
|
|
||||||
@import "custom.scss";
|
@import "custom.scss";
|
||||||
|
|
||||||
|
34
assets/ts/createElement.ts
Normal file
34
assets/ts/createElement.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* createElement
|
||||||
|
* Edited from:
|
||||||
|
* @link https://stackoverflow.com/a/42405694
|
||||||
|
*/
|
||||||
|
function createElement(tag, attrs, children) {
|
||||||
|
var element = document.createElement(tag);
|
||||||
|
|
||||||
|
for (let name in attrs) {
|
||||||
|
if (name && attrs.hasOwnProperty(name)) {
|
||||||
|
let value = attrs[name];
|
||||||
|
|
||||||
|
if (name == "dangerouslySetInnerHTML") {
|
||||||
|
element.innerHTML = value.__html;
|
||||||
|
}
|
||||||
|
else if (value === true) {
|
||||||
|
element.setAttribute(name, name);
|
||||||
|
} else if (value !== false && value != null) {
|
||||||
|
element.setAttribute(name, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 2; i < arguments.length; i++) {
|
||||||
|
let child = arguments[i];
|
||||||
|
if (child) {
|
||||||
|
element.appendChild(
|
||||||
|
child.nodeType == null ?
|
||||||
|
document.createTextNode(child.toString()) : child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createElement;
|
@ -9,6 +9,7 @@
|
|||||||
import { createGallery } from "./gallery"
|
import { createGallery } from "./gallery"
|
||||||
import { getColor } from './color';
|
import { getColor } from './color';
|
||||||
import menu from './menu';
|
import menu from './menu';
|
||||||
|
import createElement from './createElement';
|
||||||
|
|
||||||
let Stack = {
|
let Stack = {
|
||||||
init: () => {
|
init: () => {
|
||||||
@ -74,4 +75,12 @@ window.addEventListener('load', () => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
})
|
})
|
||||||
|
|
||||||
window.Stack = Stack;
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
createElement: any;
|
||||||
|
Stack: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Stack = Stack;
|
||||||
|
window.createElement = createElement;
|
263
assets/ts/search.tsx
Normal file
263
assets/ts/search.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
interface pageData {
|
||||||
|
title: string,
|
||||||
|
date: string,
|
||||||
|
permalink: string,
|
||||||
|
content: string,
|
||||||
|
image?: string,
|
||||||
|
preview: string,
|
||||||
|
matchCount: 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async searchKeywords(keywords: string[]) {
|
||||||
|
const rawData = await this.getData();
|
||||||
|
let results: pageData[] = [];
|
||||||
|
|
||||||
|
/// Sort keywords by their length
|
||||||
|
keywords.sort((a, b) => {
|
||||||
|
return b.length - a.length
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of rawData) {
|
||||||
|
let result = {
|
||||||
|
...item,
|
||||||
|
preview: '',
|
||||||
|
matchCount: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let matched = false;
|
||||||
|
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (keyword === '') continue;
|
||||||
|
|
||||||
|
const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi');
|
||||||
|
|
||||||
|
const contentMatch = regex.exec(result.content);
|
||||||
|
regex.lastIndex = 0; /// Reset regex
|
||||||
|
|
||||||
|
const titleMatch = regex.exec(result.title);
|
||||||
|
regex.lastIndex = 0; /// Reset regex
|
||||||
|
|
||||||
|
if (titleMatch) {
|
||||||
|
result.title = result.title.replace(regex, Search.marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleMatch || contentMatch) {
|
||||||
|
matched = true;
|
||||||
|
++result.matchCount;
|
||||||
|
|
||||||
|
let start = 0,
|
||||||
|
end = 100;
|
||||||
|
|
||||||
|
if (contentMatch) {
|
||||||
|
start = contentMatch.index - 20;
|
||||||
|
end = contentMatch.index + 80
|
||||||
|
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.preview.indexOf(keyword) !== -1) {
|
||||||
|
result.preview = result.preview.replace(regex, Search.marker);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (start !== 0) result.preview += `[...] `;
|
||||||
|
result.preview += `${result.content.slice(start, end).replace(regex, Search.marker)} `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
result.preview += '[...]';
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result with more matches appears first */
|
||||||
|
return results.sort((a, b) => {
|
||||||
|
return b.matchCount - a.matchCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static marker(match) {
|
||||||
|
return '<mark>' + match + '</mark>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindSearchForm() {
|
||||||
|
let lastSearch = '';
|
||||||
|
|
||||||
|
const eventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const keywords = this.input.value;
|
||||||
|
|
||||||
|
Search.updateQueryString(keywords, true);
|
||||||
|
|
||||||
|
if (keywords === '') {
|
||||||
|
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>
|
||||||
|
<secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion>
|
||||||
|
</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;
|
@ -40,7 +40,7 @@ DefaultContentLanguage = "en" # Theme i18n support
|
|||||||
theme = "preferred-color-scheme"
|
theme = "preferred-color-scheme"
|
||||||
|
|
||||||
[params.widgets]
|
[params.widgets]
|
||||||
enabled = ['archives', 'tag-cloud']
|
enabled = ['search', 'archives', 'tag-cloud']
|
||||||
[params.widgets.archives]
|
[params.widgets.archives]
|
||||||
limit = 5
|
limit = 5
|
||||||
### Archives page relative URL
|
### Archives page relative URL
|
||||||
@ -78,6 +78,12 @@ DefaultContentLanguage = "en" # Theme i18n support
|
|||||||
url = "archives"
|
url = "archives"
|
||||||
weight = -70
|
weight = -70
|
||||||
pre = "archives"
|
pre = "archives"
|
||||||
|
[[menu.main]]
|
||||||
|
identifier = "search"
|
||||||
|
name = "Search"
|
||||||
|
url = "search"
|
||||||
|
weight = -60
|
||||||
|
pre = "search"
|
||||||
|
|
||||||
[related]
|
[related]
|
||||||
includeNewer = true
|
includeNewer = true
|
||||||
|
8
exampleSite/content/page/search.md
Normal file
8
exampleSite/content/page/search.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Search"
|
||||||
|
slug: "search"
|
||||||
|
layout: "search"
|
||||||
|
outputs:
|
||||||
|
- html
|
||||||
|
- json
|
||||||
|
---
|
11
i18n/en.toml
11
i18n/en.toml
@ -20,4 +20,13 @@
|
|||||||
other = "Not Found"
|
other = "Not Found"
|
||||||
|
|
||||||
[notFoundSubtitle]
|
[notFoundSubtitle]
|
||||||
other = "This page does not exist."
|
other = "This page does not exist."
|
||||||
|
|
||||||
|
[searchTitle]
|
||||||
|
other = "Search"
|
||||||
|
|
||||||
|
[searchPlaceholder]
|
||||||
|
other = "Type something..."
|
||||||
|
|
||||||
|
[searchResultTitle]
|
||||||
|
other = "#PAGES_COUNT pages (#TIME_SECONDS seconds)"
|
@ -20,4 +20,13 @@
|
|||||||
other = "404 错误"
|
other = "404 错误"
|
||||||
|
|
||||||
[notFoundSubtitle]
|
[notFoundSubtitle]
|
||||||
other = "页面不存在"
|
other = "页面不存在"
|
||||||
|
|
||||||
|
[searchTitle]
|
||||||
|
other = "搜索"
|
||||||
|
|
||||||
|
[searchPlaceholder]
|
||||||
|
other = "输入关键词..."
|
||||||
|
|
||||||
|
[searchResultTitle]
|
||||||
|
other = "#PAGES_COUNT 个结果 (用时 #TIME_SECONDS 秒)"
|
@ -1,6 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ .Site.LanguageCode }}">
|
<html lang="{{ .Site.LanguageCode }}">
|
||||||
{{- partial "head/head.html" . -}}
|
<head>
|
||||||
|
{{- partial "head/head.html" . -}}
|
||||||
|
{{- block "head" . -}}{{ end }}
|
||||||
|
</head>
|
||||||
<body class="{{ block `body-class` . }}{{ end }}">
|
<body class="{{ block `body-class` . }}{{ end }}">
|
||||||
<div class="container flex on-phone--column align-items--flex-start {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }} {{ block `container-class` . }}{{end}}">
|
<div class="container flex on-phone--column align-items--flex-start {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }} {{ block `container-class` . }}{{end}}">
|
||||||
{{ partial "sidebar/left.html" . }}
|
{{ partial "sidebar/left.html" . }}
|
||||||
|
31
layouts/page/search.html
Normal file
31
layouts/page/search.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{ define "body-class" }}template-search{{ end }}
|
||||||
|
{{ define "head" }}
|
||||||
|
{{- with .OutputFormats.Get "json" -}}
|
||||||
|
<link rel="preload" href="{{ .Permalink }}" as="fetch" crossorigin="anonymous">
|
||||||
|
{{- end -}}
|
||||||
|
{{ end }}
|
||||||
|
{{ define "main" }}
|
||||||
|
<form action="{{ .Permalink }}" class="search-form"{{ with .OutputFormats.Get "json" -}} data-json="{{ .Permalink }}"{{- end }}>
|
||||||
|
<p>
|
||||||
|
<label>{{ T "searchTitle" }}</label>
|
||||||
|
<input name="keyword" placeholder="{{ T `searchPlaceholder` }}" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button title="Search">
|
||||||
|
{{ partial "helper/icon" "search" }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3 class="search-result--title section-title"></h3>
|
||||||
|
<div class="search-result--list article-list--compact"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.searchResultTitleTemplate = "{{ T `searchResultTitle` }}"
|
||||||
|
</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 }}
|
20
layouts/page/search.json
Normal file
20
layouts/page/search.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
|
||||||
|
{{- $notHidden := where .Site.RegularPages "Params.hidden" "!=" true -}}
|
||||||
|
{{- $filtered := ($pages | intersect $notHidden) -}}
|
||||||
|
|
||||||
|
{{- $result := slice -}}
|
||||||
|
|
||||||
|
{{- range $filtered -}}
|
||||||
|
{{- $data := dict "title" .Title "date" .Date "permalink" .Permalink "content" (.Plain) -}}
|
||||||
|
|
||||||
|
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}}
|
||||||
|
{{- if and $image.exists $image.resource -}}
|
||||||
|
{{- $thumbnail := $image.resource.Fill "120x120" -}}
|
||||||
|
{{- $image := dict "image" (absURL $thumbnail.Permalink) -}}
|
||||||
|
{{- $data = merge $data $image -}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{- $result = $result | append $data -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{ jsonify $result }}
|
@ -1,22 +1,20 @@
|
|||||||
<head>
|
<meta charset='utf-8'>
|
||||||
<meta charset='utf-8'>
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
||||||
|
|
||||||
{{- $description := partialCached "data/description" . .RelPermalink -}}
|
|
||||||
<meta name='description' content='{{ $description }}'>
|
|
||||||
|
|
||||||
{{- $title := partialCached "data/title" . .RelPermalink -}}
|
{{- $description := partialCached "data/description" . .RelPermalink -}}
|
||||||
<title>{{ $title }}</title>
|
<meta name='description' content='{{ $description }}'>
|
||||||
|
|
||||||
<link rel='canonical' href='{{ .Permalink }}'>
|
|
||||||
|
|
||||||
{{- partial "head/style.html" . -}}
|
|
||||||
{{- partial "head/script.html" . -}}
|
|
||||||
{{- partial "head/opengraph/include.html" . -}}
|
|
||||||
|
|
||||||
{{- range .AlternativeOutputFormats -}}
|
{{- $title := partialCached "data/title" . .RelPermalink -}}
|
||||||
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
|
<title>{{ $title }}</title>
|
||||||
{{- end -}}
|
|
||||||
|
<link rel='canonical' href='{{ .Permalink }}'>
|
||||||
{{- partial "head/custom.html" . -}}
|
|
||||||
</head>
|
{{- partial "head/style.html" . -}}
|
||||||
|
{{- partial "head/script.html" . -}}
|
||||||
|
{{- partial "head/opengraph/include.html" . -}}
|
||||||
|
|
||||||
|
{{- range .AlternativeOutputFormats -}}
|
||||||
|
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- partial "head/custom.html" . -}}
|
10
layouts/partials/widget/search.html
Normal file
10
layouts/partials/widget/search.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<form action="/search" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>
|
||||||
|
<p>
|
||||||
|
<label>{{ T "searchTitle" }}</label>
|
||||||
|
<input name="keyword" required placeholder="{{ T `searchPlaceholder` }}" />
|
||||||
|
|
||||||
|
<button title="Search">
|
||||||
|
{{ partial "helper/icon" "search" }}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
Loading…
Reference in New Issue
Block a user