feat(ui): dark theme redesign, fix image search and defaults
- Inline CSS in base.html (Inter, dark mode, sticky search, tabs, results) - Remove HTMX/JS from templates; pagination via GET links - Atmospheric side gradients + grid; wider column on large viewports - Parse ?category= for HTML tabs (fixes Images category routing) - Include bing_images, ddg_images, qwant_images in local_ported defaults - Default listen port 5355; update Docker, compose, flake, README - Favicon img uses /favicon/ proxy; preferences without inline JS Made-with: Cursor
This commit is contained in:
parent
bdc3dae4f5
commit
518215f62e
16 changed files with 1107 additions and 106 deletions
|
|
@ -21,7 +21,7 @@ RUN apk add --no-cache ca-certificates tzdata
|
||||||
COPY --from=builder /kafka /usr/local/bin/kafka
|
COPY --from=builder /kafka /usr/local/bin/kafka
|
||||||
COPY config.example.toml /etc/kafka/config.example.toml
|
COPY config.example.toml /etc/kafka/config.example.toml
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 5355
|
||||||
|
|
||||||
ENTRYPOINT ["kafka"]
|
ENTRYPOINT ["kafka"]
|
||||||
CMD ["-config", "/etc/kafka/config.toml"]
|
CMD ["-config", "/etc/kafka/config.toml"]
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ go run ./cmd/samsa -config config.toml
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl "http://localhost:8080/search?q=golang&format=json&engines=github,duckduckgo"
|
curl "http://localhost:5355/search?q=golang&format=json&engines=github,duckduckgo"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response (JSON)
|
### Response (JSON)
|
||||||
|
|
@ -155,7 +155,7 @@ Copy `config.example.toml` to `config.toml` and edit. All settings can also be o
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `PORT` | Listen port (default: 8080) |
|
| `PORT` | Listen port (default: 5355) |
|
||||||
| `BASE_URL` | Public URL for OpenSearch XML |
|
| `BASE_URL` | Public URL for OpenSearch XML |
|
||||||
| `UPSTREAM_SEARXNG_URL` | Upstream instance URL |
|
| `UPSTREAM_SEARXNG_URL` | Upstream instance URL |
|
||||||
| `LOCAL_PORTED_ENGINES` | Comma-separated local engine list |
|
| `LOCAL_PORTED_ENGINES` | Comma-separated local engine list |
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
# Listen port (env: PORT)
|
# Listen port (env: PORT)
|
||||||
port = 8080
|
port = 5355
|
||||||
|
|
||||||
# HTTP timeout for engine and upstream calls (env: HTTP_TIMEOUT)
|
# HTTP timeout for engine and upstream calls (env: HTTP_TIMEOUT)
|
||||||
http_timeout = "10s"
|
http_timeout = "10s"
|
||||||
|
|
@ -27,7 +27,8 @@ url = ""
|
||||||
[engines]
|
[engines]
|
||||||
# Comma-separated list of engines to execute locally in Go (env: LOCAL_PORTED_ENGINES)
|
# Comma-separated list of engines to execute locally in Go (env: LOCAL_PORTED_ENGINES)
|
||||||
# Engines not listed here will be proxied to the upstream instance.
|
# Engines not listed here will be proxied to the upstream instance.
|
||||||
local_ported = ["wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing", "google", "youtube"]
|
# Include bing_images, ddg_images, qwant_images for image search when [upstream].url is empty.
|
||||||
|
local_ported = ["wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing", "google", "youtube", "bing_images", "ddg_images", "qwant_images"]
|
||||||
|
|
||||||
[engines.brave]
|
[engines.brave]
|
||||||
# Brave Search API key (env: BRAVE_API_KEY)
|
# Brave Search API key (env: BRAVE_API_KEY)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ services:
|
||||||
kafka:
|
kafka:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "5355:5355"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.toml:/etc/kafka/config.toml:ro
|
- ./config.toml:/etc/kafka/config.toml:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
default = 8080;
|
default = 5355;
|
||||||
description = "Port to listen on.";
|
description = "Port to listen on.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,12 +160,12 @@ func validateConfig(cfg *Config) error {
|
||||||
func defaultConfig() *Config {
|
func defaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 8080,
|
Port: 5355,
|
||||||
HTTPTimeout: "10s",
|
HTTPTimeout: "10s",
|
||||||
},
|
},
|
||||||
Upstream: UpstreamConfig{},
|
Upstream: UpstreamConfig{},
|
||||||
Engines: EnginesConfig{
|
Engines: EnginesConfig{
|
||||||
LocalPorted: []string{"wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing", "google", "youtube"},
|
LocalPorted: []string{"wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing", "google", "youtube", "bing_images", "ddg_images", "qwant_images"},
|
||||||
Qwant: QwantConfig{
|
Qwant: QwantConfig{
|
||||||
Category: "web-lite",
|
Category: "web-lite",
|
||||||
ResultsPerPage: 10,
|
ResultsPerPage: 10,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ func TestLoadDefaults(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load with missing file should return defaults: %v", err)
|
t.Fatalf("Load with missing file should return defaults: %v", err)
|
||||||
}
|
}
|
||||||
if cfg.Server.Port != 8080 {
|
if cfg.Server.Port != 5355 {
|
||||||
t.Errorf("expected default port 8080, got %d", cfg.Server.Port)
|
t.Errorf("expected default port 5355, got %d", cfg.Server.Port)
|
||||||
}
|
}
|
||||||
if len(cfg.Engines.LocalPorted) != 11 {
|
if len(cfg.Engines.LocalPorted) != 14 {
|
||||||
t.Errorf("expected 11 default engines, got %d", len(cfg.Engines.LocalPorted))
|
t.Errorf("expected 14 default engines, got %d", len(cfg.Engines.LocalPorted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,10 @@ func ParseSearchRequest(r *http.Request) (SearchRequest, error) {
|
||||||
delete(catSet, category)
|
delete(catSet, category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// HTML UI uses a single ?category=images (etc.) query param; honor it here.
|
||||||
|
if single := strings.TrimSpace(r.FormValue("category")); single != "" {
|
||||||
|
catSet[single] = true
|
||||||
|
}
|
||||||
categories := make([]string, 0, len(catSet))
|
categories := make([]string, 0, len(catSet))
|
||||||
for c := range catSet {
|
for c := range catSet {
|
||||||
categories = append(categories, c)
|
categories = append(categories, c)
|
||||||
|
|
@ -250,4 +254,3 @@ func parseAccessToken(r *http.Request) string {
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,21 @@ func TestParseSearchRequest_CategoriesAndEngineData(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSearchRequest_SingularCategoryParam(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/search?q=cats&category=images", nil)
|
||||||
|
req, err := ParseSearchRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, c := range req.Categories {
|
||||||
|
if c == "images" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected category images from ?category=images, got %v", req.Categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,9 +2,9 @@
|
||||||
<a class="image-result" href="{{.URL}}" target="_blank" rel="noopener noreferrer">
|
<a class="image-result" href="{{.URL}}" target="_blank" rel="noopener noreferrer">
|
||||||
<div class="image-thumb">
|
<div class="image-thumb">
|
||||||
{{if .Thumbnail}}
|
{{if .Thumbnail}}
|
||||||
<img src="{{.Thumbnail}}" alt="{{.Title}}" loading="lazy" onerror="this.parentElement.classList.add('image-error')">
|
<img src="{{.Thumbnail}}" alt="{{.Title}}" loading="lazy">
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="image-placeholder">🖼️</div>
|
<div class="image-placeholder" aria-hidden="true">🖼️</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="image-meta">
|
<div class="image-meta">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<h2 class="pref-section-title">Appearance</h2>
|
<h2 class="pref-section-title">Appearance</h2>
|
||||||
<div class="pref-row">
|
<div class="pref-row">
|
||||||
<label for="theme-select">Theme</label>
|
<label for="theme-select">Theme</label>
|
||||||
<select name="theme" id="theme-select" onchange="this.form.submit()">
|
<select name="theme" id="theme-select">
|
||||||
<option value="light" {{if eq .Theme "light"}}selected{{end}}>Light</option>
|
<option value="light" {{if eq .Theme "light"}}selected{{end}}>Light</option>
|
||||||
<option value="dark" {{if eq .Theme "dark"}}selected{{end}}>Dark</option>
|
<option value="dark" {{if eq .Theme "dark"}}selected{{end}}>Dark</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="pref-row">
|
<div class="pref-row">
|
||||||
<div class="pref-row-info">
|
<div class="pref-row-info">
|
||||||
<label>Favicon Service</label>
|
<label for="pref-favicon">Favicon Service</label>
|
||||||
<p class="pref-desc">Fetch favicons for result URLs. "None" is most private.</p>
|
<p class="pref-desc">Fetch favicons for result URLs. "None" is most private.</p>
|
||||||
</div>
|
</div>
|
||||||
<select name="favicon" id="pref-favicon">
|
<select name="favicon" id="pref-favicon">
|
||||||
|
|
@ -105,26 +105,4 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
// Load saved engine preferences
|
|
||||||
var savedEngines = JSON.parse(localStorage.getItem('samsa-engines') || 'null');
|
|
||||||
if (savedEngines) {
|
|
||||||
savedEngines.forEach(function(engine) {
|
|
||||||
var checkbox = document.querySelector('input[name="engine"][value="' + engine.id + '"]');
|
|
||||||
if (checkbox) checkbox.checked = engine.enabled;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save on submit
|
|
||||||
document.querySelector('.preferences-form').addEventListener('submit', function() {
|
|
||||||
var engines = [];
|
|
||||||
document.querySelectorAll('input[name="engine"]').forEach(function(cb) {
|
|
||||||
engines.push({ id: cb.value, enabled: cb.checked });
|
|
||||||
});
|
|
||||||
localStorage.setItem('samsa-engines', JSON.stringify(engines));
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="result_url">
|
<div class="result_url">
|
||||||
<img class="result-favicon" data-domain="{{.Domain}}" src="" alt="" loading="lazy" style="display:none">
|
{{if .Domain}}
|
||||||
|
<img class="result-favicon" src="/favicon/{{.Domain}}" alt="" loading="lazy" width="14" height="14">
|
||||||
|
{{end}}
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
||||||
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
|
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="results-container">
|
<div class="results-container">
|
||||||
<div class="results-header">
|
<div class="results-header">
|
||||||
|
<div class="results-header-inner">
|
||||||
<a href="/" class="results-logo">
|
<a href="/" class="results-logo">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<circle cx="11" cy="11" r="8"/>
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
|
@ -10,7 +11,10 @@
|
||||||
<span>samsa</span>
|
<span>samsa</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<form class="header-search" method="GET" action="/search" role="search" hx-get="/search" hx-target="#urls" hx-swap="innerHTML" hx-select="#urls">
|
<form class="header-search" method="GET" action="/search" role="search">
|
||||||
|
{{if and .ActiveCategory (ne .ActiveCategory "all")}}
|
||||||
|
<input type="hidden" name="category" value="{{.ActiveCategory}}">
|
||||||
|
{{end}}
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input type="text" name="q" value="{{.Query}}" placeholder="Search…" autocomplete="off">
|
<input type="text" name="q" value="{{.Query}}" placeholder="Search…" autocomplete="off">
|
||||||
<button type="submit" class="search-btn" aria-label="Search">
|
<button type="submit" class="search-btn" aria-label="Search">
|
||||||
|
|
@ -22,13 +26,14 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="category-tabs" role="tablist">
|
<div class="category-tabs" role="tablist">
|
||||||
<a href="/search?q={{.Query | urlquery}}&category=" class="category-tab {{if eq .ActiveCategory ""}}active{{end}}">All</a>
|
<a href="/search?q={{.Query | urlquery}}&category=" class="category-tab {{if or (eq .ActiveCategory "") (eq .ActiveCategory "all")}}active{{end}}">All</a>
|
||||||
<a href="/search?q={{.Query | urlquery}}&category=general" class="category-tab {{if eq .ActiveCategory "general"}}active{{end}}">General</a>
|
<a href="/search?q={{.Query | urlquery}}&category=general" class="category-tab {{if eq .ActiveCategory "general"}}active{{end}}">General</a>
|
||||||
<a href="/search?q={{.Query | urlquery}}&category=it" class="category-tab {{if eq .ActiveCategory "it"}}active{{end}}">IT</a>
|
<a href="/search?q={{.Query | urlquery}}&category=it" class="category-tab {{if eq .ActiveCategory "it"}}active{{end}}">IT</a>
|
||||||
<a href="/search?q={{.Query | urlquery}}&category=news" class="category-tab {{if eq .ActiveCategory "news"}}active{{end}}">News</a>
|
<a href="/search?q={{.Query | urlquery}}&category=news" class="category-tab {{if eq .ActiveCategory "news"}}active{{end}}">News</a>
|
||||||
<a href="/search?q={{.Query | urlquery}}&category=images" class="category-tab {{if eq .ActiveCategory "images"}}active{{end}}">Images</a>
|
<a href="/search?q={{.Query | urlquery}}&category=images" class="category-tab {{if eq .ActiveCategory "images"}}active{{end}}">Images</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="results-content">
|
<div class="results-content">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{{define "results_inner"}}
|
{{define "results_inner"}}
|
||||||
{{if .Corrections}}
|
{{if .Corrections}}
|
||||||
<div id="corrections" class="correction" hx-swap-oob="true">{{range .Corrections}}{{.}} {{end}}</div>
|
<div id="corrections" class="correction">{{range .Corrections}}{{.}} {{end}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if or .Answers .Infoboxes}}
|
{{if or .Answers .Infoboxes}}
|
||||||
|
|
@ -11,13 +11,13 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="results-meta" id="results-meta" hx-swap-oob="true">
|
<div class="results-meta" id="results-meta">
|
||||||
{{if .NumberOfResults}}
|
{{if .NumberOfResults}}
|
||||||
<span>{{.NumberOfResults}} results</span>
|
<span>{{.NumberOfResults}} results</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="urls" role="main" hx-select="#urls" hx-swap="innerHTML" hx-target="#urls">
|
<div id="urls" role="main">
|
||||||
{{if .Results}}
|
{{if .Results}}
|
||||||
{{if .IsImageSearch}}
|
{{if .IsImageSearch}}
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else if not .Answers}}
|
{{else if not .Answers}}
|
||||||
<div class="no-results">
|
<div class="no-results">
|
||||||
<div class="no-results-icon">🔍</div>
|
<div class="no-results-icon" aria-hidden="true">🔍</div>
|
||||||
<h2>No results found</h2>
|
<h2>No results found</h2>
|
||||||
<p>Try different keywords or check your spelling.</p>
|
<p>Try different keywords or check your spelling.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,42 +48,26 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Pageno}}
|
{{if .Pageno}}
|
||||||
<nav class="pagination" role="navigation">
|
<nav class="pagination" role="navigation" aria-label="Pagination">
|
||||||
{{if gt .Pageno 1}}
|
{{if gt .Pageno 1}}
|
||||||
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.PrevPage}}">← Prev</button>
|
<a class="pag-link" href="/search?q={{.Query | urlquery}}&pageno={{.PrevPage}}{{if and .ActiveCategory (ne .ActiveCategory "all")}}&category={{.ActiveCategory | urlquery}}{{end}}">← Prev</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{range .PageNumbers}}
|
{{range .PageNumbers}}
|
||||||
{{if .IsCurrent}}
|
{{if .IsCurrent}}
|
||||||
<span class="page-current">{{.Num}}</span>
|
<span class="page-current" aria-current="page">{{.Num}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" class="paginate-btn" data-q="{{$.Query}}" data-page="{{.Num}}">{{.Num}}</button>
|
<a class="pag-link" href="/search?q={{$.Query | urlquery}}&pageno={{.Num}}{{if and $.ActiveCategory (ne $.ActiveCategory "all")}}&category={{$.ActiveCategory | urlquery}}{{end}}">{{.Num}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .HasNext}}
|
{{if .HasNext}}
|
||||||
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.NextPage}}">Next →</button>
|
<a class="pag-link" href="/search?q={{.Query | urlquery}}&pageno={{.NextPage}}{{if and .ActiveCategory (ne .ActiveCategory "all")}}&category={{.ActiveCategory | urlquery}}{{end}}">Next →</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="back-to-top" id="backToTop">
|
<div class="back-to-top">
|
||||||
<a href="#">↑ Back to top</a>
|
<a href="#top">↑ Back to top</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="htmx-indicator">Searching…</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
document.body.addEventListener('click', function(e) {
|
|
||||||
var btn = e.target.closest('.paginate-btn');
|
|
||||||
if (!btn) return;
|
|
||||||
var q = btn.getAttribute('data-q');
|
|
||||||
var page = btn.getAttribute('data-page');
|
|
||||||
if (!q || !page) return;
|
|
||||||
var url = '/search?q=' + encodeURIComponent(q) + '&pageno=' + encodeURIComponent(page);
|
|
||||||
htmx.ajax(url, {target: '#urls', swap: 'innerHTML', select: '#urls'});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{{define "video_item"}}
|
{{define "video_item"}}
|
||||||
<article class="result video-result">
|
<article class="result video-result" data-engine="{{.Engine}}">
|
||||||
{{if .Thumbnail}}
|
{{if .Thumbnail}}
|
||||||
<div class="result_thumbnail">
|
<div class="result_thumbnail">
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">
|
||||||
|
|
@ -9,13 +9,16 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="result_content_wrapper">
|
<div class="result_content_wrapper">
|
||||||
<div class="result_header">
|
<div class="result_header">
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="result_url">
|
<div class="result_url">
|
||||||
<span class="engine-badge">youtube</span>
|
{{if .URL}}
|
||||||
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
||||||
|
{{end}}
|
||||||
|
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Content}}
|
{{if .Content}}
|
||||||
<p class="result_content">{{.Content}}</p>
|
<p class="result_content">{{.SafeContent}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue