Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/Article.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* An article (RFC-0018 §3, the article view): kicker (the primary category), title, dek, byline
* (author, published, updated, reading time), cover, an "On this page" table of contents for
* longer pieces (headings carry ids; no JavaScript), then the body with the theme's own blocks and
* the corrections, newest first (RFC-0019 §2; the latest one is the "Updated" date).
* the corrections, newest first (RFC-0019 §2; the latest one is the "Updated" date). Collections
* with typed fields (RFC-0041 §9) show their values as a "Details" list under the cover.
*/
import { Body } from "@cloudhal/astro-renderer/components";
import { Body, EntryFields } from "@cloudhal/astro-renderer/components";
import type { RenderedFields } from "@cloudhal/astro-renderer/fields";
import { articleCorrections, articleOutline } from "@cloudhal/astro-renderer/kit";
import { entryArticleParts } from "@cloudhal/astro-renderer/markdown";
import type { MediaContext } from "@cloudhal/astro-renderer/media";
Expand All @@ -22,8 +24,10 @@ interface Props {
media?: MediaContext;
blocks?: Record<string, unknown>;
blockTypes?: readonly BlockType[];
/** The entry's typed fields (`source.entryFields`); absent or empty draws nothing. */
fields?: RenderedFields;
}
const { entry, terms, locale, t, media, blocks, blockTypes } = Astro.props;
const { entry, terms, locale, t, media, blocks, blockTypes, fields } = Astro.props;
const parts = entryArticleParts(entry, locale, media);
const outline = articleOutline(entry);
const date = (iso: string) =>
Expand Down Expand Up @@ -73,6 +77,7 @@ const author =
</p>
</header>
<Fragment set:html={parts.cover} />
{fields && <EntryFields fields={fields} label={t.details} />}
{outline.toc.length >= 3 && (
<details class="ch-toc" open>
<summary>{t.onThisPage}</summary>
Expand Down
57 changes: 37 additions & 20 deletions src/pages/[...path].astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { commentsJsonLd, loadComments } from "@cloudhal/astro-renderer/comments";
import { Comments, Sections } from "@cloudhal/astro-renderer/components";
import { EMBED_HEADER, usesEmbeds } from "@cloudhal/astro-renderer/embeds";
import type { RenderedFields } from "@cloudhal/astro-renderer/fields";
import { gateNotice } from "@cloudhal/astro-renderer/gates";
import {
alternatesFor,
Expand All @@ -34,15 +35,18 @@ import {
navigationCacheTags,
} from "@cloudhal/astro-renderer/navigation";
import { notFoundPage, notFoundPageIds } from "@cloudhal/astro-renderer/not-found";
import { contentListEntries } from "@cloudhal/astro-renderer/queries";
import { resolveRoute, staticPaths } from "@cloudhal/astro-renderer/routing";
import { getSource, peerEditions } from "@cloudhal/astro-renderer/runtime";
import { searchAvailable } from "@cloudhal/astro-renderer/search";
import {
archiveHead,
collectionFeedLink,
entryHead,
pageHead,
searchHead,
siteOrigin,
withStructuredData,
} from "@cloudhal/astro-renderer/seo";
import { loadSurfaceSources, surfaceSources } from "@cloudhal/astro-renderer/surfaces";
import {
Expand Down Expand Up @@ -256,6 +260,7 @@ let entries: Record<string, ContentEntry[]> = {};
let surfaces: Awaited<ReturnType<typeof loadSurfaceSources>> = {};
let terms = entryTaxonomy(undefined, { tags: [] });
let related: ContentEntry[] = [];
let fields: RenderedFields | undefined;
if (route.kind === "page") {
tags.push(nodeCacheTag(route.page.id));
// Lists of the site's entries change with any entry: the site's list tag covers them.
Expand All @@ -267,26 +272,17 @@ if (route.kind === "page") {
tags.push(listCacheTag(site.id));
// Generated UI (RFC-0020 §3): entry sources load from the same source as the page.
surfaces = await loadSurfaceSources(source, route.page.sections);
const collections = [
...new Set(
route.page.sections.filter((s) => s.kind === "contentList").map((s) => s.collection),
),
];
entries = Object.fromEntries(
await Promise.all(collections.map(async (c) => [c, await source.entries(c, locale)])),
);
// A section with a `filter` (RFC-0002 §5) lists its own entries, keyed by section id.
for (const s of route.page.sections)
if (s.kind === "contentList" && s.filter)
entries[s.id] = await source
.entriesByFilter(`collection:${s.collection}+${s.filter}`, { locale })
.catch((err: unknown) => {
console.warn(`cloudhal: section ${s.id} has an invalid filter: ${String(err)}`);
return [];
});
// Each collection's newest entries; a section with a `filter` (RFC-0002 §5) or `where`/`orderBy`
// (RFC-0042 §4) lists its own, keyed by section id.
entries = await contentListEntries(source, route.page.sections, locale);
} else if (route.kind === "entry") {
terms = entryTaxonomy(index, route.entry);
related = relatedEntries(route.entry, all, index, RELATED_POSTS);
// RFC-0041 §9: typed fields in the entry's language; values read from the original tag it too.
fields = await source.entryFields?.(route.entry, { media, words: { yes: t.yes, no: t.no } });
const group = route.entry.translation?.group;
if (group && Object.values(fields ?? {}).some((f) => f.fromOriginal))
tags.push(nodeCacheTag(group));
tags.push(
nodeCacheTag(route.entry.id),
// the related posts it links to: a retitled or removed one purges this page too
Expand Down Expand Up @@ -332,7 +328,7 @@ const gated = route.kind === "page" ? route.page : route.kind === "entry" ? rout
const gate = gated ? gateNotice(gated, site, locale, Date.now()) : undefined;
const notice = gate && { state: gate.state, at: gate.at, text: t[gate.state](gate.when) };

const head =
const baseHead =
route.kind === "page"
? pageHead(site, route.page, origin, media)
: route.kind === "entry"
Expand All @@ -346,6 +342,11 @@ const head =
title: `${isMenu ? t.menu : t.notFoundTitle} · ${site.name}`,
canonical: `${origin}${isMenu ? "/menu" : path}`,
};
// RFC-0042 §2: a collection's JSON-LD preset replaces the article's generic BlogPosting.
const head =
route.kind === "entry"
? await withStructuredData(baseHead, source, route.entry, { origin, media })
: baseHead;
// RFC-0033 §4: self-canonical in the locale, `lang`/`dir`, and the `hreflang` set from the group.
const fallback = shownIn !== locale;
const alternates =
Expand All @@ -359,7 +360,23 @@ const localized = localizeHead(head, site, {
alternates,
...(gated?.translation ? { translation: gated.translation } : {}),
});
const meta = fallback || notFound ? { ...localized, robots: "noindex,follow" } : localized;
// RFC-0042 §5: the feed of the entry's collection, or of each collection a page lists.
const listed = notFound
? []
: route.kind === "entry"
? [route.entry.collection]
: route.kind === "page"
? route.page.sections.flatMap((s) => (s.kind === "contentList" ? [s.collection] : []))
: [];
const feeds = [
...localized.feeds,
...[...new Set(listed)].flatMap((c) => collectionFeedLink(site, c, origin, locale) ?? []),
];
const meta = {
...localized,
feeds,
...(fallback || notFound ? { robots: "noindex,follow" } : {}),
};
Astro.response.headers.set("content-language", shownIn);
// The pairing's fonts as a Link header too (Early Hints), not only as <link rel=preload> in <head>.
const pairing = themeVariables(site, themeSettings).attributes["data-ch-type-pairing"];
Expand Down Expand Up @@ -448,7 +465,7 @@ const searchForm = searchAvailable(config, source)
{ name: route.entry.title },
]}
/>
<Article entry={route.entry} terms={terms} locale={locale} t={t} media={media} blocks={blocks} blockTypes={blockTypes} />
<Article entry={route.entry} terms={terms} locale={locale} t={t} media={media} blocks={blocks} blockTypes={blockTypes} fields={fields} />
<PostTerms taxonomy={terms} locale={locale} />
<RelatedPosts title={t.related} entries={related} locale={locale} media={media} />
<Comments view={comments} />
Expand Down
4 changes: 3 additions & 1 deletion src/pages/[...path]/rss.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const SUFFIX = "/rss.xml";

/**
* Static builds: one feed per served archive of a taxonomy with `feed: true`, in every locale on
* this host, and each other locale's site feed (`/<locale>/rss.xml`, RFC-0033).
* this host, and each other locale's site feed (`/<locale>/rss.xml`, RFC-0033). The paths include
* each collection's feed (`<prefix>/rss.xml`, RFC-0042 §5): this route answers 404 for it and the
* renderer's middleware answers with the feed.
*/
export async function getStaticPaths() {
const source = await getSource(config);
Expand Down
10 changes: 10 additions & 0 deletions src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const STRINGS = {
updated: (when: string) => `Updated ${when}`,
minRead: (n: number) => `${n} min read`,
onThisPage: "On this page",
/** RFC-0041 §9: the typed fields list on an article, and the words of yes/no fields. */
details: "Details",
yes: "Yes",
no: "No",
/** RFC-0019 §2: the corrections list at the end of an article, and the note under the byline. */
corrections: "Corrections and updates",
correctionKind: { correction: "Correction", clarification: "Clarification", update: "Update" },
Expand Down Expand Up @@ -102,6 +106,9 @@ const STRINGS = {
updated: (when: string) => `Güncellendi: ${when}`,
minRead: (n: number) => `${n} dk okuma`,
onThisPage: "Bu sayfada",
details: "Ayrıntılar",
yes: "Evet",
no: "Hayır",
corrections: "Düzeltmeler ve güncellemeler",
correctionKind: { correction: "Düzeltme", clarification: "Açıklama", update: "Güncelleme" },
appended: (kind: string) => `${kind} eklendi`,
Expand Down Expand Up @@ -178,6 +185,9 @@ const STRINGS = {
updated: (when: string) => `${when} 更新`,
minRead: (n: number) => `${n} 分で読めます`,
onThisPage: "このページの内容",
details: "詳細",
yes: "はい",
no: "いいえ",
corrections: "訂正と更新",
correctionKind: { correction: "訂正", clarification: "補足", update: "更新" },
appended: (kind: string) => `${kind}を追記`,
Expand Down
3 changes: 2 additions & 1 deletion src/styles/features/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
.ch-body :is(p, li, blockquote) {
text-wrap: pretty;
}
.ch-richtext a {
/* Links in a body, not buttons: a surface Button keeps its own colours (on-brand on brand). */
.ch-richtext a:not(.ch-button) {
color: var(--ch-color-link);
}
Loading