chore(spa): toter code aus pre-prerender-aera entfernt
Nach etappe 5 (runtime-fallback entfernt) sind diese files/exports ohne aufrufer: Files (ganz weg): - app/src/lib/components/PostView.svelte - app/src/lib/components/LanguageAvailability.svelte - app/src/lib/nostr/translations.ts - app/src/lib/nostr/translations.test.ts - app/src/lib/nostr/loaders.loadTranslations.test.ts Aus app/src/lib/nostr/loaders.ts entfernt: - loadPost(), loadTranslations(), TranslationInfo - resolveTranslationsFromRefs() (nur von loadTranslations.test.ts genutzt) - TranslationRef-import von ./translations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0ec72f9426
commit
bb9d35076d
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="__HTML_LANG__">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { NostrEvent, TranslationInfo } from '$lib/nostr/loaders';
|
||||
import { loadTranslations } from '$lib/nostr/loaders';
|
||||
import { activeLocale } from '$lib/i18n';
|
||||
import type { SupportedLocale } from '$lib/i18n/activeLocale';
|
||||
|
||||
interface Props {
|
||||
event: NostrEvent;
|
||||
}
|
||||
let { event }: Props = $props();
|
||||
|
||||
let translations: TranslationInfo[] = $state([]);
|
||||
let loading = $state(true);
|
||||
|
||||
$effect(() => {
|
||||
const currentId = event.id;
|
||||
loading = true;
|
||||
translations = [];
|
||||
loadTranslations(event)
|
||||
.then((infos) => {
|
||||
if (event.id !== currentId) return;
|
||||
translations = infos;
|
||||
})
|
||||
.finally(() => {
|
||||
if (event.id === currentId) loading = false;
|
||||
});
|
||||
});
|
||||
|
||||
function currentLang(): string {
|
||||
return event.tags.find((tag) => tag[0] === 'l')?.[1] ?? 'de';
|
||||
}
|
||||
|
||||
interface Option {
|
||||
code: string;
|
||||
href: string | null; // null = aktueller post, kein klick-ziel
|
||||
}
|
||||
|
||||
const options = $derived.by<Option[]>(() => {
|
||||
const self: Option = { code: currentLang(), href: null };
|
||||
const others: Option[] = translations.map((t) => ({
|
||||
code: t.lang,
|
||||
href: `/${t.slug}/`
|
||||
}));
|
||||
// aktuelle sprache zuerst, dann rest sortiert nach code
|
||||
return [self, ...others.sort((a, b) => a.code.localeCompare(b.code))];
|
||||
});
|
||||
|
||||
function selectOther(code: string, href: string) {
|
||||
activeLocale.set(code as SupportedLocale);
|
||||
// hartes location-setzen, damit svelte-kit-router den post-load triggert
|
||||
window.location.href = href;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !loading && translations.length > 0}
|
||||
<p class="lang-switch" role="group" aria-label="Article language">
|
||||
<span class="icon" aria-hidden="true">📖</span>
|
||||
{#each options as opt, i}
|
||||
{#if opt.href === null}
|
||||
<span class="btn active" aria-current="true">{opt.code.toUpperCase()}</span>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick={() => selectOther(opt.code, opt.href!)}
|
||||
>{opt.code.toUpperCase()}</button>
|
||||
{/if}
|
||||
{#if i < options.length - 1}<span class="sep" aria-hidden="true">|</span>{/if}
|
||||
{/each}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.lang-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
margin: 0.25rem 0 1rem;
|
||||
}
|
||||
.icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
border-radius: 3px;
|
||||
padding: 1px 7px;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover:not(.active) {
|
||||
color: var(--fg);
|
||||
}
|
||||
.btn.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
cursor: default;
|
||||
}
|
||||
.sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { NostrEvent } from '$lib/nostr/loaders';
|
||||
import type { SignedEvent } from '$lib/nostr/signer';
|
||||
import { renderMarkdown } from '$lib/render/markdown';
|
||||
import Reactions from './Reactions.svelte';
|
||||
import ReplyList from './ReplyList.svelte';
|
||||
import ReplyComposer from './ReplyComposer.svelte';
|
||||
import ExternalClientLinks from './ExternalClientLinks.svelte';
|
||||
import LanguageAvailability from './LanguageAvailability.svelte';
|
||||
import { t, activeLocale } from '$lib/i18n';
|
||||
|
||||
interface Props {
|
||||
event: NostrEvent;
|
||||
}
|
||||
let { event }: Props = $props();
|
||||
|
||||
function tagValue(e: NostrEvent, name: string): string {
|
||||
return e.tags.find((t) => t[0] === name)?.[1] ?? '';
|
||||
}
|
||||
function tagsAll(e: NostrEvent, name: string): string[] {
|
||||
return e.tags.filter((t) => t[0] === name).map((t) => t[1]);
|
||||
}
|
||||
|
||||
const dtag = $derived(tagValue(event, 'd'));
|
||||
let currentLocale = $state('de');
|
||||
activeLocale.subscribe((v) => (currentLocale = v));
|
||||
|
||||
const title = $derived(tagValue(event, 'title') || $t('post.untitled'));
|
||||
const summary = $derived(tagValue(event, 'summary'));
|
||||
const image = $derived(tagValue(event, 'image'));
|
||||
const publishedAt = $derived(
|
||||
parseInt(tagValue(event, 'published_at') || `${event.created_at}`, 10)
|
||||
);
|
||||
const date = $derived(
|
||||
new Date(publishedAt * 1000).toLocaleDateString(
|
||||
currentLocale === 'en' ? 'en-US' : 'de-DE',
|
||||
{ year: 'numeric', month: 'long', day: 'numeric' }
|
||||
)
|
||||
);
|
||||
const tags = $derived(tagsAll(event, 't'));
|
||||
const bodyHtml = $derived(renderMarkdown(event.content));
|
||||
|
||||
// Optimistisch gesendete Replies: der Composer pusht sie rein,
|
||||
// ReplyList merged sie mit den vom Relay geladenen Replies (dedup per id).
|
||||
let optimisticReplies: NostrEvent[] = $state([]);
|
||||
function handlePublished(signed: SignedEvent) {
|
||||
optimisticReplies = [...optimisticReplies, signed as unknown as NostrEvent];
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
document.title = `${title} – Jörg Lohrer`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1 class="post-title">{title}</h1>
|
||||
<div class="meta">
|
||||
{$t('post.published_on', { values: { date } })}
|
||||
{#if tags.length > 0}
|
||||
<div class="tags">
|
||||
{#each tags as t}
|
||||
<a class="tag" href="/tag/{encodeURIComponent(t)}/">{t}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<LanguageAvailability {event} />
|
||||
|
||||
{#if image}
|
||||
<p class="cover"><img src={image} alt="Cover-Bild" /></p>
|
||||
{/if}
|
||||
|
||||
{#if summary}
|
||||
<p class="summary">{summary}</p>
|
||||
{/if}
|
||||
|
||||
<article>{@html bodyHtml}</article>
|
||||
|
||||
{#if dtag}
|
||||
<Reactions {dtag} />
|
||||
<ExternalClientLinks {dtag} />
|
||||
<ReplyComposer {dtag} eventId={event.id} onPublished={handlePublished} />
|
||||
<ReplyList {dtag} optimistic={optimisticReplies} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.post-title {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 0.4rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.post-title {
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.tags {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: var(--code-bg);
|
||||
border-radius: 3px;
|
||||
padding: 1px 7px;
|
||||
margin: 0 4px 4px 0;
|
||||
font-size: 0.85em;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
.tag:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
.cover {
|
||||
max-width: 480px;
|
||||
margin: 1rem auto 1.5rem;
|
||||
}
|
||||
.cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.summary {
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
}
|
||||
article :global(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
article :global(a) {
|
||||
color: var(--accent);
|
||||
word-break: break-word;
|
||||
}
|
||||
article :global(pre) {
|
||||
background: var(--code-bg);
|
||||
padding: 0.8rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.88em;
|
||||
max-width: 100%;
|
||||
}
|
||||
article :global(code) {
|
||||
background: var(--code-bg);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.92em;
|
||||
word-break: break-word;
|
||||
}
|
||||
article :global(pre code) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
word-break: normal;
|
||||
}
|
||||
article :global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
article :global(blockquote) {
|
||||
border-left: 3px solid var(--border);
|
||||
padding: 0 0 0 1rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { resolveTranslationsFromRefs } from './loaders';
|
||||
import type { NostrEvent } from './loaders';
|
||||
import type { TranslationRef } from './translations';
|
||||
|
||||
function ev(tags: string[][]): NostrEvent {
|
||||
return {
|
||||
id: 'x',
|
||||
pubkey: 'p',
|
||||
created_at: 0,
|
||||
kind: 30023,
|
||||
tags,
|
||||
content: '',
|
||||
sig: 's'
|
||||
} as unknown as NostrEvent;
|
||||
}
|
||||
|
||||
describe('resolveTranslationsFromRefs', () => {
|
||||
it('liefert lang/slug/title für jeden aufgelösten ref', async () => {
|
||||
const refs: TranslationRef[] = [
|
||||
{ kind: 30023, pubkey: 'p1', dtag: 'hello' }
|
||||
];
|
||||
const fetcher = async () => [
|
||||
ev([
|
||||
['d', 'hello'],
|
||||
['title', 'Hello World'],
|
||||
['L', 'ISO-639-1'],
|
||||
['l', 'en', 'ISO-639-1']
|
||||
])
|
||||
];
|
||||
const result = await resolveTranslationsFromRefs(refs, fetcher);
|
||||
expect(result).toEqual([
|
||||
{ lang: 'en', slug: 'hello', title: 'Hello World' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignoriert refs, zu denen kein event gefunden wird', async () => {
|
||||
const refs: TranslationRef[] = [
|
||||
{ kind: 30023, pubkey: 'p1', dtag: 'hello' },
|
||||
{ kind: 30023, pubkey: 'p1', dtag: 'missing' }
|
||||
];
|
||||
const fetcher = async (r: TranslationRef) =>
|
||||
r.dtag === 'hello'
|
||||
? [ev([
|
||||
['d', 'hello'],
|
||||
['title', 'Hi'],
|
||||
['l', 'en', 'ISO-639-1']
|
||||
])]
|
||||
: [];
|
||||
const result = await resolveTranslationsFromRefs(refs, fetcher);
|
||||
expect(result).toEqual([{ lang: 'en', slug: 'hello', title: 'Hi' }]);
|
||||
});
|
||||
|
||||
it('ignoriert events ohne l-tag (sprache unklar)', async () => {
|
||||
const refs: TranslationRef[] = [
|
||||
{ kind: 30023, pubkey: 'p', dtag: 'x' }
|
||||
];
|
||||
const fetcher = async () => [
|
||||
ev([
|
||||
['d', 'x'],
|
||||
['title', 'kein lang-tag']
|
||||
])
|
||||
];
|
||||
const result = await resolveTranslationsFromRefs(refs, fetcher);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('leere ref-liste → leere ergebnis-liste', async () => {
|
||||
const fetcher = async () => {
|
||||
throw new Error('should not be called');
|
||||
};
|
||||
expect(await resolveTranslationsFromRefs([], fetcher)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,6 @@ import type { Filter as ApplesauceFilter } from 'applesauce-core/helpers/filter'
|
|||
import { pool } from './pool';
|
||||
import { readRelays } from '$lib/stores/readRelays';
|
||||
import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config';
|
||||
import type { TranslationRef } from './translations';
|
||||
|
||||
/** Re-export als sprechenden Alias */
|
||||
export type { NostrEvent };
|
||||
|
|
@ -89,21 +88,6 @@ export async function loadPostList(
|
|||
});
|
||||
}
|
||||
|
||||
/** Einzelpost per d-Tag */
|
||||
export async function loadPost(dtag: string): Promise<NostrEvent | null> {
|
||||
const relays = get(readRelays);
|
||||
const events = await collectEvents(relays, {
|
||||
kinds: [30023],
|
||||
authors: [AUTHOR_PUBKEY_HEX],
|
||||
'#d': [dtag],
|
||||
limit: 1
|
||||
});
|
||||
if (events.length === 0) return null;
|
||||
return events.reduce((best, cur) =>
|
||||
cur.created_at > best.created_at ? cur : best
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Profil-Event kind:0 (neueste Version).
|
||||
* Default: Autoren-Pubkey der SPA. Optional: beliebiger Pubkey für
|
||||
|
|
@ -190,55 +174,3 @@ export async function loadReactions(dtag: string): Promise<ReactionSummary[]> {
|
|||
.map(([content, count]) => ({ content, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
export interface TranslationInfo {
|
||||
lang: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure Variante für Tests — erhält die Events via Fetcher statt Relays.
|
||||
*/
|
||||
export async function resolveTranslationsFromRefs(
|
||||
refs: TranslationRef[],
|
||||
fetcher: (ref: TranslationRef) => Promise<NostrEvent[]>
|
||||
): Promise<TranslationInfo[]> {
|
||||
if (refs.length === 0) return [];
|
||||
const results = await Promise.all(refs.map(fetcher));
|
||||
const infos: TranslationInfo[] = [];
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const evs = results[i];
|
||||
if (evs.length === 0) continue;
|
||||
const latest = evs.reduce((best, cur) =>
|
||||
cur.created_at > best.created_at ? cur : best
|
||||
);
|
||||
const lang = latest.tags.find((t) => t[0] === 'l')?.[1];
|
||||
if (!lang) continue;
|
||||
const slug = latest.tags.find((t) => t[0] === 'd')?.[1] ?? refs[i].dtag;
|
||||
const title = latest.tags.find((t) => t[0] === 'title')?.[1] ?? '';
|
||||
infos.push({ lang, slug, title });
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader: findet die anderssprachigen Varianten eines Posts.
|
||||
* Liefert leere Liste, wenn keine a-Tags mit marker "translation" vorhanden.
|
||||
*/
|
||||
export async function loadTranslations(
|
||||
event: NostrEvent
|
||||
): Promise<TranslationInfo[]> {
|
||||
const { parseTranslationRefs } = await import('./translations');
|
||||
const refs = parseTranslationRefs(event);
|
||||
if (refs.length === 0) return [];
|
||||
const relays = get(readRelays);
|
||||
return resolveTranslationsFromRefs(refs, (ref) =>
|
||||
collectEvents(relays, {
|
||||
kinds: [ref.kind],
|
||||
authors: [ref.pubkey],
|
||||
'#d': [ref.dtag],
|
||||
limit: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { parseTranslationRefs } from './translations';
|
||||
import type { NostrEvent } from './loaders';
|
||||
|
||||
function ev(tags: string[][]): NostrEvent {
|
||||
return {
|
||||
id: 'x',
|
||||
pubkey: 'p',
|
||||
created_at: 0,
|
||||
kind: 30023,
|
||||
tags,
|
||||
content: '',
|
||||
sig: 's'
|
||||
} as unknown as NostrEvent;
|
||||
}
|
||||
|
||||
describe('parseTranslationRefs', () => {
|
||||
it('extrahiert a-tags mit marker "translation"', () => {
|
||||
const e = ev([
|
||||
['d', 'x'],
|
||||
['a', '30023:abc:other-slug', '', 'translation'],
|
||||
['a', '30023:abc:third-slug', '', 'translation']
|
||||
]);
|
||||
expect(parseTranslationRefs(e)).toEqual([
|
||||
{ kind: 30023, pubkey: 'abc', dtag: 'other-slug' },
|
||||
{ kind: 30023, pubkey: 'abc', dtag: 'third-slug' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignoriert a-tags ohne marker "translation"', () => {
|
||||
const e = ev([
|
||||
['a', '30023:abc:root-thread', '', 'root'],
|
||||
['a', '30023:abc:x', '', 'reply']
|
||||
]);
|
||||
expect(parseTranslationRefs(e)).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignoriert a-tags mit malformed coordinate', () => {
|
||||
const e = ev([
|
||||
['a', 'not-a-coord', '', 'translation'],
|
||||
['a', '30023:abc:ok', '', 'translation']
|
||||
]);
|
||||
expect(parseTranslationRefs(e)).toEqual([
|
||||
{ kind: 30023, pubkey: 'abc', dtag: 'ok' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('leeres tag-array → leere liste', () => {
|
||||
expect(parseTranslationRefs(ev([]))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import type { NostrEvent } from './loaders';
|
||||
|
||||
export interface TranslationRef {
|
||||
kind: number;
|
||||
pubkey: string;
|
||||
dtag: string;
|
||||
}
|
||||
|
||||
const COORD_RE = /^(\d+):([0-9a-f]+):([a-z0-9][a-z0-9-]*)$/;
|
||||
|
||||
export function parseTranslationRefs(event: NostrEvent): TranslationRef[] {
|
||||
const refs: TranslationRef[] = [];
|
||||
for (const tag of event.tags) {
|
||||
if (tag[0] !== 'a') continue;
|
||||
if (tag[3] !== 'translation') continue;
|
||||
const coord = tag[1];
|
||||
if (typeof coord !== 'string') continue;
|
||||
const m = coord.match(COORD_RE);
|
||||
if (!m) continue;
|
||||
refs.push({
|
||||
kind: parseInt(m[1], 10),
|
||||
pubkey: m[2],
|
||||
dtag: m[3]
|
||||
});
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
|
@ -13,13 +13,32 @@
|
|||
const snapshot = $derived(data.snapshot)
|
||||
|
||||
const siteUrl = '__SITE_URL__'
|
||||
|
||||
// Site-default-OG-bild aus app/static. Dimensionen sind hartcodiert,
|
||||
// weil das asset stabil ist (siehe spec §Algorithmus-Schritt 8).
|
||||
const DEFAULT_OG_IMAGE = `${siteUrl}/joerg-profil-2024.webp`
|
||||
const DEFAULT_OG_IMAGE_WIDTH = 512
|
||||
const DEFAULT_OG_IMAGE_HEIGHT = 512
|
||||
|
||||
const canonical = $derived(`${siteUrl}/${snapshot?.slug ?? dtag}/`)
|
||||
const ogImage = $derived(
|
||||
snapshot?.cover_image?.url ?? `${siteUrl}/joerg-profil-2024.webp`,
|
||||
)
|
||||
const ogImage = $derived(snapshot?.cover_image?.url ?? DEFAULT_OG_IMAGE)
|
||||
const ogImageAlt = $derived(
|
||||
snapshot?.cover_image?.alt ?? snapshot?.title ?? 'Jörg Lohrer',
|
||||
)
|
||||
const ogImageWidth = $derived(
|
||||
snapshot?.cover_image?.width ?? (snapshot?.cover_image ? undefined : DEFAULT_OG_IMAGE_WIDTH),
|
||||
)
|
||||
const ogImageHeight = $derived(
|
||||
snapshot?.cover_image?.height ?? (snapshot?.cover_image ? undefined : DEFAULT_OG_IMAGE_HEIGHT),
|
||||
)
|
||||
// x-default zeigt auf die DE-variante, weil der autor DE-first arbeitet.
|
||||
// Bei EN-posts: DE-slug aus translations[] suchen; sonst (DE-post)
|
||||
// bleibt x-default = canonical.
|
||||
const xDefaultHref = $derived(
|
||||
snapshot?.lang === 'en'
|
||||
? `${siteUrl}/${snapshot.translations.find((tr) => tr.lang === 'de')?.slug ?? snapshot.slug}/`
|
||||
: canonical,
|
||||
)
|
||||
const bodyHtmlPrerendered = $derived(
|
||||
snapshot ? renderMarkdown(snapshot.content_markdown) : '',
|
||||
)
|
||||
|
|
@ -59,11 +78,11 @@
|
|||
<meta property="og:locale" content={snapshot.lang === 'de' ? 'de_DE' : 'en_US'} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:alt" content={ogImageAlt} />
|
||||
{#if snapshot.cover_image?.width}
|
||||
<meta property="og:image:width" content={String(snapshot.cover_image.width)} />
|
||||
{#if ogImageWidth}
|
||||
<meta property="og:image:width" content={String(ogImageWidth)} />
|
||||
{/if}
|
||||
{#if snapshot.cover_image?.height}
|
||||
<meta property="og:image:height" content={String(snapshot.cover_image.height)} />
|
||||
{#if ogImageHeight}
|
||||
<meta property="og:image:height" content={String(ogImageHeight)} />
|
||||
{/if}
|
||||
<meta property="article:published_time" content={new Date(snapshot.published_at * 1000).toISOString()} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
|
@ -73,7 +92,7 @@
|
|||
{#each snapshot.translations as alt}
|
||||
<link rel="alternate" hreflang={alt.lang} href={`${siteUrl}/${alt.slug}/`} />
|
||||
{/each}
|
||||
<link rel="alternate" hreflang="x-default" href={canonical} />
|
||||
<link rel="alternate" hreflang="x-default" href={xDefaultHref} />
|
||||
{@html `<script type="application/ld+json">${jsonLd.replace(/<\/script>/gi, '<\\/script>')}</script>`}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
|
|||
Loading…
Reference in New Issue