Add <link rel="canonical"> to some pages

This commit is contained in:
Alex Cabal 2024-05-08 11:01:18 -05:00
parent f8b817c4e1
commit 7c8463d297
8 changed files with 128 additions and 82 deletions

View file

@ -36,10 +36,6 @@ class Formatter{
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES, 'utf-8');
}
public static function EscapeXhtmlQueryString(?string $text): string{
return str_replace('&', '&amp;', trim($text ?? ''));
}
public static function EscapeXml(?string $text): string{
// Accepts a query string that has already been url-encoded. For example,
// ?foo=bar+baz&x=y

View file

@ -11,6 +11,7 @@ $feedUrl = $feedUrl ?? null;
$feedTitle = $feedTitle ?? '';
$isErrorPage = $isErrorPage ?? false;
$downloadUrl = $downloadUrl ?? null;
$canonicalUrl = $canonicalUrl ?? null;
// As of Sep 2022, all versions of Safari have a bug where if the page is served as XHTML,
// then <picture> elements download all <source>s instead of the first supported match.
@ -55,6 +56,9 @@ if(!$isXslt){
<? if($artwork){ ?>
<link href="/css/artwork.css?version=<?= filemtime(WEB_ROOT . '/css/artwork.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
<? } ?>
<? if($canonicalUrl){ ?>
<link rel="canonical" href="<?= Formatter::EscapeHtml($canonicalUrl) ?>" />
<? } ?>
<link href="/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120"/>
<link href="/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152"/>
<link href="/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"/>
@ -66,9 +70,9 @@ if(!$isXslt){
<link rel="alternate" type="application/atom+xml;profile=opds-catalog;kind=acquisition" title="Standard Ebooks - New Releases" href="https://standardebooks.org/feeds/opds/new-releases"/>
<link rel="alternate" type="application/rss+xml" title="Standard Ebooks - New Releases" href="https://standardebooks.org/feeds/rss/new-releases"/>
<? }else{ ?>
<link rel="alternate" type="application/atom+xml" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/atom<?= $feedUrl ?>"/>
<link rel="alternate" type="application/atom+xml;profile=opds-catalog;kind=acquisition" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/opds<?= $feedUrl ?>"/>
<link rel="alternate" type="application/rss+xml" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/rss<?= $feedUrl ?>"/>
<link rel="alternate" type="application/atom+xml" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/atom<?= Formatter::EscapeHtml($feedUrl) ?>"/>
<link rel="alternate" type="application/atom+xml;profile=opds-catalog;kind=acquisition" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/opds<?= Formatter::EscapeHtml($feedUrl) ?>"/>
<link rel="alternate" type="application/rss+xml" title="<?= Formatter::EscapeHtml($feedTitle) ?>" href="/feeds/rss<?= Formatter::EscapeHtml($feedUrl) ?>"/>
<? } ?>
<link rel="search" href="/ebooks" type="application/xhtml+xml; charset=utf-8"/>
<link rel="search" href="/ebooks/opensearch" type="application/opensearchdescription+xml; charset=utf-8"/>

View file

@ -97,8 +97,23 @@ try{
$queryStringParams['per-page'] = $perPage;
}
if($page > 1){
$queryStringParams['page'] = $page;
}
ksort($queryStringParams);
$queryString = http_build_query($queryStringParams);
unset($queryStringParams['page']);
$queryStringWithoutPage = http_build_query($queryStringParams);
$canonicalUrl = SITE_URL . '/artworks';
if($queryString != ''){
$canonicalUrl .= '?' . $queryString;
}
$pages = ceil($totalArtworkCount / $perPage);
if($pages > 0 && $page > $pages){
throw new Exceptions\PageOutOfBoundsException();
@ -106,14 +121,14 @@ try{
}
catch(Exceptions\PageOutOfBoundsException){
$url = '/artworks?page=' . $pages;
if($queryString != ''){
$url .= '&' . $queryString;
if($queryStringWithoutPage != ''){
$url .= '&' . $queryStringWithoutPage;
}
header('Location: ' . $url);
exit();
}
?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription]) ?>
?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription, 'canonicalUrl' => $canonicalUrl]) ?>
<main class="artworks">
<section class="narrow">
<h1>Browse U.S. Public Domain Artwork</h1>
@ -167,13 +182,13 @@ catch(Exceptions\PageOutOfBoundsException){
<? if($totalArtworkCount > 0){ ?>
<nav class="pagination">
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<ol>
<? for($i = 1; $i < $pages + 1; $i++){ ?>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>"><?= $i ?></a></li>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>"><?= $i ?></a></li>
<? } ?>
</ol>
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
</nav>
<? } ?>
</section>

View file

@ -28,7 +28,7 @@ try{
$pageDescription = 'A list of free ebooks in the ' . Formatter::EscapeHtml($collectionName) . ' ' . $collectionType;
$pageHeader = 'Free Ebooks in the ' . Formatter::EscapeHtml($collectionName) . ' ' . ucfirst($collectionType);
$feedUrl = '/collections/' . Formatter::EscapeHtml($collection);
$feedUrl = '/collections/' . $collection;
$feedTitle = 'Standard Ebooks - Ebooks in the ' . Formatter::EscapeHtml($collectionName) . ' ' . $collectionType;
}
catch(Exceptions\CollectionNotFoundException){

View file

@ -19,17 +19,17 @@ try{
}
$author = strip_tags($ebooks[0]->AuthorsHtml);
$authorUrl = Formatter::EscapeHtml($ebooks[0]->AuthorsUrl);
$authorUrl = $ebooks[0]->AuthorsUrl;
}
catch(Exceptions\AuthorNotFoundException){
Template::Emit404();
}
?><?= Template::Header(['title' => 'Ebooks by ' . $author, 'feedUrl' => str_replace('/ebooks/', '/authors/', $authorUrl), 'feedTitle' => 'Standard Ebooks - Ebooks by ' . $author, 'highlight' => 'ebooks', 'description' => 'All of the Standard Ebooks ebooks by ' . $author]) ?>
?><?= Template::Header(['title' => 'Ebooks by ' . $author, 'feedUrl' => str_replace('/ebooks/', '/authors/', $authorUrl), 'feedTitle' => 'Standard Ebooks - Ebooks by ' . $author, 'highlight' => 'ebooks', 'description' => 'All of the Standard Ebooks ebooks by ' . $author, 'canonicalUrl' => SITE_URL . $authorUrl]) ?>
<main class="ebooks">
<h1 class="is-collection">Ebooks by <?= $ebooks[0]->AuthorsHtml ?></h1>
<p class="ebooks-toolbar">
<a class="button" href="<?= $authorUrl ?>/downloads">Download collection</a>
<a class="button" href="<?= $authorUrl ?>/feeds">Feeds for this author</a>
<a class="button" href="<?= Formatter::EscapeHtml($authorUrl) ?>/downloads">Download collection</a>
<a class="button" href="<?= Formatter::EscapeHtml($authorUrl) ?>/feeds">Feeds for this author</a>
</p>
<?= Template::EbookGrid(['ebooks' => $ebooks, 'view' => VIEW_GRID]) ?>
<p class="feeds-alert">We also have <a href="/bulk-downloads">bulk ebook downloads</a> and a <a href="/collections">list of collections</a> available, as well as <a href="/feeds">ebook catalog feeds</a> for use directly in your ereader app or RSS reader.</p>

View file

@ -107,7 +107,7 @@ catch(Exceptions\SeeOtherEbookException $ex){
catch(Exceptions\EbookNotFoundException){
Template::Emit404();
}
?><?= Template::Header(['title' => strip_tags($ebook->TitleWithCreditsHtml) . ' - Free ebook download', 'ogType' => 'book', 'coverUrl' => $ebook->DistCoverUrl, 'highlight' => 'ebooks', 'description' => 'Free epub ebook download of the Standard Ebooks edition of ' . $ebook->Title . ': ' . $ebook->Description]) ?>
?><?= Template::Header(['title' => strip_tags($ebook->TitleWithCreditsHtml) . ' - Free ebook download', 'ogType' => 'book', 'coverUrl' => $ebook->DistCoverUrl, 'highlight' => 'ebooks', 'description' => 'Free epub ebook download of the Standard Ebooks edition of ' . $ebook->Title . ': ' . $ebook->Description, 'canonicalUrl' => SITE_URL . $ebook->Url]) ?>
<main>
<article class="ebook" typeof="schema:Book" about="<?= $ebook->Url ?>">
<meta property="schema:description" content="<?= Formatter::EscapeHtml($ebook->Description) ?>"/>

View file

@ -8,69 +8,100 @@ $tags = HttpInput::GetArray('tags') ?? [];
$view = HttpInput::Str(GET, 'view');
$sort = EbookSort::tryFrom(HttpInput::Str(GET, 'sort') ?? '');
$queryString = '';
$queryStringParams = [];
if($page <= 0){
$page = 1;
try{
if($page <= 0){
$page = 1;
}
if($perPage != EBOOKS_PER_PAGE && $perPage != 24 && $perPage != 48){
$perPage = EBOOKS_PER_PAGE;
}
// If we're passed string values that are the same as the defaults,
// set them to null so that we can have cleaner query strings in the navigation footer
if($view !== null){
$view = mb_strtolower($view);
}
if($view === 'grid'){
$view = null;
}
if($sort == EbookSort::Newest){
$sort = null;
}
if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){
$tags = [];
}
$ebooks = Library::FilterEbooks($query != '' ? $query : null, $tags, $sort);
$pageTitle = 'Browse Standard Ebooks';
$pageHeader = 'Browse Ebooks';
$pages = ceil(sizeof($ebooks) / $perPage);
$totalEbooks = sizeof($ebooks);
$ebooks = array_slice($ebooks, ($page - 1) * $perPage, $perPage);
if($page > 1){
$pageTitle .= ', page ' . $page;
}
$pageDescription = 'Page ' . $page . ' of the Standard Ebooks free ebook library';
if($query != ''){
$queryStringParams['query'] = $query;
}
if(sizeof($tags) > 0){
$queryStringParams['tags'] = $tags;
}
if($view !== null){
$queryStringParams['view'] = $view;
}
if($sort !== null){
$queryStringParams['sort'] = $sort->value;
}
if($perPage !== EBOOKS_PER_PAGE){
$queryStringParams['per-page'] = $perPage;
}
if($page > 1){
$queryStringParams['page'] = $page;
}
ksort($queryStringParams);
$queryString = http_build_query($queryStringParams);
unset($queryStringParams['page']);
$queryStringWithoutPage = http_build_query($queryStringParams);
$canonicalUrl = SITE_URL . '/ebooks';
if($queryString != ''){
$canonicalUrl .= '?' . $queryString;
}
if($pages > 0 && $page > $pages){
throw new Exceptions\PageOutOfBoundsException();
}
}
catch(Exceptions\PageOutOfBoundsException){
$url = '/ebooks?page=' . $pages;
if($queryStringWithoutPage != ''){
$url .= '&' . $queryStringWithoutPage;
}
if($perPage != EBOOKS_PER_PAGE && $perPage != 24 && $perPage != 48){
$perPage = EBOOKS_PER_PAGE;
header('Location: ' . $url);
exit();
}
// If we're passed string values that are the same as the defaults,
// set them to null so that we can have cleaner query strings in the navigation footer
if($view !== null){
$view = mb_strtolower($view);
}
if($view === 'grid'){
$view = null;
}
if($sort == EbookSort::Newest){
$sort = null;
}
if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){
$tags = [];
}
$ebooks = Library::FilterEbooks($query != '' ? $query : null, $tags, $sort);
$pageTitle = 'Browse Standard Ebooks';
$pageHeader = 'Browse Ebooks';
$pages = ceil(sizeof($ebooks) / $perPage);
$totalEbooks = sizeof($ebooks);
$ebooks = array_slice($ebooks, ($page - 1) * $perPage, $perPage);
if($page > 1){
$pageTitle .= ', page ' . $page;
}
$pageDescription = 'Page ' . $page . ' of the Standard Ebooks free ebook library';
if($query != ''){
$queryString .= '&amp;query=' . urlencode($query);
}
foreach($tags as $tag){
$queryString .= '&amp;tags[]=' . urlencode($tag);
}
if($view !== null){
$queryString .= '&amp;view=' . urlencode($view);
}
if($sort !== null){
$queryString .= '&amp;sort=' . urlencode($sort->value);
}
if($perPage !== EBOOKS_PER_PAGE){
$queryString .= '&amp;per-page=' . urlencode((string)$perPage);
}
$queryString = preg_replace('/^&amp;/ius', '', $queryString);
?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?>
?><?= Template::Header(['title' => $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription, 'canonicalUrl' => $canonicalUrl]) ?>
<main class="ebooks">
<h1><?= $pageHeader ?></h1>
<?= Template::DonationCounter() ?>
@ -86,13 +117,13 @@ $queryString = preg_replace('/^&amp;/ius', '', $queryString);
<? } ?>
<? if(sizeof($ebooks) > 0){ ?>
<nav class="pagination">
<a<? if($page > 1){ ?> href="/ebooks?page=<?= $page - 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<a<? if($page > 1){ ?> href="/ebooks?page=<?= $page - 1 ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<ol>
<? for($i = 1; $i < $pages + 1; $i++){ ?>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/ebooks?page=<?= $i ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>"><?= $i ?></a></li>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/ebooks?page=<?= $i ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>"><?= $i ?></a></li>
<? } ?>
</ol>
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&amp;<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks?page=<?= $page + 1 ?><? if($queryStringWithoutPage != ''){ ?>&amp;<?= Formatter::EscapeHtml($queryStringWithoutPage) ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
</nav>
<? } ?>

View file

@ -1,4 +1,4 @@
<?= Template::Header(['description' => 'Free and liberated ebooks, carefully produced for the true book lover. Download free ebooks with professional-quality formatting and typography, in formats compatible with your ereader.']) ?>
<?= Template::Header(['description' => 'Free and liberated ebooks, carefully produced for the true book lover. Download free ebooks with professional-quality formatting and typography, in formats compatible with your ereader.', 'canonicalUrl' => SITE_URL]) ?>
<main class="front-page">
<h1>Free and liberated ebooks,<br/> carefully produced for the true book lover.</h1>
<picture>