move search to its own link with search screen

This commit is contained in:
Kyle Drake
2026-05-18 16:25:32 -05:00
parent aa7611289c
commit 845123b037
9 changed files with 401 additions and 155 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ def redirect_to_internet_archive_for_geocities_sites
end
end
WHITELISTED_POST_PATHS = ['/create_validate_all', '/create_validate', '/create'].freeze
WHITELISTED_POST_PATHS = ['/create_validate_all', '/create_validate', '/create', '/search', '/search/'].freeze
before do
if request.path.match /^\/api\//i
-74
View File
@@ -129,77 +129,3 @@ def browse_sites_dataset
ds
end
def daily_search_max?
query_count = $redis_cache.get('search_query_count').to_i
query_count >= $config['google_custom_search_query_limit']
end
get '/browse/search' do
@title = 'Site Search'
@description = 'Search websites hosted on Neocities.'
@daily_search_max_reached = daily_search_max?
if @daily_search_max_reached
params[:q] = nil
end
if !params[:q].blank?
created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400)
$redis_cache.incr('search_query_count') unless created
@start = params[:start].to_i
@start = 0 if @start < 0
@resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: {
key: $config['google_custom_search_key'],
cx: $config['google_custom_search_cx'],
safe: 'active',
start: @start,
q: Rack::Utils.escape(params[:q]) + ' -filetype:pdf -filetype:txt site:*.neocities.org'
})
@items = []
if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0"
@total_results = @resp['searchInformation']['totalResults'].to_i
@resp['items'].each do |item|
link = Addressable::URI.parse(item['link'])
path = link.path || '/'
unencoded_path = begin
Rack::Utils.unescape(Rack::Utils.unescape(path)) # Yes, it needs to be decoded twice
rescue ArgumentError
path # Fall back when the path includes invalid %-encoding
end
item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path
item['link'] = link
next if link.host == 'neocities.org'
username = link.host.split('.').first
site = Site[username: username]
next if site.nil? || site.is_deleted || site.is_nsfw
screenshot_path = unencoded_path
screenshot_path << 'index' if screenshot_path[-1] == '/'
['.html', '.htm'].each do |ext|
if site.screenshot_exists?(screenshot_path + ext, '540x405')
screenshot_path += ext
break
end
end
item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405')
@items << item
end
end
else
@items = nil
@total_results = 0
end
erb :'search'
end
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
def daily_search_max?
query_count = $redis_cache.get('search_query_count').to_i
query_count >= $config['google_custom_search_query_limit']
end
post '/search/?' do
query = params[:q].to_s.strip
redirect query.blank? ? '/search' : "/search?#{Rack::Utils.build_query(q: query)}"
end
get '/search/?' do
@query = params[:q].to_s.strip
@title = @query.blank? ? 'Neocities Search' : 'Site Search'
@description = 'Search websites hosted on Neocities.'
@daily_search_max_reached = daily_search_max?
if @daily_search_max_reached
@items = nil
@total_results = 0
elsif !@query.blank?
created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400)
$redis_cache.incr('search_query_count') unless created
@start = params[:start].to_i
@start = 0 if @start < 0
@resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: {
key: $config['google_custom_search_key'],
cx: $config['google_custom_search_cx'],
safe: 'active',
start: @start,
q: Rack::Utils.escape(@query) + ' -filetype:pdf -filetype:txt site:*.neocities.org'
})
@items = []
if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0"
@total_results = @resp['searchInformation']['totalResults'].to_i
@resp['items'].each do |item|
link = Addressable::URI.parse(item['link'])
path = link.path || '/'
unencoded_path = begin
Rack::Utils.unescape(Rack::Utils.unescape(path)) # Yes, it needs to be decoded twice
rescue ArgumentError
path # Fall back when the path includes invalid %-encoding
end
item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path
item['link'] = link
next if link.host == 'neocities.org'
username = link.host.split('.').first
site = Site[username: username]
next if site.nil? || site.is_deleted || site.is_nsfw
screenshot_path = unencoded_path
screenshot_path << 'index' if screenshot_path[-1] == '/'
['.html', '.htm'].each do |ext|
if site.screenshot_exists?(screenshot_path + ext, '540x405')
screenshot_path += ext
break
end
end
item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405')
@items << item
end
end
else
@items = nil
@total_results = 0
end
erb :'search'
end
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 65 KiB

+185
View File
@@ -2674,6 +2674,60 @@ a.tag:hover {
.browse-search-page .filter {
width: 100%;
}
.search-home-page {
align-items: center;
background:
radial-gradient(circle at 50% 20%, rgba(123, 202, 197, .32), rgba(123, 202, 197, 0) 260px),
linear-gradient(180deg, rgba(255,255,255,.8), rgba(255,255,255,0) 64%);
display: flex;
min-height: 535px;
padding: 54px 0 70px 0;
}
.search-home-content {
box-sizing: border-box;
padding-bottom: 0;
padding-top: 0;
width: 100%;
}
.search-home {
margin: 0 auto;
max-width: 720px;
text-align: center;
}
.search-mascot {
display: block;
filter: drop-shadow(0 10px 18px rgba(58, 71, 71, .13));
height: auto;
margin: 0 auto 12px auto;
max-width: 360px;
width: 66%;
}
.search-results-mascot-frame {
display: block;
height: 62px;
margin: 0 0 6px 0;
overflow: hidden;
position: relative;
width: 230px;
}
.search-results-mascot {
display: block;
filter: drop-shadow(0 4px 8px rgba(0,0,0,.18));
height: auto;
left: 50%;
max-width: none;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 230px;
z-index: 1;
}
.search-tagline {
color: #425c5a;
font-size: 1.28em;
margin: 0 0 28px 0;
text-shadow: 0 1px 0 rgba(255,255,255,.75);
}
.browse-search-form {
margin: 0;
@@ -2706,6 +2760,88 @@ a.tag:hover {
padding: 0 18px;
}
}
.search-home-form {
margin-left: auto;
margin-right: auto;
max-width: 650px;
fieldset {
display: grid;
gap: 18px;
justify-items: center;
}
}
.search-home-field {
position: relative;
width: 100%;
.fa {
color: #6a9a96;
font-size: 1.25em;
left: 19px;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.input-Area {
background: rgba(255,255,255,.94);
border: 2px solid rgba(104, 184, 178, .6);
border-radius: 30px;
box-shadow: 0 8px 24px rgba(33, 64, 61, .12), 0 1px 0 rgba(255,255,255,.95) inset;
box-sizing: border-box;
color: #333;
font-size: 1.2em;
height: 56px!important;
line-height: 24px;
margin: 0;
padding: 12px 18px 12px 52px;
width: 100%;
&:focus {
border-color: #42b7ae;
box-shadow: 0 10px 28px rgba(33, 64, 61, .16), 0 0 0 3px rgba(66, 183, 174, .18);
}
}
}
.search-home-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
.btn-Action {
height: 42px;
line-height: 42px;
margin: 0;
min-width: 152px;
padding: 0 18px;
}
.btn-Secondary {
background: #f7fbfa;
border: 1px solid rgba(88, 163, 157, .55);
box-shadow: 0 2px 5px rgba(0,0,0,.08);
color: #3e6864!important;
text-shadow: none;
&:hover,
&:focus {
background: #e8f4f2;
border-color: rgba(66, 183, 174, .85);
color: #284f4b!important;
}
&:visited {
color: #3e6864!important;
}
}
}
.search-message {
font-size: 1.1em;
margin: 0;
}
.browse-search-page .search-results {
background: #fff;
}
@@ -2780,22 +2916,66 @@ a.tag:hover {
}
.browse-search-page .row.content {
box-sizing: border-box;
padding-bottom: 16px;
padding-left: 5.4%;
padding-right: 5.4%;
padding-top: 14px;
}
.interior .header-Outro.with-columns.browse-search-page .col {
box-sizing: border-box;
padding-left: 0;
padding-right: 0;
}
.browse-search-page h1 {
font-size: 2.6em;
line-height: 1.05;
margin-bottom: 16px;
}
.search-home-page {
min-height: 390px;
padding-bottom: 44px;
padding-top: 44px;
}
.search-mascot {
max-width: 280px;
width: 82%;
}
.search-results-mascot-frame {
height: 56px;
margin: 0 auto 6px auto;
width: 205px;
}
.search-results-mascot {
width: 205px;
}
.search-tagline {
font-size: 1.05em;
margin-bottom: 18px;
}
.search-home-field .input-Area {
height: 50px!important;
padding-left: 46px;
}
.browse-search-form {
width: 100%;
fieldset {
align-items: stretch;
box-sizing: border-box;
display: grid;
gap: 10px;
justify-items: center;
width: 100%;
}
.input-Area {
@@ -2821,6 +3001,11 @@ a.tag:hover {
}
}
.search-home-form .search-home-field .input-Area {
height: 50px!important;
padding-left: 46px;
}
.browse-search-page .result-item {
gap: 10px;
grid-template-columns: 1fr;
+56
View File
@@ -0,0 +1,56 @@
# frozen_string_literal: true
require_relative './environment.rb'
describe '/search' do
include Capybara::DSL
include Capybara::Minitest::Assertions
include Rack::Test::Methods
def app
Sinatra::Application
end
before do
Capybara.default_driver = :rack_test
Capybara.reset_sessions!
$redis_cache.del('search_query_count')
end
it 'shows a playful standalone search page' do
visit '/search'
_(page).must_have_selector 'img.search-mascot[src="/img/hotcat.svg"][alt="Hot Cat"]'
_(page).must_have_content 'Search the handmade web'
_(page).must_have_selector 'form[action="/search"] input[name="q"]'
_(page).must_have_link 'Random Site', href: '/browse?sort_by=random'
end
it 'links to search immediately after websites in the top nav' do
visit '/search'
nav_links = all('.constant-Nav a').map(&:text)
_(nav_links[nav_links.index('Websites') + 1]).must_equal 'Search'
end
it 'does not mount the old browse search path' do
visit '/browse/search'
_(page.status_code).must_equal 404
end
it 'redirects post search submissions to the get search page' do
post '/search', q: 'weird web'
_(last_response.status).must_equal 302
_(last_response.headers['Location']).must_equal 'http://example.org/search?q=weird+web'
end
it 'shows the mascot on the results header' do
$redis_cache.set('search_query_count', $config['google_custom_search_query_limit'], ex: 86400)
visit '/search?q=web'
_(page).must_have_selector '.header-Outro img.search-results-mascot[src="/img/hotcat.svg"][alt="Hot Cat"]'
_(page).wont_have_selector '.header-Outro h1', text: 'Site Search'
end
end
+3
View File
@@ -34,6 +34,9 @@
});
</script>
</li>
<li>
<a href="/search">Search</a>
</li>
<% unless is_education? %>
<li>
<a href="/activity">Activity</a>
-30
View File
@@ -154,41 +154,11 @@
<% end %>
<% if params[:sort_by] != 'moderation' %>
<% unless is_education? %>
<div class="row content misc txt-Center">
<h3>Site Search</h3>
<% if !daily_search_max? %>
<form method="GET" action="/browse/search">
<fieldset>
<input name="q" type="text" placeholder="keywords" class="input-Area" autocapitalize="off" autocorrect="off" style="width: 300px;">
<input class="btn btn-Action" type="submit" value="Search">
</fieldset>
</form>
<% else %>
<form id="searchForm" method="GET" action="https://duckduckgo.com" class="content" onsubmit="return addSiteToSearch()">
<fieldset>
<input id="searchQuery" name="q" type="text" placeholder="keywords" class="input-Area" autocapitalize="off" autocorrect="off" value="<%= flash[:username] %>" style="width: 50%">
<input class="btn btn-Action" type="submit" value="Search">
</fieldset>
</form>
<p>Search powered by <a href="https://duckduckgo.com/">Duck Duck Go</a></p>
<% end %>
</div>
<%== erb :_browse_tags, layout: false %>
<% end %>
<% end %>
</div>
<script>
function addSiteToSearch() {
var searchQuery = $('#searchQuery')
var finalSearchQuery = searchQuery.val() + ' site:neocities.org'
window.location = 'https://duckduckgo.com/?q='+encodeURI(finalSearchQuery)
return false
}
</script>
<% if signed_in? && current_site.is_admin %>
<script>
function banSite(usernames, classifier, el) {
+75 -50
View File
@@ -1,58 +1,83 @@
<div class="header-Outro with-columns browse-page browse-search-page">
<div class="row content">
<div class="col col-100">
<h1>Site Search</h1>
</div>
<% if @items.nil? && !@daily_search_max_reached %>
<div class="browse-search-page search-home-page">
<div class="content search-home-content">
<div class="search-home">
<h1 class="hidden">Neocities Search</h1>
<img class="search-mascot" src="/img/hotcat.svg" alt="Hot Cat">
<p class="search-tagline">Search the handmade web</p>
<div class="col col-100 filter">
<form id="search_criteria" class="browse-search-form" action="/browse/search" method="GET">
<fieldset class="grouping">
<input name="q" type="text" class="input-Area" value="<%= params[:q] %>" placeholder="keywords">
<button type="submit" class="btn-Action">Search</button>
</fieldset>
</form>
<form id="search_criteria" class="browse-search-form search-home-form" action="/search" method="GET">
<fieldset class="grouping">
<div class="search-home-field">
<i class="fa fa-search" aria-hidden="true"></i>
<input name="q" type="text" class="input-Area" value="<%= Rack::Utils.escape_html(@query) %>" placeholder="keywords" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" autofocus>
</div>
<div class="search-home-actions">
<button type="submit" class="btn-Action">Search</button>
<a href="/browse?sort_by=random" class="btn-Action btn-Secondary">Random Site</a>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
<% else %>
<div class="header-Outro with-columns browse-page browse-search-page">
<div class="row content">
<div class="col col-100">
<span class="search-results-mascot-frame">
<img class="search-results-mascot" src="/img/hotcat.svg" alt="Hot Cat">
</span>
</div>
<div class="content single-Col misc-page browse-search-page">
<% if @daily_search_max_reached %>
Search temporarily unavailable, please try again tomorrow.
<% elsif @items == [] %>
No results.
<% elsif !@items.nil? %>
<div class="search-results">
<% @items.each do |item| %>
<div class="result-item">
<a class="result-screenshot" href="<%= item['link'] %>"><img src="<%= item['screenshot_url'] %>" alt="<%= item['title'] %>"></a>
<div class="result-details">
<h3 class="result-title">
<a href="<%= item['link'] %>"><%= item['title'] %></a>
</h3>
<div class="col col-100 filter">
<form id="search_criteria" class="browse-search-form" action="/search" method="GET">
<fieldset class="grouping">
<input name="q" type="text" class="input-Area" value="<%= Rack::Utils.escape_html(@query) %>" placeholder="keywords" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false">
<button type="submit" class="btn-Action">Search</button>
</fieldset>
</form>
</div>
</div>
</div>
<div class="result-url">
<a href="<%= item['link'] %>"><%= item['unencoded_link'] %></a>
<div class="content single-Col misc-page browse-search-page">
<% if @daily_search_max_reached %>
<p class="search-message">Search temporarily unavailable, please try again tomorrow.</p>
<% elsif @items == [] %>
<p class="search-message">No results.</p>
<% elsif !@items.nil? %>
<div class="search-results">
<% @items.each do |item| %>
<div class="result-item">
<a class="result-screenshot" href="<%= item['link'] %>"><img src="<%= item['screenshot_url'] %>" alt="<%= item['title'] %>"></a>
<div class="result-details">
<h3 class="result-title">
<a href="<%= item['link'] %>"><%= item['title'] %></a>
</h3>
<div class="result-url">
<a href="<%= item['link'] %>"><%= item['unencoded_link'] %></a>
</div>
<p class="result-snippet">
<%== item['htmlSnippet'] %>
</p>
</div>
<p class="result-snippet">
<%== item['htmlSnippet'] %>
</p>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<div class="txt-Center">
<h2>
<% if @start > 0 %>
<a href="?q=<%= Rack::Utils.escape params[:q] %>&start=<%= [@start-10, 0].max %>"><i class="fa fa-arrow-left arrow">&nbsp;&nbsp;</i></a>
<% end %>
<% if @total_results > @start+10 && @start+10 < 100 %>
<a href="?q=<%= Rack::Utils.escape params[:q] %>&start=<%= @start+10 %>"><i class="fa fa-arrow-right arrow"></i></a>
<% end %>
</h2>
</div>
<% else %>
Enter some keywords to begin searching.
<% end %>
</div>
<div class="txt-Center">
<h2>
<% if @start > 0 %>
<a href="?q=<%= Rack::Utils.escape @query %>&start=<%= [@start-10, 0].max %>"><i class="fa fa-arrow-left arrow">&nbsp;&nbsp;</i></a>
<% end %>
<% if @total_results > @start+10 && @start+10 < 100 %>
<a href="?q=<%= Rack::Utils.escape @query %>&start=<%= @start+10 %>"><i class="fa fa-arrow-right arrow"></i></a>
<% end %>
</h2>
</div>
<% end %>
</div>
<% end %>