Skip to content

Commit 72ff768

Browse files
committed
Add recent notes to empty search
1 parent b15a5d9 commit 72ff768

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

internal/app/app.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,10 +496,23 @@ func (s *Server) home(w http.ResponseWriter, r *http.Request) {
496496

497497
func (s *Server) search(w http.ResponseWriter, r *http.Request) {
498498
q := r.URL.Query().Get("q")
499-
res, err := s.searcher.Search(q)
499+
qTrimmed := strings.TrimSpace(q)
500+
var res []SearchResult
501+
var recent []NoteMeta
502+
var err error
503+
if qTrimmed != "" {
504+
res, err = s.searcher.Search(qTrimmed)
505+
} else {
506+
var idx *VaultIndex
507+
idx, err = s.vault.BuildIndex()
508+
if err == nil {
509+
recent = recentNoteMetas(idx, 100)
510+
}
511+
}
500512
c := s.common("Search")
501513
c["Q"] = q
502514
c["Results"] = res
515+
c["RecentNotes"] = recent
503516
c["Err"] = err
504517
s.render(w, "search", c)
505518
}

internal/app/search_dashboard_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package app
22

33
import (
4+
"fmt"
45
"net/http/httptest"
56
"os"
67
"path/filepath"
78
"strings"
89
"testing"
10+
"time"
911
)
1012

1113
func TestBacklinksAndSearch(t *testing.T) {
@@ -61,6 +63,55 @@ func TestSearchRanksTitleMatchesAndHighlightsSnippets(t *testing.T) {
6163
}
6264
}
6365

66+
func TestEmptySearchPageShowsHundredMostRecentlyModifiedNotes(t *testing.T) {
67+
v := makeVault(t)
68+
base := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
69+
for i := 0; i < 105; i++ {
70+
rel := fmt.Sprintf("Recent/Note-%03d.md", i)
71+
p := filepath.Join(v.Root, filepath.FromSlash(rel))
72+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
73+
t.Fatal(err)
74+
}
75+
if err := os.WriteFile(p, []byte(fmt.Sprintf("# Recent %03d\n", i)), 0o644); err != nil {
76+
t.Fatal(err)
77+
}
78+
mt := base.Add(time.Duration(i) * time.Minute)
79+
if err := os.Chtimes(p, mt, mt); err != nil {
80+
t.Fatal(err)
81+
}
82+
}
83+
84+
s := NewServer(v, "", "")
85+
w := httptest.NewRecorder()
86+
r := httptest.NewRequest("GET", "/_search", nil)
87+
s.ServeHTTP(w, r)
88+
body := w.Body.String()
89+
90+
for _, want := range []string{
91+
`<h2>Recently modified</h2>`,
92+
`Showing the 100 latest Markdown files by modification date.`,
93+
`Recent 104`,
94+
`Recent/Note-104.md`,
95+
} {
96+
if !strings.Contains(body, want) {
97+
t.Fatalf("empty search page missing %q in:\n%s", want, body)
98+
}
99+
}
100+
recentSectionStart := strings.Index(body, `class="recent-search-results card"`)
101+
if recentSectionStart < 0 {
102+
t.Fatalf("missing recent results section in:\n%s", body)
103+
}
104+
recentSection := body[recentSectionStart:]
105+
if strings.Contains(recentSection, `<small>Recent/Note-004.md`) {
106+
t.Fatalf("empty search page should cap recent notes to 100; oldest extra note leaked in recent section:\n%s", recentSection)
107+
}
108+
newest := strings.Index(recentSection, `Recent/Note-104.md`)
109+
older := strings.Index(recentSection, `Recent/Note-103.md`)
110+
if newest < 0 || older < 0 || newest > older {
111+
t.Fatalf("recent notes should be sorted by descending mtime; got indexes newest=%d older=%d", newest, older)
112+
}
113+
}
114+
64115
func TestSearchQuerySyntaxFiltersTagsPathsTitlesFrontmatterAndPhrases(t *testing.T) {
65116
v := makeVault(t)
66117
searcher := NewSearcher(v)

internal/app/templates/search.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
{{define "search"}}{{template "layout-start" .}}<div class="page-header"><div><p class="eyebrow">Find anything</p><h1>Search</h1></div></div><form class="search big search-page-form" action="/_search"><input name="q" value="{{.Q}}" placeholder="Search notes, tags, paths, or exact phrases…" autofocus><button class="btn primary">Search</button></form><details class="search-help"><summary>Search syntax</summary><p class="muted">Examples: <code>tag:daily</code>, <code>path:&quot;Areas/Daily Briefings&quot;</code>, <code>title:Target</code>, <code>frontmatter:title=&quot;Daily Briefing&quot;</code>, <code>&quot;exact phrase&quot;</code>.</p><ul><li><code>tag:daily</code> filters by tag.</li><li><code>path:&quot;Areas/Daily Briefings&quot;</code> filters by path.</li><li><code>title:Target</code> filters by title.</li><li><code>frontmatter:title=&quot;Daily Briefing&quot;</code> filters frontmatter.</li><li><code>&quot;exact phrase&quot;</code> searches an exact phrase.</li></ul></details>{{if .Err}}<p class="error">{{.Err}}</p>{{end}}{{if .Results}}<ul class="results rich-results">{{range .Results}}<li class="search-result-card"><a class="result-title" href="{{.URL}}">{{.Title}}</a><div class="result-path">{{.RelPath}}{{if .LineNo}} · line {{.LineNo}}{{end}}</div><p class="result-snippet">{{safe .SnippetHTML}}</p></li>{{end}}</ul>{{else}}{{if .Q}}<div class="empty-state"><h2>No results for <code>{{.Q}}</code></h2><p>Try a broader search, remove a filter, or open Search syntax for examples.</p></div>{{end}}{{end}}{{template "layout-end" .}}{{end}}
1+
{{define "search"}}{{template "layout-start" .}}
2+
<div class="page-header"><div><p class="eyebrow">Find anything</p><h1>Search</h1></div></div>
3+
<form class="search big search-page-form" action="/_search"><input name="q" value="{{.Q}}" placeholder="Search notes, tags, paths, or exact phrases…" autofocus><button class="btn primary">Search</button></form>
4+
<details class="search-help"><summary>Search syntax</summary><p class="muted">Examples: <code>tag:daily</code>, <code>path:&quot;Areas/Daily Briefings&quot;</code>, <code>title:Target</code>, <code>frontmatter:title=&quot;Daily Briefing&quot;</code>, <code>&quot;exact phrase&quot;</code>.</p><ul><li><code>tag:daily</code> filters by tag.</li><li><code>path:&quot;Areas/Daily Briefings&quot;</code> filters by path.</li><li><code>title:Target</code> filters by title.</li><li><code>frontmatter:title=&quot;Daily Briefing&quot;</code> filters frontmatter.</li><li><code>&quot;exact phrase&quot;</code> searches an exact phrase.</li></ul></details>
5+
{{if .Err}}<p class="error">{{.Err}}</p>{{end}}
6+
{{if .Q}}
7+
{{if .Results}}<ul class="results rich-results">{{range .Results}}<li class="search-result-card"><a class="result-title" href="{{.URL}}">{{.Title}}</a><div class="result-path">{{.RelPath}}{{if .LineNo}} · line {{.LineNo}}{{end}}</div><p class="result-snippet">{{safe .SnippetHTML}}</p></li>{{end}}</ul>{{else}}<div class="empty-state"><h2>No results for <code>{{.Q}}</code></h2><p>Try a broader search, remove a filter, or open Search syntax for examples.</p></div>{{end}}
8+
{{else}}
9+
<section class="recent-search-results card"><div class="section-heading"><div><p class="eyebrow">Files</p><h2>Recently modified</h2></div><span class="count">{{len .RecentNotes}}</span></div><p class="muted">Showing the 100 latest Markdown files by modification date.</p><ul class="compact-list">{{range .RecentNotes}}<li><a href="{{.URL}}">{{.Title}}</a><small>{{.RelPath}} · {{.ModTime.Format "2006-01-02 15:04"}}</small></li>{{else}}<li class="empty-state">No Markdown files found.</li>{{end}}</ul></section>
10+
{{end}}
11+
{{template "layout-end" .}}{{end}}

0 commit comments

Comments
 (0)