summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/js/main.js113
-rw-r--r--hugo.toml25
-rw-r--r--i18n/de.toml8
-rw-r--r--i18n/en.toml8
-rw-r--r--layouts/_default/list.html28
-rw-r--r--layouts/_default/search.html69
-rw-r--r--layouts/index.json14
-rw-r--r--layouts/partials/footer.html42
-rw-r--r--layouts/partials/header.html4
-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();
})();
diff --git a/hugo.toml b/hugo.toml
index 8120249..7f7cea2 100644
--- a/hugo.toml
+++ b/hugo.toml
@@ -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">&times;</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>&copy; {{ 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