mirror of
https://git.lolcat.ca/lolcat/4get.git
synced 2024-11-08 17:43:07 -05:00
added autocomplete
This commit is contained in:
parent
71a61304b0
commit
edc42ea35d
9 changed files with 594 additions and 53 deletions
225
api/v1/ac.php
Normal file
225
api/v1/ac.php
Normal file
|
@ -0,0 +1,225 @@
|
|||
<?php
|
||||
|
||||
new autocomplete();
|
||||
|
||||
class autocomplete{
|
||||
|
||||
public function __construct(){
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
$this->scrapers = [
|
||||
"brave" => "https://search.brave.com/api/suggest?q={searchTerms}",
|
||||
"ddg" => "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
|
||||
"yandex" => "https://suggest.yandex.com/suggest-ff.cgi?part={searchTerms}&uil=en&v=3&sn=5&lr=21276&yu=4861394161661655015",
|
||||
"google" => "https://www.google.com/complete/search?client=mobile-gws-lite&q={searchTerms}",
|
||||
"qwant" => "https://api.qwant.com/v3/suggest/?q={searchTerms}&client=opensearch",
|
||||
"yep" => "https://api.yep.com/ac/?query={searchTerms}",
|
||||
"marginalia" => "https://search.marginalia.nu/suggest/?partial={searchTerms}",
|
||||
"yt" => "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q={searchTerms}",
|
||||
"sc" => "https://api-v2.soundcloud.com/search/queries?q={searchTerms}&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=10&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en"
|
||||
];
|
||||
|
||||
/*
|
||||
Sanitize input
|
||||
*/
|
||||
if(!isset($_GET["s"])){
|
||||
|
||||
$this->do404("Missing search(s) parameter");
|
||||
}
|
||||
|
||||
if(is_string($_GET["s"]) === false){
|
||||
|
||||
$this->do404("Invalid search(s) parameter");
|
||||
}
|
||||
|
||||
if(strlen($_GET["s"]) > 500){
|
||||
|
||||
$this->do404("Search(s) exceeds the 500 char length");
|
||||
}
|
||||
|
||||
if(
|
||||
isset($_GET["scraper"]) &&
|
||||
is_string($_GET["scraper"]) === false
|
||||
){
|
||||
|
||||
$_GET["scraper"] = "brave"; // default option
|
||||
}
|
||||
|
||||
/*
|
||||
Get $scraper
|
||||
*/
|
||||
if(!isset($_GET["scraper"])){
|
||||
|
||||
if(isset($_COOKIE["scraper_ac"])){
|
||||
|
||||
$scraper = $_COOKIE["scraper_ac"];
|
||||
}else{
|
||||
|
||||
$scraper = "brave"; // default option
|
||||
}
|
||||
}else{
|
||||
|
||||
$scraper = $_GET["scraper"];
|
||||
}
|
||||
|
||||
if($scraper == "disabled"){
|
||||
|
||||
// this shouldnt happen, but let's handle it anyways
|
||||
$this->doempty();
|
||||
}
|
||||
|
||||
// make sure it exists
|
||||
if(!isset($this->scrapers[$scraper])){
|
||||
|
||||
$scraper = "brave"; // default option
|
||||
}
|
||||
|
||||
// return results
|
||||
|
||||
switch($scraper){
|
||||
|
||||
case "google":
|
||||
case "yt":
|
||||
// handle google cause they want to be a special snowflake :(
|
||||
$js = $this->get($this->scrapers[$scraper], $_GET["s"]);
|
||||
|
||||
preg_match(
|
||||
'/\((\[.*\])\)/',
|
||||
$js,
|
||||
$js
|
||||
);
|
||||
|
||||
if(!isset($js[1])){
|
||||
|
||||
$this->doempty();
|
||||
}
|
||||
|
||||
$js = json_decode($js[1]);
|
||||
$json = [];
|
||||
|
||||
foreach($js[1] as $item){
|
||||
|
||||
$json[] = strip_tags($item[0]);
|
||||
}
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
$_GET["s"],
|
||||
$json
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
case "sc":
|
||||
// soundcloud
|
||||
$js = $this->get($this->scrapers[$scraper], $_GET["s"]);
|
||||
|
||||
$js = json_decode($js, true);
|
||||
|
||||
if(!isset($js["collection"])){
|
||||
|
||||
$this->doempty();
|
||||
}
|
||||
|
||||
$json = [];
|
||||
foreach($js["collection"] as $item){
|
||||
|
||||
$json[] = $item["query"];
|
||||
}
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
$_GET["s"],
|
||||
$json
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
case "marginalia":
|
||||
$json = $this->get($this->scrapers[$scraper], $_GET["s"]);
|
||||
|
||||
$json = json_decode($json, true);
|
||||
if($json === null){
|
||||
|
||||
|
||||
$this->doempty();
|
||||
}
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
$_GET["s"],
|
||||
$json
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// if it respects the openSearch protocol
|
||||
$json = json_decode($this->get($this->scrapers[$scraper], $_GET["s"]), true);
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
$_GET["s"],
|
||||
$json[1] // ensure it contains valid key 0
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function get($url, $query){
|
||||
|
||||
$curlproc = curl_init();
|
||||
|
||||
$url = str_replace("{searchTerms}", urlencode($query), $url);
|
||||
|
||||
curl_setopt($curlproc, CURLOPT_URL, $url);
|
||||
|
||||
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
|
||||
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
|
||||
["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
|
||||
"Accept: application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language: en-US,en;q=0.5",
|
||||
"Accept-Encoding: gzip",
|
||||
"DNT: 1",
|
||||
"Connection: keep-alive",
|
||||
"Sec-Fetch-Dest: empty",
|
||||
"Sec-Fetch-Mode: cors",
|
||||
"Sec-Fetch-Site: same-site"]
|
||||
);
|
||||
|
||||
curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
|
||||
curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$data = curl_exec($curlproc);
|
||||
|
||||
if(curl_errno($curlproc)){
|
||||
|
||||
throw new Exception(curl_error($curlproc));
|
||||
}
|
||||
|
||||
curl_close($curlproc);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function do404($error){
|
||||
|
||||
echo json_encode(["error" => $error]);
|
||||
die();
|
||||
}
|
||||
|
||||
private function doempty(){
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
$_GET["s"],
|
||||
[]
|
||||
]
|
||||
);
|
||||
die();
|
||||
}
|
||||
}
|
|
@ -574,8 +574,6 @@ class brave{
|
|||
}
|
||||
}
|
||||
|
||||
echo "test";
|
||||
|
||||
if($rating !== null){
|
||||
|
||||
$table["Rating"] = $rating;
|
||||
|
|
|
@ -1616,6 +1616,7 @@ class google{
|
|||
$imgvl
|
||||
);
|
||||
|
||||
if(isset($imgvl[1])){
|
||||
$imgvl = $imgvl[1];
|
||||
|
||||
$params["async"] = "_id:islrg_c,_fmt:html";
|
||||
|
@ -1633,6 +1634,7 @@ class google{
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
|
|
@ -288,7 +288,7 @@ class sc{
|
|||
|
||||
if(count($description) != 0){
|
||||
|
||||
$description = $count . " songs. " . implode(", ", $description);
|
||||
$description = trim($count . " songs. " . implode(", ", $description));
|
||||
}
|
||||
|
||||
if(
|
||||
|
@ -320,7 +320,7 @@ class sc{
|
|||
|
||||
$out["playlist"][] = [
|
||||
"title" => $item["title"],
|
||||
"description" => $description,
|
||||
"description" => $this->limitstrlen($description),
|
||||
"author" => [
|
||||
"name" => $item["user"]["username"],
|
||||
"url" => $item["user"]["permalink_url"],
|
||||
|
@ -385,13 +385,14 @@ class sc{
|
|||
"\n",
|
||||
wordwrap(
|
||||
str_replace(
|
||||
"\n",
|
||||
["\n\r", "\r\n", "\n", "\r"],
|
||||
" ",
|
||||
$text
|
||||
),
|
||||
300,
|
||||
"\n"
|
||||
)
|
||||
),
|
||||
2
|
||||
)[0];
|
||||
}
|
||||
}
|
||||
|
|
75
settings.php
75
settings.php
|
@ -58,6 +58,56 @@ $settings = [
|
|||
[
|
||||
"name" => "Scrapers to use",
|
||||
"settings" => [
|
||||
[
|
||||
"description" => "Autocomplete<br><i>Picking <div class=\"code-inline\">Auto</div> changes the source dynamically depending of the page's scraper<br>Picking <div class=\"code-inline\">Disabled</div> disables this feature</i>",
|
||||
"parameter" => "scraper_ac",
|
||||
"options" => [
|
||||
[
|
||||
"value" => "disabled",
|
||||
"text" => "Disabled"
|
||||
],
|
||||
[
|
||||
"value" => "auto",
|
||||
"text" => "Auto"
|
||||
],
|
||||
[
|
||||
"value" => "brave",
|
||||
"text" => "Brave"
|
||||
],
|
||||
[
|
||||
"value" => "ddg",
|
||||
"text" => "DuckDuckGo"
|
||||
],
|
||||
[
|
||||
"value" => "yandex",
|
||||
"text" => "Yandex"
|
||||
],
|
||||
[
|
||||
"value" => "google",
|
||||
"text" => "Google"
|
||||
],
|
||||
[
|
||||
"value" => "qwant",
|
||||
"text" => "Qwant"
|
||||
],
|
||||
[
|
||||
"value" => "yep",
|
||||
"text" => "Yep"
|
||||
],
|
||||
[
|
||||
"value" => "marginalia",
|
||||
"text" => "Marginalia"
|
||||
],
|
||||
[
|
||||
"value" => "yt",
|
||||
"text" => "YouTube"
|
||||
],
|
||||
[
|
||||
"value" => "sc",
|
||||
"text" => "SoundCloud"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"description" => "Web",
|
||||
"parameter" => "scraper_web",
|
||||
|
@ -183,8 +233,13 @@ $settings = [
|
|||
if($_POST){
|
||||
|
||||
$loop = &$_POST;
|
||||
}else{
|
||||
}elseif(count($_GET) !== 0){
|
||||
|
||||
// redirect user to front page
|
||||
$loop = &$_GET;
|
||||
header("Location: /");
|
||||
|
||||
}else{
|
||||
// refresh cookie dates
|
||||
$loop = &$_COOKIE;
|
||||
}
|
||||
|
@ -245,7 +300,7 @@ echo
|
|||
'<head>' .
|
||||
'<meta http-equiv="Content-Type" content="text/html;charset=utf-8">' .
|
||||
'<title>Settings</title>' .
|
||||
'<link rel="stylesheet" href="/static/style.css?v3">' .
|
||||
'<link rel="stylesheet" href="/static/style.css?v4">' .
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1">' .
|
||||
'<meta name="robots" content="index,follow">' .
|
||||
'<link rel="icon" type="image/x-icon" href="/favicon.ico">' .
|
||||
|
@ -260,14 +315,14 @@ $left =
|
|||
'By clicking <div class="code-inline">Update settings!</div>, a plaintext <div class="code-inline">key=value</div> cookie will be stored on your browser. When selecting a default setting, the parameter is removed from your cookies.';
|
||||
|
||||
$c = count($_COOKIE);
|
||||
$code = "";
|
||||
|
||||
if($c !== 0){
|
||||
|
||||
$left .=
|
||||
'<br><br>Your current cookie looks like this:' .
|
||||
'<div class="code">';
|
||||
|
||||
$code = "";
|
||||
|
||||
$ca = 0;
|
||||
foreach($_COOKIE as $key => $value){
|
||||
|
||||
|
@ -326,17 +381,23 @@ $left .=
|
|||
'</div>' .
|
||||
'<div class="settings-submit">' .
|
||||
'<input type="submit" value="Update settings!">' .
|
||||
'<a href="../">< Return to main page</a>' .
|
||||
'<a href="../">< Return to front page</a>' .
|
||||
'</div>' .
|
||||
'</form>';
|
||||
|
||||
echo
|
||||
if(count($_GET) === 0){
|
||||
|
||||
echo
|
||||
$frontend->load(
|
||||
"search.html",
|
||||
[
|
||||
"class" => "",
|
||||
"right-left" => "",
|
||||
"right-left" =>
|
||||
'<div class="infobox"><h2>Preference link</h2>Follow this link to auto-apply all cookies. Useful if your browser clears out cookies after a browsing session. Following this link will redirect you to the front page, unless no settings are set.<br><br>' .
|
||||
'<a href="settings' . rtrim("?" . str_replace("; ", "&", $code), "?") . '">Bookmark me!</a>' .
|
||||
'</div>',
|
||||
"right-right" => "",
|
||||
"left" => $left
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
264
static/client.js
264
static/client.js
|
@ -660,15 +660,16 @@ function changeimage(event){
|
|||
centerpopup();
|
||||
}
|
||||
|
||||
/*
|
||||
Shortcuts
|
||||
*/
|
||||
var searchbox_wrapper = document.getElementsByClassName("searchbox");
|
||||
|
||||
if(searchbox_wrapper.length !== 0){
|
||||
|
||||
searchbox_wrapper = searchbox_wrapper[0];
|
||||
var searchbox = searchbox_wrapper.getElementsByTagName("input")[1];
|
||||
|
||||
/*
|
||||
Textarea shortcuts
|
||||
*/
|
||||
document.addEventListener("keydown", function(key){
|
||||
|
||||
switch(key.keyCode){
|
||||
|
@ -695,4 +696,261 @@ if(searchbox_wrapper.length !== 0){
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Autocompleter
|
||||
*/
|
||||
if( // make sure the user wants it
|
||||
document.cookie.includes("scraper_ac=") &&
|
||||
document.cookie.includes("scraper_ac=disabled") === false
|
||||
){
|
||||
|
||||
var autocomplete_cache = [];
|
||||
var focuspos = -1;
|
||||
var list = [];
|
||||
var autocomplete_div = document.getElementsByClassName("autocomplete")[0];
|
||||
|
||||
if(
|
||||
document.cookie.includes("scraper_ac=auto") &&
|
||||
typeof scraper_dropdown != "undefined"
|
||||
){
|
||||
|
||||
var ac_req_appendix = "&scraper=" + scraper_dropdown.value;
|
||||
}else{
|
||||
|
||||
var ac_req_appendix = "";
|
||||
}
|
||||
|
||||
function getsearchboxtext(){
|
||||
|
||||
var value =
|
||||
searchbox.value
|
||||
.trim()
|
||||
.replace(
|
||||
/ +/g,
|
||||
" "
|
||||
)
|
||||
.toLowerCase();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
searchbox.addEventListener("input", async function(){
|
||||
|
||||
// ratelimit on input only
|
||||
// dont ratelimit if we already have res
|
||||
if(typeof autocomplete_cache[getsearchboxtext()] != "undefined"){
|
||||
|
||||
await getac();
|
||||
}else{
|
||||
|
||||
await getac_ratelimit();
|
||||
}
|
||||
});
|
||||
|
||||
async function getac(){
|
||||
|
||||
var curvalue = getsearchboxtext();
|
||||
|
||||
if(curvalue == ""){
|
||||
|
||||
// hide autocompleter
|
||||
autocomplete_div.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof autocomplete_cache[curvalue] == "undefined"){
|
||||
|
||||
/*
|
||||
Fetch autocomplete
|
||||
*/
|
||||
// make sure we dont fetch same thing twice
|
||||
autocomplete_cache[curvalue] = [];
|
||||
|
||||
var res = await fetch("/api/v1/ac?s=" + encodeURIComponent(curvalue) + ac_req_appendix);
|
||||
var json = await res.json();
|
||||
|
||||
autocomplete_cache[curvalue] = json[1];
|
||||
|
||||
if(curvalue == getsearchboxtext()){
|
||||
|
||||
render_ac(curvalue, autocomplete_cache[curvalue]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
render_ac(curvalue, autocomplete_cache[curvalue]);
|
||||
}
|
||||
|
||||
var ac_func = null;
|
||||
function getac_ratelimit(){
|
||||
|
||||
return new Promise(async function(resolve, reject){
|
||||
|
||||
if(ac_func !== null){
|
||||
|
||||
clearTimeout(ac_func);
|
||||
}//else{
|
||||
|
||||
// no ratelimits
|
||||
//getac();
|
||||
//}
|
||||
|
||||
ac_func =
|
||||
setTimeout(function(){
|
||||
|
||||
ac_func = null;
|
||||
getac(); // get results after 100ms of no keystroke
|
||||
resolve();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
function render_ac(query, list){
|
||||
|
||||
if(list.length === 0){
|
||||
|
||||
autocomplete_div.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
html = "";
|
||||
|
||||
// prepare regex
|
||||
var highlight = query.split(" ");
|
||||
var regex = [];
|
||||
|
||||
for(var k=0; k<highlight.length; k++){
|
||||
|
||||
// espace regex
|
||||
regex.push(
|
||||
highlight[k].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
);
|
||||
}
|
||||
|
||||
regex = new RegExp(highlight.join("|"), "gi");
|
||||
|
||||
for(var i=0; i<list.length; i++){
|
||||
|
||||
html +=
|
||||
'<div tabindex="0" class="entry" onclick="handle_entry_click(this);">' +
|
||||
htmlspecialchars(
|
||||
list[i]
|
||||
).replace(
|
||||
regex,
|
||||
'<u>$&</u>'
|
||||
) +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
autocomplete_div.innerHTML = html;
|
||||
autocomplete_div.style.display = "block";
|
||||
}
|
||||
|
||||
var should_focus = false;
|
||||
document.addEventListener("keydown", function(event){
|
||||
|
||||
if(event.key == "Escape"){
|
||||
|
||||
document.activeElement.blur();
|
||||
focuspos = -1;
|
||||
autocomplete_div.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if(
|
||||
is_click_within(event.target, "searchbox") === false ||
|
||||
typeof autocomplete_cache[getsearchboxtext()] == "undefined"
|
||||
){
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch(event.key){
|
||||
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
focuspos--;
|
||||
if(focuspos === -2){
|
||||
|
||||
focuspos = autocomplete_cache[getsearchboxtext()].length - 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
case "Tab":
|
||||
event.preventDefault();
|
||||
|
||||
focuspos++;
|
||||
if(focuspos >= autocomplete_cache[getsearchboxtext()].length){
|
||||
|
||||
focuspos = -1;
|
||||
}
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
should_focus = true;
|
||||
|
||||
if(focuspos !== -1){
|
||||
|
||||
// replace input content
|
||||
event.preventDefault();
|
||||
searchbox.value =
|
||||
autocomplete_div.getElementsByClassName("entry")[focuspos].innerText;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
focuspos = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if(focuspos === -1){
|
||||
|
||||
searchbox.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
autocomplete_div.getElementsByClassName("entry")[focuspos].focus();
|
||||
});
|
||||
|
||||
window.addEventListener("blur", function(){
|
||||
|
||||
autocomplete_div.style.display = "none";
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", function(event){
|
||||
|
||||
// handle ENTER key on entry
|
||||
if(should_focus){
|
||||
|
||||
should_focus = false;
|
||||
searchbox.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousedown", function(event){
|
||||
|
||||
// hide input if click is outside
|
||||
if(is_click_within(event.target, "searchbox") === false){
|
||||
|
||||
autocomplete_div.style.display = "none";
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
function handle_entry_click(event){
|
||||
|
||||
searchbox.value = event.innerText;
|
||||
focuspos = -1;
|
||||
searchbox.focus();
|
||||
}
|
||||
|
||||
searchbox.addEventListener("focus", function(){
|
||||
|
||||
focuspos = -1;
|
||||
getac();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,31 +149,27 @@ h3,h4,h5,h6{
|
|||
left:-1px;
|
||||
right:-1px;
|
||||
background:var(--282828);
|
||||
border:1px solid var(--504945);
|
||||
border:1px solid var(--928374);
|
||||
border-top:none;
|
||||
border-radius:0 0 2px 2px;
|
||||
z-index:10;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.autocomplete .entry{
|
||||
overflow:hidden;
|
||||
padding:4px 10px;
|
||||
cursor:pointer;
|
||||
outline:none;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
.autocomplete .entry:hover{
|
||||
background:var(--3c3836);
|
||||
}
|
||||
|
||||
.autocomplete .title{
|
||||
float:left;
|
||||
}
|
||||
|
||||
.autocomplete .subtext{
|
||||
float:right;
|
||||
font-size:14px;
|
||||
color:var(--928374);
|
||||
margin-left:7px;
|
||||
.autocomplete .entry:focus{
|
||||
background:var(--3c3836);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<title>{%title%}</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v3">
|
||||
<link rel="stylesheet" href="/static/style.css?v4">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="robots" content="{%index%}index,{%index%}follow">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<title>4get</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/style.css?v3">
|
||||
<link rel="stylesheet" href="/static/style.css?v4">
|
||||
<meta name="robots" content="index,follow">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<meta name="description" content="4get.ca: They live in our walls!">
|
||||
|
@ -33,6 +33,6 @@
|
|||
Report a problem: <a href="https://lolcat.ca">lolcat.ca</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/client.js?v3"></script>
|
||||
<script src="/static/client.js?v4"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue