Skip to content

Instantly share code, notes, and snippets.

@cmod
Forked from eddiewebb/readme.md
Last active February 22, 2025 21:10
Show Gist options
  • Save cmod/5410eae147e4318164258742dd053993 to your computer and use it in GitHub Desktop.
Save cmod/5410eae147e4318164258742dd053993 to your computer and use it in GitHub Desktop.
Fast, instant client side search for Hugo static site generator

Super fast, keyboard-optimized, client side Hugo search

This is a fork of and builds upon the work of Eddie Webb's search and Matthew Daly's search explorations.

It's built for the Hugo static site generator, but could be adopted to function with any json index.

To see it in action, go to craigmod.com and press CMD-/ and start typing.

Fast Search



Why client side, why fast?

I believe Fast Software is the Best Software and wanted keyboard-based, super fast search for my homepage / online collection of essays. This method was highly inspired by Sublime Text's CMD-P/CMD-shift-P method of opening files / using functions.

The core precepts of this exploration were:

  • Minimal / zero external dependencies (no jQuery)
  • Smallest possible size added to each page
  • json index only delivered when needed (further minimizing overall impact on page speed / user experience)
  • Keyboard friendly, instant navigation (ala Alfred / macOS Spotlight)

As Eddie Webb points out, this method has the additional benefits of:

  • No NPM, grunt, etc
  • No additional build time steps, just hugo as you would normally
  • Easy to swap out choice of client side search tools, anything that can use a json index

Example: craigmod.com

This search is live on my site, craigmod.com on every page. How to use:

  • Press CMD-/ to invoke search
  • start typing
  • use tab / arrow keys to select result
  • press enter to navigate to that page

Setup

  1. Add index.json file to layouts/_default
  2. Add JSON as additional output format in config.toml
  3. Add search.js to static/js
  4. Add searchbox html to bottom of layouts/_default/baseof.html
  5. Add css styles to your site's main css file or top of baseof.html
  6. hugo
  7. Visit localhost:1313/
  8. Press CMD-/ to invoke search

You can check the json index by visiting localhost:1313/index.json

Files

layouts/_default/baseof.html addition

Add this block to the bottom of your baseof.html file, just before the footer

 <div id="fastSearch">
   <input id="searchInput" tabindex="0">
   <ul id="searchResults">
   </ul>
 </div>
 <script src="/js/fastsearch.js"></script>

static/js/fastsearch.js

This "makes" the search engine based on the index.json file, and wires up all keyboard handling

/*  
====================================================================

FAST SEARCH — 
https://gist.github.com/cmod/5410eae147e4318164258742dd053993
Updated to work with fuse 7 (Jan 2025)
Updated Feb 2025 — no more fuse dependency, more modern js, 
   proper config items, ability to easily modify shortcut, 
   general speed improvements

==================================================================== 
*/
// Configuration
const DEFAULT_CONFIG = {
  shortcuts: {
    open: {                    // Shortcut to open/close search
      key: '/',                // The key to trigger the shortcut
      metaKey: true,          // Requires Cmd/Win key
      altKey: false,          // Requires Alt key
      ctrlKey: false,         // Requires Ctrl key
      shiftKey: false         // Requires Shift key
    }
  },
  search: {
    minChars: 2,              // Minimum characters before searching
    maxResults: 5,            // Maximum number of results to show
    fields: {                 // Fields to search through
      title: true,            // Allow searching in title
      description: true,      // Allow searching in description
      section: true           // Allow searching in section
    }
  }
};

// Function to initialize search with custom config
function initSearch(userConfig = {}) {
  // Deep merge of default config with user config
  const CONFIG = mergeConfigs(DEFAULT_CONFIG, userConfig);
  
  // Cache DOM elements
  const fastSearch = document.getElementById('fastSearch');
  const searchInput = document.getElementById('searchInput');
  const searchResults = document.getElementById('searchResults');

  let searchIndex = null;
  let searchVisible = false;
  let resultsAvailable = false;
  let firstRun = true;

  // Load the search index
  async function loadSearchIndex() {
    try {
      const response = await fetch('/index.json');
      if (!response.ok) throw new Error('Failed to load search index');
      const data = await response.json();
      
      searchIndex = data.map(item => ({
        ...item,
        searchableTitle: item.title?.toLowerCase() || '',
        searchableDesc: item.desc?.toLowerCase() || '',
        searchableSection: item.section?.toLowerCase() || ''
      }));
      
      if (searchInput.value) {
        performSearch(searchInput.value);
      }
    } catch (error) {
      console.error('Error loading search index:', error);
      searchResults.innerHTML = '<li class="search-message">Error loading search index...</li>';
    }
  }

  // Simple fuzzy match for single words
  function simpleFuzzyMatch(text, term) {
    if (text.includes(term)) return true;
    if (term.length < 3) return false;

    let matches = 0;
    let lastMatchIndex = -1;

    for (let i = 0; i < term.length; i++) {
      const found = text.indexOf(term[i], lastMatchIndex + 1);
      if (found > -1) {
        matches++;
        lastMatchIndex = found;
      }
    }

    return matches === term.length;
  }

  // Check if keyboard event matches shortcut config
  function matchesShortcut(event, shortcutConfig) {
    return event.key === shortcutConfig.key &&
           event.metaKey === shortcutConfig.metaKey &&
           event.altKey === shortcutConfig.altKey &&
           event.ctrlKey === shortcutConfig.ctrlKey &&
           event.shiftKey === shortcutConfig.shiftKey;
  }

  // Keyboard shortcuts
  document.addEventListener('keydown', (event) => {
    // Check for configured search shortcut
    if (matchesShortcut(event, CONFIG.shortcuts.open)) {
      event.preventDefault();
      searchVisible = !searchVisible;
      fastSearch.style.visibility = searchVisible ? 'visible' : 'hidden';
      
      if (searchVisible) {
        if (firstRun) {
          loadSearchIndex();
          firstRun = false;
        }
        searchInput.focus();
        searchInput.value = '';
      } else {
        searchInput.blur();
        searchResults.innerHTML = '';
      }
    }

    // ESC to close search
    if (event.key === 'Escape' && searchVisible) {
      fastSearch.style.visibility = 'hidden';
      searchInput.blur();
      searchInput.value = '';
      searchResults.innerHTML = '';
      searchVisible = false;
    }
    
    // Enter to select first result
    if (event.key === 'Enter' && searchVisible) {
      const firstResult = searchResults.querySelector('a');
      if (firstResult) {
        event.preventDefault();
        window.location.href = firstResult.href;
      }
    }

    // Arrow navigation
    if (searchVisible && resultsAvailable) {
      const links = Array.from(searchResults.getElementsByTagName('a'));
      if (!links.length) return;

      const first = links[0];
      const last = links[links.length - 1];
      const active = document.activeElement;
      
      if (event.key === 'ArrowDown') {
        event.preventDefault();
        if (active === searchInput) {
          first.focus();
        } else if (active.tagName === 'A') {
          const currentIndex = links.indexOf(active);
          if (currentIndex !== -1 && currentIndex < links.length - 1) {
            links[currentIndex + 1].focus();
          }
        }
      }
      
      if (event.key === 'ArrowUp') {
        event.preventDefault();
        if (active === first) {
          searchInput.focus();
        } else if (active.tagName === 'A') {
          const currentIndex = links.indexOf(active);
          if (currentIndex > 0) {
            links[currentIndex - 1].focus();
          } else {
            searchInput.focus();
          }
        }
      }
    }
  });

  function performSearch(term) {
    term = term.toLowerCase().trim();
    
    if (!term || !searchIndex) {
      searchResults.innerHTML = '';
      resultsAvailable = false;
      return;
    }

    if (term.length < CONFIG.search.minChars) {
      searchResults.innerHTML = '<li class="search-message">Please enter at least 2 characters...</li>';
      resultsAvailable = false;
      return;
    }

    // Split search into terms
    const searchTerms = term.split(/\s+/).filter(t => t.length > 0);
    
    // Search with scoring
    const results = searchIndex
      .map(item => {
        let score = 0;
        const matchesAllTerms = searchTerms.every(term => {
          let matched = false;
          
          // Title matches (weighted higher)
          if (CONFIG.search.fields.title) {
            if (item.searchableTitle.startsWith(term)) {
              score += 3;  // Highest score for prefix matches in title
              matched = true;
            } else if (simpleFuzzyMatch(item.searchableTitle, term)) {
              score += 2;  // Good score for fuzzy matches in title
              matched = true;
            }
          }
          
          // Other field matches
          if (!matched) {
            if (CONFIG.search.fields.description && item.searchableDesc.includes(term)) {
              score += 0.5;  // Lower score for description matches
              matched = true;
            }
            if (CONFIG.search.fields.section && item.searchableSection.includes(term)) {
              score += 0.5;  // Lower score for section matches
              matched = true;
            }
          }
          
          return matched;
        });

        return {
          item,
          score: matchesAllTerms ? score : 0
        };
      })
      .filter(result => result.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, CONFIG.search.maxResults)
      .map(result => result.item);

    resultsAvailable = results.length > 0;
    
    if (!resultsAvailable) {
      searchResults.innerHTML = '<li class="search-message">No matching results found...</li>';
      return;
    }

    const searchItems = results.map(item => `
      <li>
        <a href="${escapeHtml(item.permalink)}" tabindex="0">
          <span class="title">${escapeHtml(item.title)}</span><br />
          <span class="sc">${escapeHtml(item.section)}</span> — 
          ${escapeHtml(item.date)}
          <em>${escapeHtml(item.desc)}</em>
        </a>
      </li>
    `).join('');
    
    searchResults.innerHTML = searchItems;
  }

  searchInput.addEventListener('input', function() {
    if (!searchIndex && !firstRun) {
      searchResults.innerHTML = '<li class="search-message">Loading search index...</li>';
      return;
    }
    performSearch(this.value);
  });

  // Add minimal styles
  const style = document.createElement('style');
  style.textContent = `
    .search-message {
      padding: 8px;
      color: #666;
      font-style: italic;
    }

    #searchResults li {
      animation: fadeSlideIn 0.2s ease-out;
      animation-fill-mode: both;
    }

    #searchResults li:nth-child(1) { animation-delay: 0.0s; }
    #searchResults li:nth-child(2) { animation-delay: 0.02s; }
    #searchResults li:nth-child(3) { animation-delay: 0.04s; }
    #searchResults li:nth-child(4) { animation-delay: 0.06s; }
    #searchResults li:nth-child(5) { animation-delay: 0.08s; }

    @keyframes fadeSlideIn {
      from {
        opacity: 0;
        transform: translateY(-10px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
  `;
  document.head.appendChild(style);
}

// Helper function to deep merge configs
function mergeConfigs(defaultConfig, userConfig) {
  const merged = { ...defaultConfig };
  
  for (const [key, value] of Object.entries(userConfig)) {
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      merged[key] = mergeConfigs(defaultConfig[key] || {}, value);
    } else {
      merged[key] = value;
    }
  }
  
  return merged;
}

// Basic HTML escaping for security
function escapeHtml(unsafe) {
  if (!unsafe) return '';
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// Initialize with default config
initSearch();

CSS Styling

#fastSearch { 
  visibility: hidden;
  position: absolute;
  right: 0px;
  top: 0px;
  display: inline-block;
  width: 300px;
}      

#fastSearch input { 
  padding: 4px 10px;
  width: 100%;
  height: 31px;
  font-size: 1.6em;
  color: #aaa;
  font-weight: bold;
  background-color: #000;
  border-radius: 3px 3px 0px 0px;
  border: none;
  outline: none;
  text-align: left;
  display: inline-block;
}

#searchResults li { 
  list-style: none; 
  margin-left: 0em;
  background-color: #333; 
  border-bottom: 1px dotted #000;
}
  #searchResults li .title { font-size: 1.1em; margin-bottom: 10px; display: inline-block;}

#searchResults { visibility: inherit; display: inline-block; width: 320px; }
#searchResults a { text-decoration: none !important; padding: 10px; display: inline-block; }
  #searchResults a:hover, a:focus { outline: 0; background-color: #666; color: #fff; }
  

layouts/_default/index.json

Hugo already builds indexes of all pages, we can cherry-pick which aspects should be searchable. The result is a newly created JSON index at /index.json

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "contents" .Plain "permalink" .Permalink) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

Config.toml

Add this snippet to your config file to instruct Hugo to create the index file in JSON format. (RSS and HTML are default outputs, what's important is to add JSON.

...
[outputs]
  home = ["HTML", "RSS", "JSON"]

Alternately if using a custom _index.md for home page, you can just add the output formats to front matter.

outputs:
- html
- rss
- json

See https://gohugo.io/templates/output-formats#output-formats-for-pages

License

The above cobbled together bits are provided as is under the so-called MIT License. Do whatever ya want with it all.

Copyright 2020 Craig Mod

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@tomcritchlow
Copy link

tomcritchlow commented Mar 9, 2020

This will work as is for Jekyll sites if you generate an index.json file using:

{"items":[{% for post in site.posts %}{ "title": {{post.title | jsonify}}, "permalink": {{ post.url | prepend: site.baseurl | prepend: site.url | jsonify }}, "date": {{ post.date | date: '%B %-d, %Y' | jsonify }}, "summary": {{ post.content | strip_html | strip_newlines | jsonify }} }{% unless forloop.last %},{% endunless %}{% endfor %}]}

This is adapted from here btw: https://gist.github.com/tomtorggler/45bd5478bc8d3542cb7ddb84e2c8eee7

@sarahnator
Copy link

Hi,
I'm having an issue with the search results -- each field displayed in the results says "undefined". I opened chrome's inspector and the title, link, and description fields are there, but in red. Someone mentioned the red highlighting means a detached DOM tree. I'm confused and wondering if I made a simple mistake somewhere. Does anyone have ideas?

@truckyforme
Copy link

truckyforme commented May 28, 2020

Hi,
I'm having an issue with the search results -- each field displayed in the results says "undefined". I opened chrome's inspector and the title, link, and description fields are there, but in red. Someone mentioned the red highlighting means a detached DOM tree. I'm confused and wondering if I made a simple mistake somewhere. Does anyone have ideas?

@sarahnator I think you need to reference the results differently:
results[item].permalink should be results[item].item.permalink
and the same for the rest of results

@froilan
Copy link

froilan commented May 28, 2020

I have encountered the "undefined" in search results. Upon digging in the code, I noticed the part that generates the html does not access the search results array properly, causing it to show as undefined in html.

In the function executeSearch(term)
Just replace this:

searchitems = searchitems + '<li><a href="' + results[item].permalink + '" tabindex="0">' + '<span class="title">' + results[item].title + '</span><br /> <span class="sc">'+ results[item].section +'</span> — ' + results[item].date + ' — <em>' + results[item].desc + '</em></a></li>';

with this:

  searchitems = searchitems + '<li><a href="' + results[item].item.permalink + '" tabindex="0">' + '<span class="title">' + results[item].item.title + '</span><br /> <span class="sc">'+ results[item].item.section +'</span> — ' + results[item].item.date + ' — <em>' + results[item].item.desc + '</em></a></li>';

The permalink, title, etc. are nested under an "item" key, so you'd need to access from that.

@froilan
Copy link

froilan commented May 28, 2020

For people encountering duplicate results on searches.
If your Hugo page is not a Single Page App, it will repeatedly execute the index.json.
This will cause the pages to be repeatedly added into the "index" search data. This will cause duplicate search results.

@iootaa
Copy link

iootaa commented Jul 6, 2020

Seems great! Unfortunately I'm not able to make it work, neither on craigmod.com or locally :

Hitting Shift + / displays Firefox internal search engine

Still nothing

Any idea of solution or way to debug?

Is there a way to always display the search form? (I've tried to set var searchVisible = true; , no succes)

@cmod
Copy link
Author

cmod commented Jul 6, 2020

@iootaa — 
it's Cmd + /
not Shift + /
:)

@iootaa
Copy link

iootaa commented Jul 6, 2020

Hi @cmod !
Sorry I should have been more explicit:

  • I have a french keyboard layout and the character / is obtained by typing Shift + :
  • The 'Cmd' seem to be specific to Mac, and as I'm using a PC under Linux, the equivalent is the 'Windows' key.

so I tried both combinations:

Win + : Nothing happens
Win + Shift + : Firefox internal research appears
Shift + : Firefox internal research appears

Keyboard layouts are a mess!
I'll dig more into it later, and try another keybinding.
Thanks for the follow-up!

@quangloc1993
Copy link

ur Hugo page is not a Single Page App, it will repeatedly execute the index.json.

do you know how to fix this issue?

@thenktor
Copy link

Example: craigmod.com
This search is live on my site, craigmod.com on every page. How to use:
Press CMD-/ to invoke search

On a non Mac keyboard (especially with a non english layout) it probably is impossible to test this 😞

@iootaa
Copy link

iootaa commented Aug 1, 2020

On a non Mac keyboard (especially with a non english layout) it probably is impossible to test this

Same here. Really frustrating :(

@renard
Copy link

renard commented Aug 4, 2020

Nice one.

Would it be possible to bind it to a search box in a topbar? CMD+/ is not obviously shown on the webpage.

@quangloc1993
Copy link

if anyone want to show search input field, just remove line visibility:hidden

#fastSearch {
visibility: hidden;
position: absolute;
right: 0px;
top: 0px;
display: inline-block;
width: 300px;
}

remove this line : visibility:hidden

@doncabreraphone
Copy link

CMD-/ ---> WINDOWS SOLUTION

All right, since people need it, here it is:

Replace this line:

if (event.metaKey && event.which === 191) {

With this line

  if (window.event.ctrlKey && event.which === 191) {

You then need to press CTRL + / to make it work in windows.

You are welcome. Check out Victor Hugo SEO tool by the way, by yours truly.

@doncabreraphone
Copy link

doncabreraphone commented Sep 4, 2020

ur Hugo page is not a Single Page App, it will repeatedly execute the index.json.

do you know how to fix this issue?

I do. Use this:

{{- range where .Site.RegularPages "Type" "not in"  (slice "page" "json") -}}

Instead of this:

{{- range .Site.RegularPages -}}

And that's about all you have to do.

@ciro-mota
Copy link

Please, is it possible to change the script to be read when loading the page instead of typing CMD + /? This key combination opens Firefox's internal search.

@Arty2
Copy link

Arty2 commented Dec 30, 2020

Thanks to Craig’s post which motivated me to work on this for my own setup. It's a very welcome improvement over the original jQuery based Gist, but unfortunately as noted in earlier comments, the code isn’t plug & play.

Hopefully I can gather the fixes into a new fork or a theme component, but till then:

  1. The Cmd key (Win under Windows) will not work in Firefox as it clashes with the Quick Find shortcut. Just replace event.metaKey with event.ctrlKey and use the Ctrl + / combo instead.
  2. “Undefined“ will appear in place of entries. Fixed at comment 3321668.
  3. Duplicate entries may occur. Fixed at comment 3442629.
  4. Expectedly, the index file can get very large if the full contents from all posts are included. This shouldn’t be a default as it’s likely to cause more trouble than benefit.
  5. Original index.json does not populate the section, date or description. Instead you can use:
{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.RegularPages "Type" "not in"  (slice "page" "json") -}}
   {{- $.Scratch.Add "index" (dict "title" .Title "permalink" .Permalink "section" (i18n (.Section | title)) "tags" (apply .Params.tags "i18n" "." ) "categories" (apply .Params.categories "i18n" "." ) "summary" (.Params.summary | markdownify | htmlUnescape | plainify) "date" .Date -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

An additional change in my code, is the i18n function, which is needed for localising categories, tags, sections in multilingual setups. But… you'd also need to point to the appropriate index.json per language, as noted in comment 3397955 on the original Gist.

For anyone arriving here looking for a quick solution, you might be interested in https://github.com/kaushalmodi/hugo-search-fuse-js which reaches to a similar point. (Without jQuery, edited out from my previous comment.)

@Arty2
Copy link

Arty2 commented Jan 3, 2021

Gathered the above with a few further additions in a fork here: https://gist.github.com/Arty2/8b0c43581013753438a3d35c15091a9f
Many thanks to @cmod and @eddiewebb for their work, and the commenters of the threads pointing to fixes and suggestions.

@moweiwei
Copy link

fastsearch.js:127 Uncaught TypeError: Cannot read properties of undefined (reading 'search')

image

@kepler-69c
Copy link

The events in the keyUp listener are deprecated.

Use event.key instead of event.keyCode, e.g. (line 16)

if (event.metaKey && event.key === '/') {
...

@cmod
Copy link
Author

cmod commented Aug 24, 2023

The events in the keyUp listener are deprecated.

Use event.key instead of event.keyCode, e.g. (line 16)

if (event.metaKey && event.key === '/') {
...

Thanks, updated!

@dsryu0822
Copy link

Is there any successed multilingual model? Maybe it fails search, even through index.json is well constructed.

@ZMYaro
Copy link

ZMYaro commented Nov 18, 2024

Very late contributing to this, but I would like to suggest Alt+/ as the shortcut on non-Mac platforms. It is already used in other applications (most notably the Google Workspace suite), it aligns with the Windows standard of Alt+ shortcuts to open menus, and as far as I can tell, it doesn't conflict with other browser or system shortcuts.

Regardless, using event.metaKey shortcuts in a web page (or even native app) on Chrome OS or Windows is generally bad practice since 🔍+ shortcuts (CrOS) or + shortcuts (Windows) are expected to be system shortcuts.

Because of Apple's different keyboard shortcut conventions, I have often done things like:

const isApple = (navigator.userAgent.indexOf('Mac') !== -1);
function isPlatformAltOrCmdKey(ev) {
	// On MacOS and iOS, check Cmd; on other platforms, check Alt.
	return ((!isApple && ev.altKey) || (isApple && ev.metaKey));
}

if (isPlatformAltOrCmdKey(event) && event.key === '/') {
	

@TajangSec
Copy link

Can it be used explicitly? I have designed a nice search box, and I want it to automatically search and display content as I type characters.

@cmod
Copy link
Author

cmod commented Jan 17, 2025

Updated to work with latest Fuse (7.0)

@cmod
Copy link
Author

cmod commented Feb 16, 2025

updated to be more performant and allow easy changing of hotkey instantiation

@cmod
Copy link
Author

cmod commented Feb 20, 2025

now no longer dependent on fuse, added Enter to select and go to first result url

@matthieugd
Copy link

@cmod in the instructions, the index.json template doesn't fit the variable names used in the JS file. I tweaked it to

  • have the content as the desc (searchable)
  • a new summary property to be displayed in lieu of the desc property
  • added the date variable missing in the index.json but used in the result template
  • added the section variable missing

{{- $.Scratch.Add "index" slice -}} {{- range .Site.RegularPages -}} {{- $.Scratch.Add "index" (dict "date" (.PublishDate.Format "01-12-2006") "title" .Title "tags" .Params.tags "section" (index .Params.categories 0) "desc" .Plain "summary" (printf "%s..." (substr (.Summary | plainify) 0 90)) "permalink" .Permalink) -}} {{- end -}} {{- $.Scratch.Get "index" | jsonify -}}

Line 251 of the template, I use item.summary to display the first X characters

Thanks for this great tip for Hugo websites !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment