Merge pull request 'SEO: page descriptions, canonical URLs, structured data' (#97) from feature/seo-pages into main
Reviewed-on: #97
This commit is contained in:
commit
709b5d4594
@ -5,13 +5,16 @@ import '@fontsource/noto-naskh-arabic/700.css';
|
|||||||
import '@fontsource/noto-nastaliq-urdu/400.css';
|
import '@fontsource/noto-nastaliq-urdu/400.css';
|
||||||
import '@fontsource/noto-nastaliq-urdu/700.css';
|
import '@fontsource/noto-nastaliq-urdu/700.css';
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
interface Props { title?: string; description?: string; q?: string; author?: string; jsonld?: object }
|
interface Props { title?: string; description?: string; q?: string; author?: string; jsonld?: object; ogType?: string }
|
||||||
// author and jsonld describe a work for search engines and the browser's own reader view
|
// author and jsonld describe a work for search engines and the browser's own reader view
|
||||||
const { title, description = 'اردو کا کلاسیکی ادب', q = '', author, jsonld } = Astro.props;
|
const { title, description = 'اردو کا کلاسیکی ادب', q = '', author, jsonld, ogType = 'website' } = Astro.props;
|
||||||
|
// one address per page for search engines: the public origin, no query (?hl=, ?font=, ?kind= are views of the same page)
|
||||||
|
const canonical = siteUrl(Astro.url) + Astro.url.pathname;
|
||||||
const fullTitle = title ? `${title} · ${brand.name}` : `${brand.name} · ${brand.tagline}`;
|
const fullTitle = title ? `${title} · ${brand.name}` : `${brand.name} · ${brand.tagline}`;
|
||||||
import Icon from '../components/Icon.astro';
|
import Icon from '../components/Icon.astro';
|
||||||
import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can turn its rays
|
import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can turn its rays
|
||||||
import { brand } from '../lib/brand';
|
import { brand } from '../lib/brand';
|
||||||
|
import { siteUrl } from '../lib/site-url';
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ur-PK" dir="rtl">
|
<html lang="ur-PK" dir="rtl">
|
||||||
@ -20,6 +23,7 @@ import { brand } from '../lib/brand';
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{fullTitle}</title>
|
<title>{fullTitle}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="alternate" type="application/rss+xml" title={`${brand.name}: نئی تبدیلیاں`} href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title={`${brand.name}: نئی تبدیلیاں`} href="/rss.xml" />
|
||||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
||||||
@ -29,11 +33,11 @@ import { brand } from '../lib/brand';
|
|||||||
<meta name="theme-color" content="#14120f" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#14120f" media="(prefers-color-scheme: dark)" />
|
||||||
<!-- link previews (WhatsApp, Facebook, X, Slack): the logo and headline -->
|
<!-- link previews (WhatsApp, Facebook, X, Slack): the logo and headline -->
|
||||||
<meta property="og:site_name" content={brand.name} />
|
<meta property="og:site_name" content={brand.name} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content={ogType} />
|
||||||
<meta property="og:locale" content="ur_PK" />
|
<meta property="og:locale" content="ur_PK" />
|
||||||
<meta property="og:title" content={fullTitle} />
|
<meta property="og:title" content={fullTitle} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:url" content={new URL(Astro.url.pathname, Astro.url.origin).href} />
|
<meta property="og:url" content={canonical} />
|
||||||
<meta property="og:image" content={new URL('/og-image.png', Astro.url.origin).href} />
|
<meta property="og:image" content={new URL('/og-image.png', Astro.url.origin).href} />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
|
|||||||
@ -88,10 +88,21 @@ const ebookCount = page.type === 'poet' ? ((await api<any[]>(`/api/ebooks?poet=$
|
|||||||
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
|
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
|
||||||
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
|
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
|
||||||
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
||||||
|
// for search engines and link previews: a work's opening lines, a poet's intro, a book's contents (#83)
|
||||||
|
const clip = (t: string) => (t.length > 160 ? t.slice(0, 159) + '…' : t);
|
||||||
|
const description = clip(page.type === 'poem' ? `${blocks[0]?.lines.join(' / ') ?? page.poem.title} (${page.poet.nickname})`
|
||||||
|
: page.type === 'poet' ? page.poet.description || `${page.poet.nickname}: کلام، کتابیں اور ای بکس`
|
||||||
|
: `${page.category.title} · ${page.poet.nickname}: ${page.poems?.length ? `${ud(page.poems.length)} تخلیقات` : `${ud(page.children?.length ?? 0)} حصے`}`);
|
||||||
|
const here = siteUrl(Astro.url) + url, poetLd = { '@type': 'Person', name: page.poet?.nickname, url: siteUrl(Astro.url) + (crumbs[0]?.url ?? '') };
|
||||||
|
const jsonld = { '@context': 'https://schema.org', inLanguage: 'ur', url: here, ...(
|
||||||
|
page.type === 'poem' ? { '@type': 'CreativeWork', name: page.poem.title, author: poetLd, isPartOf: crumbs.at(-1)?.title }
|
||||||
|
: page.type === 'poet' ? { ...poetLd, url: here, description: page.poet.description || undefined }
|
||||||
|
: { '@type': 'CollectionPage', name: page.category.title, author: poetLd }) };
|
||||||
|
import { siteUrl } from '../lib/site-url';
|
||||||
import Icon from '../components/Icon.astro';
|
import Icon from '../components/Icon.astro';
|
||||||
import TagsBox from '../components/TagsBox.astro';
|
import TagsBox from '../components/TagsBox.astro';
|
||||||
---
|
---
|
||||||
<Base author={page.poet?.nickname} jsonld={page.type === 'poem' ? { '@context': 'https://schema.org', '@type': 'CreativeWork', name: page.poem.title, inLanguage: 'ur', author: { '@type': 'Person', name: page.poet.nickname }, isPartOf: crumbs.at(-1)?.title, url: Astro.url.href.split('?')[0] } : undefined} title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={page.type === 'poet' ? page.poet.description?.slice(0, 160) : undefined}>
|
<Base author={page.poet?.nickname} jsonld={jsonld} title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={description} ogType={page.type === 'poem' ? 'article' : 'website'}>
|
||||||
{page.type !== 'poet' && (
|
{page.type !== 'poet' && (
|
||||||
<nav class="crumbs">
|
<nav class="crumbs">
|
||||||
{crumbs.map((c: any, i: number) => (
|
{crumbs.map((c: any, i: number) => (
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user
|
|||||||
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
|
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
|
||||||
import Icon from '../../components/Icon.astro';
|
import Icon from '../../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title={`ای بکس · ${page.poet.nickname}`}>
|
<Base title={`ای بکس · ${page.poet.nickname}`} description={`${page.poet.nickname} کی ای بکس: پی ڈی ایف، ای پب اور یونیکوڈ متن`}>
|
||||||
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
|
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
|
||||||
<h1>ای بکس</h1>
|
<h1>ای بکس</h1>
|
||||||
{mayAdd && <p class="center"><a class="btn primary" href={`/mod/ebook/new?poet=${page.poet.id}`}><Icon name="plus" size={18} /> ای بک شامل کریں</a></p>}
|
{mayAdd && <p class="center"><a class="btn primary" href={`/mod/ebook/new?poet=${page.poet.id}`}><Icon name="plus" size={18} /> ای بک شامل کریں</a></p>}
|
||||||
|
|||||||
@ -20,7 +20,8 @@ const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/
|
|||||||
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
|
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
|
||||||
import Icon from '../../components/Icon.astro';
|
import Icon from '../../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title={`${b.title} · ${b.poet}`}>
|
<Base title={`${b.title} · ${b.poet}`} author={b.poet} ogType="book" description={`${b.title}: ${b.poet}${b.coauthors.length ? '، ' + b.coauthors.map((c: any) => c.name).join('، ') : ''} کی کتاب، ${KIND[b.kind]} میں`}
|
||||||
|
jsonld={{ '@context': 'https://schema.org', '@type': 'Book', name: b.title, inLanguage: 'ur', author: [b.poet, ...b.coauthors.map((c: any) => c.name)].map((name) => ({ '@type': 'Person', name })) }}>
|
||||||
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
|
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
|
||||||
<h1>{b.title}</h1>
|
<h1>{b.title}</h1>
|
||||||
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
|
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ if (!res) return new Response(null, { status: 404, statusText: 'Not found' });
|
|||||||
const { tag, categories, works, ebooks = [] } = res;
|
const { tag, categories, works, ebooks = [] } = res;
|
||||||
const count = categories.length + works.length + ebooks.length;
|
const count = categories.length + works.length + ebooks.length;
|
||||||
---
|
---
|
||||||
<Base title={`${tag.type}: ${tag.name}`}>
|
<Base title={`${tag.type}: ${tag.name}`} description={`${tag.type} «${tag.name}»: ${ud(count)} شاعر، کتابیں، تخلیقات اور اشعار`}>
|
||||||
<h1><small class="muted">{tag.type}:</small> {tag.name}</h1>
|
<h1><small class="muted">{tag.type}:</small> {tag.name}</h1>
|
||||||
<p class="center muted">{ud(count)} جگہ · <a href={`/search?q=${encodeURIComponent(`tag:"${tag.name}"`)}`}>تلاش میں دیکھیں</a> · <a href="/tags">تمام ٹیگ</a></p>
|
<p class="center muted">{ud(count)} جگہ · <a href={`/search?q=${encodeURIComponent(`tag:"${tag.name}"`)}`}>تلاش میں دیکھیں</a> · <a href="/tags">تمام ٹیگ</a></p>
|
||||||
{categories.length > 0 && (
|
{categories.length > 0 && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user