Add screenshots

This commit is contained in:
2026-03-07 11:23:28 +11:00
parent 14b30bdb6e
commit f20b7f5fce
6 changed files with 36 additions and 42 deletions
+26 -33
View File
@@ -1,8 +1,30 @@
# HeySearch # HeySearch
A privacy-respecting [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine) that aggregates results from multiple search engines. A privacy-respecting [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine) that aggregates results from multiple search engines — with a modern UI designed for humans and AI agents alike.
Inspired by [SearXNG](https://github.com/searxng/searxng). > Inspired by [SearXNG](https://github.com/searxng/searxng), built for the modern web.
## Screenshots
| Home | Web Search | Image Search |
|------|-----------|--------------|
| [![Home page](screenshots/home-page.png)](screenshots/home-page.png) | [![Web search results](screenshots/text-search.png)](screenshots/text-search.png) | [![Image search with lightbox](screenshots/image-search.png)](screenshots/image-search.png) |
## Why HeySearch over SearXNG?
Both are open-source, self-hosted, privacy-respecting metasearch engines. Here's why HeySearch is the better choice for most people:
| | HeySearch | SearXNG |
|---|---|---|
| **Setup** | `docker run -p 8000:8000 ghcr.io/…/hey-search` — one command, zero config | Requires YAML config, engine tuning, often breaks out of the box |
| **UI** | Modern, clean React UI with dark mode, background images, image lightbox | Functional but dated — not optimised for mobile or daily use |
| **AI agent friendly** | Clean JSON REST API, OpenAPI docs at `/docs`, designed to be queried programmatically | API exists but less documented; HTML-heavy responses |
| **Bookmarks** | Built-in bookmark manager for results | ❌ |
| **Search history** | Full search history with timestamps, re-run any past query in one click | ❌ |
| **Usage stats** | Built-in analytics dashboard — top queries, click-through rates, engine usage | ❌ |
| **Background gallery** | Beautiful Unsplash/Picsum backgrounds on the home page | ❌ |
**TL;DR** — If you want something you can run in 30 seconds, looks great, works well on your phone, and exposes a clean API for your AI tools, HeySearch is for you. If you need 70+ search engines and deep customisation, SearXNG has the edge.
## Architecture ## Architecture
@@ -19,36 +41,7 @@ Inspired by [SearXNG](https://github.com/searxng/searxng).
## Running Locally ## Running Locally
### 1. Backend ### 1. Quick start
```bash
cd backend
uv sync # install dependencies
uv run uvicorn app.main:app --reload --port 8000
```
To enable Redis caching:
```bash
REDIS_URL=redis://192.168.1.2:6399 uv run uvicorn app.main:app --reload --port 8000
```
The API will be available at `http://localhost:8000`. Interactive docs:
- Swagger UI: http://localhost:8000/docs
- Redoc: http://localhost:8000/redoc
### 2. Frontend
```bash
cd frontend
npm install # install dependencies
npm run dev # start dev server with hot reload
```
The frontend dev server runs at `http://localhost:5173` and proxies `/api` requests to the backend.
### 3. Quick start (both)
```bash ```bash
# Without Redis (caching disabled) # Without Redis (caching disabled)
@@ -58,7 +51,7 @@ just dev
REDIS_URL=redis://192.168.1.2:6399 just dev REDIS_URL=redis://192.168.1.2:6399 just dev
``` ```
### 4. Docker (production) ### 2. Docker (production)
```bash ```bash
docker build -t hey-search . docker build -t hey-search .
+8 -8
View File
@@ -557,7 +557,7 @@ function App() {
{/* Category / filter bar */} {/* Category / filter bar */}
<div className="sticky top-[57px] z-30 border-b bg-background/95 backdrop-blur"> <div className="sticky top-[57px] z-30 border-b bg-background/95 backdrop-blur">
<nav aria-label="Search categories" className="flex items-center gap-1 px-4 py-2"> <nav aria-label="Search categories" className="flex items-center gap-1 px-4 py-2 overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
{([ {([
{ key: "web" as const, label: "Web", icon: Globe }, { key: "web" as const, label: "Web", icon: Globe },
{ key: "images" as const, label: "Images", icon: ImageIcon }, { key: "images" as const, label: "Images", icon: ImageIcon },
@@ -567,7 +567,7 @@ function App() {
onClick={() => handleCategoryChange(key)} onClick={() => handleCategoryChange(key)}
aria-current={category === key ? "page" : undefined} aria-current={category === key ? "page" : undefined}
className={cn( className={cn(
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", "shrink-0 flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
category === key category === key
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent" : "text-muted-foreground hover:bg-accent"
@@ -581,15 +581,15 @@ function App() {
{/* Image size filter — only visible in images category */} {/* Image size filter — only visible in images category */}
{category === "images" && ( {category === "images" && (
<> <>
<div className="mx-2 h-5 w-px bg-border" aria-hidden="true" /> <div className="mx-2 h-5 w-px shrink-0 bg-border" aria-hidden="true" />
<SlidersHorizontal className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" /> <SlidersHorizontal className="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
{IMAGE_SIZE_OPTIONS.map(({ value, label }) => ( {IMAGE_SIZE_OPTIONS.map(({ value, label }) => (
<button <button
key={value} key={value}
onClick={() => handleImageSizeChange(value)} onClick={() => handleImageSizeChange(value)}
aria-pressed={imageSize === value} aria-pressed={imageSize === value}
className={cn( className={cn(
"rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", "shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
imageSize === value imageSize === value
? "bg-secondary text-secondary-foreground" ? "bg-secondary text-secondary-foreground"
: "text-muted-foreground hover:bg-accent" : "text-muted-foreground hover:bg-accent"
@@ -603,15 +603,15 @@ function App() {
{/* Sort order — available for both web and images */} {/* Sort order — available for both web and images */}
<> <>
<div className="mx-2 h-5 w-px bg-border" aria-hidden="true" /> <div className="mx-2 h-5 w-px shrink-0 bg-border" aria-hidden="true" />
<span className="text-xs text-muted-foreground" aria-hidden="true">Sort:</span> <span className="shrink-0 text-xs text-muted-foreground" aria-hidden="true">Sort:</span>
{SORT_OPTIONS.map(({ value, label }) => ( {SORT_OPTIONS.map(({ value, label }) => (
<button <button
key={value} key={value}
onClick={() => handleSortChange(value)} onClick={() => handleSortChange(value)}
aria-pressed={sortOrder === value} aria-pressed={sortOrder === value}
className={cn( className={cn(
"rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", "shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
sortOrder === value sortOrder === value
? "bg-secondary text-secondary-foreground" ? "bg-secondary text-secondary-foreground"
: "text-muted-foreground hover:bg-accent" : "text-muted-foreground hover:bg-accent"
+2 -1
View File
@@ -106,7 +106,8 @@ export function AppHeader({
className="shrink-0 text-xl font-bold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded" className="shrink-0 text-xl font-bold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded"
style={{ color: "#27ae60" }} style={{ color: "#27ae60" }}
> >
HeySearch <span className="sm:hidden">HS</span>
<span className="hidden sm:inline">HeySearch</span>
</button> </button>
)} )}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB