diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index ee51d3e..8334ea2 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -14,7 +14,7 @@ jobs:
           fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod
 
       - name: Build Link Index
-        uses: jackyzha0/hugo-obsidian@v2.12
+        uses: jackyzha0/hugo-obsidian@v2.13
         with:
           index: true
           input: content
diff --git a/assets/js/popover.js b/assets/js/popover.js
index ee0477e..ea01156 100644
--- a/assets/js/popover.js
+++ b/assets/js/popover.js
@@ -5,19 +5,20 @@ function htmlToElement(html) {
   return template.content.firstChild
 }
 
-function initPopover(baseURL) {
+function initPopover(baseURL, useContextualBacklinks) {
   const basePath = baseURL.replace(window.location.origin, "")
   document.addEventListener("DOMContentLoaded", () => {
     fetchData.then(({ content }) => {
       const links = [...document.getElementsByClassName("internal-link")]
       links
-        .filter(li => li.dataset.src)
+        .filter(li => li.dataset.src || (li.dataset.idx && useContextualBacklinks))
         .forEach(li => {
-          const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")]
-          if (linkDest) {
+          if (li.dataset.ctx) {
+            console.log(li.dataset.ctx)
+            const linkDest = content[li.dataset.src]
             const popoverElement = `<div class="popover">
     <h3>${linkDest.title}</h3>
-    <p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p>
+    <p>${highlight(removeMarkdown(linkDest.content), li.dataset.ctx)}...</p>
     <p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
 </div>`
             const el = htmlToElement(popoverElement)
@@ -28,6 +29,23 @@ function initPopover(baseURL) {
             li.addEventListener("mouseout", () => {
               el.classList.remove("visible")
             })
+          } else {
+            const linkDest = content[li.dataset.src.replace(/\/$/g, "").replace(basePath, "")]
+            if (linkDest) {
+              const popoverElement = `<div class="popover">
+    <h3>${linkDest.title}</h3>
+    <p>${removeMarkdown(linkDest.content).split(" ", 20).join(" ")}...</p>
+    <p class="meta">${new Date(linkDest.lastmodified).toLocaleDateString()}</p>
+</div>`
+              const el = htmlToElement(popoverElement)
+              li.appendChild(el)
+              li.addEventListener("mouseover", () => {
+                el.classList.add("visible")
+              })
+              li.addEventListener("mouseout", () => {
+                el.classList.remove("visible")
+              })
+            }
           }
         })
     })
diff --git a/assets/js/search.js b/assets/js/search.js
index f124d58..c5e293c 100644
--- a/assets/js/search.js
+++ b/assets/js/search.js
@@ -52,9 +52,65 @@ const removeMarkdown = (
     return markdown
   }
   return output
-};
+}
 // -----
 
+const highlight = (content, term) => {
+  const highlightWindow = 20
+
+  // try to find direct match first
+  const directMatchIdx = content.indexOf(term)
+  if (directMatchIdx !== -1) {
+    const h = highlightWindow / 2
+    const before = content.substring(0, directMatchIdx).split(" ").slice(-h)
+    const after = content.substring(directMatchIdx + term.length, content.length - 1).split(" ").slice(0, h)
+    return (before.length == h ? `...${before.join(" ")}` : before.join(" ")) + `<span class="search-highlight">${term}</span>` + after.join(" ")
+  }
+
+  const tokenizedTerm = term.split(/\s+/).filter((t) => t !== '')
+  const splitText = content.split(/\s+/).filter((t) => t !== '')
+  const includesCheck = (token) =>
+    tokenizedTerm.some((term) =>
+      token.toLowerCase().startsWith(term.toLowerCase())
+    )
+
+  const occurrencesIndices = splitText.map(includesCheck)
+
+  // calculate best index
+  let bestSum = 0
+  let bestIndex = 0
+  for (
+    let i = 0;
+    i < Math.max(occurrencesIndices.length - highlightWindow, 0);
+    i++
+  ) {
+    const window = occurrencesIndices.slice(i, i + highlightWindow)
+    const windowSum = window.reduce((total, cur) => total + cur, 0)
+    if (windowSum >= bestSum) {
+      bestSum = windowSum
+      bestIndex = i
+    }
+  }
+
+  const startIndex = Math.max(bestIndex - highlightWindow, 0)
+  const endIndex = Math.min(
+    startIndex + 2 * highlightWindow,
+    splitText.length
+  )
+  const mappedText = splitText
+    .slice(startIndex, endIndex)
+    .map((token) => {
+      if (includesCheck(token)) {
+        return `<span class="search-highlight">${token}</span>`
+      }
+      return token
+    })
+    .join(' ')
+    .replaceAll('</span> <span class="search-highlight">', ' ')
+  return `${startIndex === 0 ? '' : '...'}${mappedText}${endIndex === splitText.length ? '' : '...'
+    }`
+};
+
 (async function() {
   const encoder = (str) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])+/)
   const contentIndex = new FlexSearch.Document({
@@ -84,52 +140,6 @@ const removeMarkdown = (
     })
   }
 
-  const highlight = (content, term) => {
-    const highlightWindow = 20
-    const tokenizedTerm = term.split(/\s+/).filter((t) => t !== '')
-    const splitText = content.split(/\s+/).filter((t) => t !== '')
-    const includesCheck = (token) =>
-      tokenizedTerm.some((term) =>
-        token.toLowerCase().startsWith(term.toLowerCase())
-      )
-
-    const occurrencesIndices = splitText.map(includesCheck)
-
-    // calculate best index
-    let bestSum = 0
-    let bestIndex = 0
-    for (
-      let i = 0;
-      i < Math.max(occurrencesIndices.length - highlightWindow, 0);
-      i++
-    ) {
-      const window = occurrencesIndices.slice(i, i + highlightWindow)
-      const windowSum = window.reduce((total, cur) => total + cur, 0)
-      if (windowSum >= bestSum) {
-        bestSum = windowSum
-        bestIndex = i
-      }
-    }
-
-    const startIndex = Math.max(bestIndex - highlightWindow, 0)
-    const endIndex = Math.min(
-      startIndex + 2 * highlightWindow,
-      splitText.length
-    )
-    const mappedText = splitText
-      .slice(startIndex, endIndex)
-      .map((token) => {
-        if (includesCheck(token)) {
-          return `<span class="search-highlight">${token}</span>`
-        }
-        return token
-      })
-      .join(' ')
-      .replaceAll('</span> <span class="search-highlight">', ' ')
-    return `${startIndex === 0 ? '' : '...'}${mappedText}${endIndex === splitText.length ? '' : '...'
-      }`
-  }
-
   const resultToHTML = ({ url, title, content, term }) => {
     const text = removeMarkdown(content)
     const resultTitle = highlight(title, term)
diff --git a/assets/styles/base.scss b/assets/styles/base.scss
index 9bbd933..0787470 100644
--- a/assets/styles/base.scss
+++ b/assets/styles/base.scss
@@ -478,17 +478,17 @@ header {
         & > h3, & > p {
           margin: 0;
         }
-
-        & .search-highlight {
-          background-color: #afbfc966;
-          padding: 0.05em 0.2em;
-          border-radius: 3px;
-        }
       }
     }
   }
 }
 
+.search-highlight {
+  background-color: #afbfc966;
+  padding: 0.05em 0.2em;
+  border-radius: 3px;
+}
+
 .section-ul {
   list-style: none;
   padding-left: 0;
diff --git a/data/config.yaml b/data/config.yaml
index afa531c..ccf38eb 100644
--- a/data/config.yaml
+++ b/data/config.yaml
@@ -4,6 +4,7 @@ openToc: false
 enableLinkPreview: true
 enableLatex: true
 enableSPA: false
+enableContextualBacklinks: true
 description:
   Host your second brain and digital garden for free. Quartz features extremely fast full-text search,
   Wikilink support, backlinks, local graph, tags, and link previews.
diff --git a/layouts/partials/backlinks.html b/layouts/partials/backlinks.html
index e42351a..23c9091 100644
--- a/layouts/partials/backlinks.html
+++ b/layouts/partials/backlinks.html
@@ -7,13 +7,18 @@
     {{$inbound := index $linkIndex.index.backlinks $curPage}}
     {{$contentTable := getJSON "/assets/indices/contentIndex.json"}}
     {{if $inbound}}
-    {{$cleanedInbound := apply (apply $inbound "index" "." "source") "replace" "." " " "-"}}
-    {{- range $cleanedInbound | uniq -}}
-      {{$l := printf "%s%s/" $host .}}
+    {{$backlinks := dict "SENTINEL" "SENTINEL"}}
+    {{range $k, $v := $inbound}}
+      {{$cleanedInbound := replace $v.source " " "-"}}
+      {{$ctx := $v.text}}
+      {{$backlinks = merge $backlinks (dict $cleanedInbound $ctx)}}
+    {{end}}
+    {{- range $lnk, $ctx := $backlinks -}}
+      {{$l := printf "%s%s/" $host $lnk}}
       {{$l = cond (eq $l "//") "/" $l}}
-      {{with (index $contentTable .)}}
+      {{with (index $contentTable $lnk)}}
       <li>
-          <a href="{{$l}}">{{index (index . "title")}}</a>
+        <a href="{{$l}}" data-ctx="{{$ctx}}" data-src="{{$lnk}}" class="internal-link">{{index (index . "title")}}</a>
       </li>
       {{end}}
     {{- end -}}
diff --git a/layouts/partials/popover.html b/layouts/partials/popover.html
index 1d16622..ba1fd03 100644
--- a/layouts/partials/popover.html
+++ b/layouts/partials/popover.html
@@ -2,6 +2,7 @@
 {{ $js := resources.Get "js/popover.js" |  resources.Fingerprint "md5" | resources.Minify }}
 <script src="{{ $js.Permalink }}"></script>
 <script>
-  initPopover({{strings.TrimRight "/" .Site.BaseURL }})
+  const useContextual = {{ $.Site.Data.config.enableContextualBacklinks }}
+  initPopover({{strings.TrimRight "/" .Site.BaseURL }}, useContextual)
 </script>
-{{end}}
\ No newline at end of file
+{{end}}