diff options
| -rw-r--r-- | assets/js/main.js | 113 | ||||
| -rw-r--r-- | hugo.toml | 25 | ||||
| -rw-r--r-- | i18n/de.toml | 8 | ||||
| -rw-r--r-- | i18n/en.toml | 8 | ||||
| -rw-r--r-- | layouts/_default/list.html | 28 | ||||
| -rw-r--r-- | layouts/_default/search.html | 69 | ||||
| -rw-r--r-- | layouts/index.json | 14 | ||||
| -rw-r--r-- | layouts/partials/footer.html | 42 | ||||
| -rw-r--r-- | layouts/partials/header.html | 4 | ||||
| -rw-r--r-- | layouts/partials/select-language.html (renamed from layouts/partials/selectLanguage.html) | 0 | ||||
| -rw-r--r-- | layouts/partials/select-theme.html (renamed from layouts/partials/selectTheme.html) | 0 |
11 files changed, 294 insertions, 17 deletions
diff --git a/assets/js/main.js b/assets/js/main.js index c3218f1..08cfcf5 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -33,6 +33,119 @@ }); } + // SEARCH + function initSearch() { + const input = document.getElementById("search-input"); + const resetBtn = document.getElementById("search-reset"); + const resultsCount = document.querySelector(".search-results__count"); + const resultsList = document.querySelector(".search-results__list"); + const template = document.getElementById("search-result-template"); + + // Only initialize search on the search page + if (!input || !template) return; + + let allPosts = []; + let searchTimeout; + + // Get JSON file with data of all posts + fetch(input.dataset.indexUrl ?? "/index.json") + .then((res) => res.json()) + .then((data) => allPosts = data) + .catch((err) => console.error("Search index.json failed to load", err)); + + function clearResults() { + resultsCount.hidden = true; + resultsList.innerHTML = ""; + resultsList.hidden = true; + } + + // Hydrate post-card list item(s) from the template with JSON data + function renderSearchResults(matches) { + clearResults(); + + resultsCount.hidden = false; + resultsCount.querySelector("#search-results-number").textContent = String(matches.length ?? 0); + + // No posts matching query found + if (!matches.length) return; + + matches.forEach((post) => { + const li = template.content.firstElementChild.cloneNode(true); + + const link = li.querySelector(".post-card__link"); + link.href = post.url; + link.tile = post.title; + + const date = li.querySelector(".post-card__date"); + date.textContent = new Date(post.date).toLocaleDateString(); + date.setAttribute("datetime", post.date); + + const summary = li.querySelector(".post-card__summary"); + if (summary) summary.textContent = post.summary; + + const tagsList = li.querySelector(".post-card__tags-list"); + if (tagsList) { + tagsList.innerHTML = ""; + if (post.tags && post.tags.length) { + post.tags.slice(0, 3).forEach((tag) => { + const liTag = document.createElement("li"); + liTag.className = "post-card__tags-item"; + liTag.textContent = `#${tag}`; + tagsList.appendChild(liTag); + }); + + if (post.tags.length > 3) { + const more = document.createElement("li"); + more.className = "post-card__tags-item post-card__tags-more"; + more.textContent = `+${post.tags.length - 3}`; + tagsList.appendChild(more); + } + } + } + + resultsList.appendChild(li); + resultsList.hidden = false; + }); + } + + // Filter all posts for ones matching the user's search query + function searchPosts(query) { + const normalizedQuery = query.trim().toLowerCase(); + + // Only search if user entered at least 3 chars + if (normalizedQuery.length < 3) { + clearResults(); + return; + } + + const matches = allPosts.filter((post) => ( + post.title.toLowerCase().includes(normalizedQuery) || + (post.summary && post.summary.toLowerCase().includes(normalizedQuery)) + )); + + // At least 1 post matching query found + renderSearchResults(matches); + } + + input.addEventListener("input", (event) => { + // Debounce search + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchPosts(event.target.value); + }, 300); + }); + + resetBtn.addEventListener("click", () => { + input.value = ""; + input.focus(); + clearResults(); + }); + + // Focus input on page load - it's what the user is here for + input.focus(); + } + initThemeToggle(); + initSearch(); })(); @@ -33,9 +33,32 @@ enableRobotsTXT = true # Copy this line to your project's hugo.toml pageRef = "/tags" weight = 40 +[outputs] # Copy outputs configuration to your project's hugo.toml + home = ["html", "json", "rss"] + section = ["html", "rss"] + taxonomy = ["html"] + term = ["html"] + [params] - # Set logo to empty string in your project's hugo.toml if you want to display site.Title instead + # In your project's hugo.toml, adjust logo to your file's name or set empty string to display site.Title instead logo = "/logo.svg" + [params.author] name = "Your Name" email = "your@email.com" + + # Replace with your actual social media profiles in your hugo.toml + [[params.socials]] + name = "Twitter" + url = "https://twitter.com/yourhandle" + icon = "/socials/twitter.svg" + + [[params.socials]] + name = "GitHub" + url = "https://github.com/yourhandle" + icon = "/socials/github.svg" + + [[params.socials]] + name = "LinkedIn" + url = "https://linkedin.com/in/yourhandle" + icon = "/socials/linkedin.svg" diff --git a/i18n/de.toml b/i18n/de.toml index bda9260..e675b16 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -28,6 +28,14 @@ recent = "Neueste Beiträge" view_all = "Alle Beiträge" +[search] + description = "Beiträge mit Wortsuche finden." + label = "Alle Beiträge durchsuchen" + matches = "Treffer" + placeholder = "Mindestens drei Buchstaben tippen, um zu suchen..." + reset = "Suche löschen" + title = "Suche" + [tags] all = "Alle Schlagwörter" empty = "Keine Schlagwörter gefunden." diff --git a/i18n/en.toml b/i18n/en.toml index fbf4cb8..4fadaee 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -28,6 +28,14 @@ recent = "Recent Posts" view_all = "View all posts" +[search] + description = "Find posts by search term." + label = "Search all posts" + matches = "matches" + placeholder = "Type at least 3 characters to search..." + reset = "Clear search" + title = "Search" + [tags] all = "All tags" empty = "No tags found." diff --git a/layouts/_default/list.html b/layouts/_default/list.html index 8ff9b18..d3d2d99 100644 --- a/layouts/_default/list.html +++ b/layouts/_default/list.html @@ -1,28 +1,34 @@ {{- define "main" }} -<section class="section-list"> - <header class="section-list__header"> - <h1 class="section-list__headline"> +<section class="list-page"> + <header class="list-page__header"> + <h1 class="list-page__headline"> {{ .Title }} </h1> + {{- with .Content }} + <div class="list-page__description"> + {{ . }} + </div> + {{- else }} {{- $description := or .Description .Summary (lang.Translate "list.default_description" .Title | default (printf "All posts in %s" .Title)) }} - <p class="section-list__description"> - {{ $description }} - </p> + <p class="list-page__description"> + {{ $description }} + </p> + {{- end }} </header> - <section class="section-list__content" aria-label="{{ lang.Translate "posts.name" | default "Posts" }}"> + <section class="list-page__content" aria-label="{{ lang.Translate "posts.name" | default "Posts" }}"> {{- /* 20 posts per site */ -}} {{- $paginator := .Paginate .Pages 20 }} {{- with $paginator.Pages }} - <ul class="section-list__posts"> + <ul class="list-page__posts-list"> {{- range . }} - <li class="section-list__post"> + <li class="list-page__post"> {{- partial "list/post-card.html" . }} </li> {{- end }} </ul> - {{- else }} - <p class="section-list__empty-message"> + {{ else }} + <p class="list-page__empty-message"> {{ lang.Translate "list.empty" | default "No posts found in this section." }} </p> {{- end }} diff --git a/layouts/_default/search.html b/layouts/_default/search.html new file mode 100644 index 0000000..569cc9a --- /dev/null +++ b/layouts/_default/search.html @@ -0,0 +1,69 @@ +{{ define "main" }} +<section class="search-page"> + <header class="search-page__header"> + <h1 class="search-page__heading"> + {{ .Title }} + </h1> + {{- with .Content }} + <div class="search-page__description"> + {{ . }} + </div> + {{- else }} + {{- $description := or .Description (lang.Translate "search.description" | default "Find posts by search term.") }} + <p class="search-page__description"> + {{ $description }} + </p> + {{- end }} + </header> + + <section class="search-page__container" aria-label="{{ lang.Translate "search.label" | default "Search all posts" }}"> + <form id="search-form" class="search-form" role="search" autocomplete="off"> + <label for="search-input" class="search-form__label"> + {{ lang.Translate "search.label" | default "Search all posts" }} + </label> + <div class="search-form__input-wrapper"> + <input + id="search-input" + class="search-form__input" + type="search" + data-index-url="{{ site.Home.RelPermalink }}index.json" + placeholder="{{ lang.Translate "search.placeholder" | default "Type to search..." }}" + /> + <button + id="search-reset" + type="button" + class="search-form__reset" + aria-label="{{ lang.Translate "search.reset" | default "Clear search" }}" + > + <span class="search-clear__icon" aria-hidden="true">×</span> + </button> + </div> + </form> + + </section> + + <div id="search-results" class="search-page__results" aria-live="polite"> + <p class="search-results__count" hidden> + <span id="search-results-number">0</span> + {{ lang.Translate "search.matches" | default "matches" }} + </p> + + <ul class="search-results__list" hidden></ul> + + {{- /* Hidden template, used for hydrating search results from index.json with JS */ -}} + <template id="search-result-template"> + {{- $mockPost := dict + "Title" "TEMPLATE_TITLE" + "RelPermalink" "TEMPLATE_URL" + "Date" now + "Summary" "TEMPLATE_SUMMARY" + "Params" (dict "tags" (slice "TEMPLATE_TAG")) + -}} + <li class="search-results__list-item"> + {{- partial "list/post-card.html" $mockPost }} + </li> + </template> + + </div> +</section> +{{ end }} diff --git a/layouts/index.json b/layouts/index.json new file mode 100644 index 0000000..29d7e14 --- /dev/null +++ b/layouts/index.json @@ -0,0 +1,14 @@ +{{- $allPages := slice -}} + +{{- range (where site.RegularPages "Section" "!=" "") -}} + {{- $currentPage := dict + "title" .Title + "summary" (.Summary | plainify | htmlUnescape) + "url" .RelPermalink + "date" (.Date.Format "2006-01-02T15:04:05Z07:00") + "tags" .Params.tags + -}} + {{- $allPages = $allPages | append $currentPage -}} +{{- end -}} + +{{ jsonify $allPages }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html index 7225d09..9b66758 100644 --- a/layouts/partials/footer.html +++ b/layouts/partials/footer.html @@ -1,7 +1,43 @@ <footer class="site__footer" role="contentinfo"> <p>© {{ now.Year }} {{ site.Params.author.name }}</p> - <a href="{{ site.Home.Permalink }}index.xml" aria-label="RSS Feed" title="RSS Feed"> - <img src="/rss.svg" class="icon icon-rss" style="max-height: 32px" alt="RSS Feed" aria-hidden="true"> - </a> + <nav class="follow-me-links" aria-label="Follow me on:"> + <ul class="follow-me-list"> + {{- range site.Params.socials }} + <li class="follow-me-item"> + <a + href="{{ .url }}" + aria-label="{{ .name }}" + title="{{ .name }}" + rel="me noopener noreferrer" + target="_blank" + > + <img + src="{{ .icon }}" + class="icon icon-{{ lower .name }}" + style="max-height: 32px" + alt="{{ .name }}" + aria-hidden="true" + /> + </a> + </li> + {{- end }} + <li class="follow-me-item"> + <a + href="{{ site.Home.Permalink }}index.xml" + aria-label="RSS Feed" + title="RSS Feed" + target="_blank" + > + <img + src="/rss.svg" + class="icon icon-rss" + style="max-height: 32px" + alt="RSS Feed" + aria-hidden="true" + /> + </a> + </li> + </ul> + </nav> </footer> diff --git a/layouts/partials/header.html b/layouts/partials/header.html index 9195345..0f3cfce 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -7,6 +7,6 @@ {{- end }} </a> - {{- partial "selectTheme.html" . }} - {{- partial "selectLanguage.html" . }} + {{- partial "select-theme.html" . }} + {{- partial "select-language.html" . }} </header> diff --git a/layouts/partials/selectLanguage.html b/layouts/partials/select-language.html index 752b605..752b605 100644 --- a/layouts/partials/selectLanguage.html +++ b/layouts/partials/select-language.html diff --git a/layouts/partials/selectTheme.html b/layouts/partials/select-theme.html index e9114cc..e9114cc 100644 --- a/layouts/partials/selectTheme.html +++ b/layouts/partials/select-theme.html |
