GitHub Pages 블로그에 사용자 정의 검색창을 추가하고 싶으셨나요?
이 포스트에서는 minimal-mistakes 테마를 기반으로 Lunr.js를 활용해 상단과 사이드바에 검색창을 구현하고, 제목 및 태그 기준의 검색 기능을 설정하는 방법을 단계별로 안내합니다.

해당 강의는 minimal-mistakes-jekyll 테마를 기준으로 작성되어있습니다.

1. 상단 위에 검색 창 만들기

_config.yml 수정

defaults:
  - scope:
      path: ""
      type: pages
    values:
      search: true
      author_profile: true
      sidebar:
        nav: "main"

search: true 코드를 추가해줍니다.

_config.yml 속성 수정

search: 해당 속성에 추가해줍니다: search: true

search_full_content: 해당 속성에 추가해 줍니다: search_full_content: true

search_provider: 해당 속성에 추가해 줍니다: search_provider: lunr

lunr: 해당 속성 하위 속성인 search_within_pages: 해당 속성에 추가해 줍니다: search_within_pages: true

blog-search-image.png

속성 수정 후 local server로 확인한 스크린샷 이미지입니다.

오른쪽 상단에 Category 옆에 search 아이콘이 생성된 것을 확인할 수 있습니다.

해당 search 아이콘의 코드 위치는 \_includes\masthead.html 에 25번째 줄에 있습니다.

{% if site.search == true %}
<button class="search__toggle" type="button">
  <span class="visually-hidden"
    >{{ site.data.ui-text[site.locale].search_label | default: "Toggle search"
    }}</span
  >
  <i class="fas fa-search"></i>
</button>
{% endif %}

해당 코드를 지우면 오른쪽 상단에 search 아이콘이 지워지는 것을 확인할 수 있습니다.

2. 사이드 바 카테고리 밑에 검색 창 추가하기

해당 코드를 \_include\sidebar.html 에 넣어 줍니다.

<div class="sidebar__search">
  <form id="sidebar-search" onsubmit="return SidebarSearchHandler();">
    <div class="search-block">
      <select id="search-type">
        <option value="title">Title</option>
        <option value="tag">Tag</option>
        <option value="both">Title + Tag</option>
      </select>
    </div>
    <div class="search-block">
      <input type="text" id="search-query" placeholder="Search input" />
    </div>
    <div class="search-block">
      <button type="submit"><i class="fa fa-search"></i> Search</button>
    </div>
  </form>
</div>

해당 코드를 넣을 때, <nav class="nav__list"></nav> 해당 nav 태그 안에 넣어 주도록 합시다.

<form id="sidebar__search" onsubmit="return SidebarSearchHandler();"> 해당 부분에 SidebarSearchHandler() 함수는 2-2. 에서 확인 하실 수 있습니다.

2-1. 사이드 바 UI 수정 (.scss)

\_sass\minimal-misktakes\_sidebar.scss 에 추가

.sidebar__search {
  font-family: $sans-serif;
  font-size: $type-size-6;
  margin-top: 1em;
  vertical-align: top;

  @include breakpoint($x-large) {
    width: initial;
    margin-inline-end: initial;
  }

  .search-block {
    margin-bottom: 0.6em;

    select,
    input,
    button {
      width: 100%;
      max-width: 100%;
      display: block;
      height: 2.2em;
      font-size: 0.9em;
      font-family: $inherit;
      box-sizing: border-box;
      border-radius: $border-radius;
      border: 1px solid $border-color;
    }

    select {
      appearance: none;
      padding: 0 0.6em;
      background-color: $background-color;
      color: inherit;
    }

    input {
      padding: 0 0.6em;
      background-color: $background-color;
      color: inherit;
    }

    button {
      background-color: $primary-color;
      color: $base07;
      border: none;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 0.4em;

      i {
        font-size: 0.9em;
      }

      &:hover {
        background-color: mix($primary-color, black, 80%);
      }
    }
  }
}

해당 스타일까지 구현했다면, 로컬 서버로 테스트 할 때, UI가 보입니다. 작동은 아직은 안하지요.

2-2. SidebarSearchHandler() 함수 구현하기

\assets\js\ 해당 폴더에 sidebar-search.js 파일을 새로 만들기 해줍니다.

\assets\js\sidebar-search.js 코드

function SidebarSearchHandler() {
  const query = document.getElementById("search-query").value;
  const type = document.getElementById("search-type").value;

  if (!query.trim()) return false;

  let finalQuery = "";
  if (type === "title") finalQuery = `title:${query}`;
  else if (type === "tag") finalQuery = `tags:${query}`;
  else finalQuery = query;

  window.location.href = `/_search/?q=${encodeURIComponent(finalQuery)}`;
  return false;
}

2-3. search page 추가하기

\_pages\ 경로에 search.md 새로 파일을 만들기 해줍니다.

search.md 코드

---
title: "search result"
layout: search
permalink: /_search/
---

image.png

serach.md를 추가하고 로컬 서버에서 http://localhost:4000/_search/ 해당 홈페이지가 만들어집니다.

2-4. serach.js 구현하기

assets\js\lunr\ 경로에 search.js 파일을 새로 만들기 합니다.

assets\js\lunr\search.js 코드

(function () {
const params = new URLSearchParams(window.location.search);
const query = params.get("q");

const searchInput = document.getElementById("search");
const resultsContainer = document.getElementById("results");

if (!searchInput || !resultsContainer) return;
if (typeof window.store === "undefined") return;

function renderResults(results, query) {
resultsContainer.innerHTML = "";

    if (results.length === 0) {
      resultsContainer.innerHTML = `<p><em>"${query}"</em>에 대한 결과가 없습니다.</p>`;
      return;
    }

    const html = results
      .map(
        (post) => `
      <div class="list__item">
        <article class="archive__item" itemscope itemtype="https://schema.org/CreativeWork">
          <h2 class="archive__item-title" itemprop="headline">
            <a href="${post.url}" rel="bookmark">${post.title}</a>
          </h2>
          <div class="archive__item-excerpt" itemprop="description">
            ${post.excerpt?.slice(0, 150) ?? ""}...
          </div>
        </article>
      </div>
    `
      )
      .join("");

    resultsContainer.innerHTML = html;

}

function performSearch(query) {
const results = [];
const lower = query.toLowerCase();

    for (const post of window.store) {
      const title = post.title?.toLowerCase() ?? "";
      const excerpt = post.excerpt?.toLowerCase() ?? "";
      const tags = post.tags?.join(" ").toLowerCase() ?? "";
      const categories = post.categories?.join(" ").toLowerCase() ?? "";

      let match = false;

      if (lower.startsWith("title:")) {
        const keyword = lower.replace("title:", "").trim();
        match = title.includes(keyword);
      } else if (lower.startsWith("tags:")) {
        const keyword = lower.replace("tags:", "").trim();
        match = tags.includes(keyword);
      } else {
        match =
          title.includes(lower) ||
          excerpt.includes(lower) ||
          tags.includes(lower) ||
          categories.includes(lower);
      }

      if (match) {
        results.push(post);
      }
    }

    renderResults(results, query);

}

if (query) {
searchInput.value = query;
performSearch(query);
}

searchInput.addEventListener("input", function (e) {
performSearch(e.target.value);
});
})();

동작 요약

  1. 검색창에 입력된 값을 실시간으로 감지
  2. 검색어(q)가 URL에 있을 경우 자동으로 검색 수행
  3. 정적 데이터(window.store)를 기준으로 title, excerpt, tags, categories에서 키워드 검색
  4. 검색 결과를 실시간으로 DOM에 렌더링

2-5. serach.js

\_layouts\default.html 코드 추가:

<!-- lunr search script -->
<script src="/assets/js/sidebar-search.js"></script>
<script src="/assets/js/lunr/lunr.min.js"></script>
<script src="/assets/js/lunr/lunr-store.js"></script>
<script src="/assets/js/lunr/search.js"></script>

\_layouts\default.html 전체 코드:

---
---
<!doctype html>
{% include copyright.html %}
<html lang="{{ site.locale | replace: "_", "-" | default: "en" }}" class="no-js">
  <head>
    {% include head.html %}
    {% include head/custom.html %}
  </head>

  <body class="layout--{{ page.layout | default: layout.layout }}{% if page.classes or layout.classes %}{{ page.classes | default: layout.classes | join: ' ' | prepend: ' ' }}{% endif %}" dir="{% if site.rtl %}rtl{% else %}ltr{% endif %}">
    {% include_cached skip-links.html %}
    {% include_cached masthead.html %}

    <div class="initial-content">
      {{ content }}
      {% include after-content.html %}
    </div>

    {% if site.search == true %}
      <div class="search-content">
        {% include_cached search/search_form.html %}
      </div>
    {% endif %}

    <div id="footer" class="page__footer">
      <footer>
        {% include footer/custom.html %}
        {% include_cached footer.html %}
      </footer>
    </div>

    <!-- JEKYLL LIQUID TAG -->
    {% include custom_code-block_custom.html %}
    {%include custom_toggle-script.html %}
    {% include custom_quiz-form.html %}

    <!-- lunr search script -->
    <script src="{{ '/assets/js/sidebar-search.js' | relative_url }}"></script>
    <script src="{{ '/assets/js/lunr/lunr.min.js' | relative_url }}"></script>
    <script src="{{ '/assets/js/lunr/lunr-store.js' | relative_url }}"></script>
    <script src="{{ '/assets/js/lunr/search.js' | relative_url }}"></script>

    {% include scripts.html %}
  </body>
</html>

저는 해당 부분을 custom_script.html 으로 분리 해서 사용했습니다.

custom_script.html 코드:

<!-- JEKYLL LIQUID TAG -->
{% include custom_code-block_custom.html %} {%include custom_toggle-script.html
%} {% include custom_quiz-form.html %}

<!-- lunr search script -->
<script src="{{ '/assets/js/sidebar-search.js' | relative_url }}"></script>
<script src="{{ '/assets/js/lunr/lunr.min.js' | relative_url }}"></script>
<script src="{{ '/assets/js/lunr/lunr-store.js' | relative_url }}"></script>
<script src="{{ '/assets/js/lunr/search.js' | relative_url }}"></script>


\_layouts\default.html 에 추가한 코드:

{% include custom_script.html %}

image1.png

title 을 assembly 으로 검색한 이미지 하지만 처음에 Assembly Category까지 검색되어 나온 모습을 볼 수 있습니다.

\_config.yml 에서 defaults: 속성에서 수정:

- scope:
    path: ""
    type: pages
  values:
    author_profile: true
    search: false
    sidebar:
      nav: "main"

카테고리까지 검색 되는 것을 방지 하기 위해 serach: 속성의 값을 false 으로 설정해주었습니다.

image2.png

다시 검색을 진행 해본 이미지 입니다. 카테고리가 빠져있고 게시글만 검색되는 것을 확인할 수 있었습니다.

Leave a comment