Skip to content

Related articles and share buttons for blog posts - #82

Open
VanescoB wants to merge 1 commit into
mainfrom
feature/74-79-related-articles-and-sharing-section
Open

VanescoB wants to merge 1 commit into
mainfrom
feature/74-79-related-articles-and-sharing-section

Conversation

@VanescoB

@VanescoB VanescoB commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Adds a “Related Articles” section to blog posts, showing up to three recommendations based on shared tags and authors. Introduces sharing options for LinkedIn, X, copying links, and native sharing where supported. Reuses the existing article grid and adds unit tests for recommendation ranking and filtering.

image

@VanescoB
VanescoB requested review from a team and martinkoenig-bbv October 6, 2026 17:54

@martinkoenig-bbv martinkoenig-bbv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verifikation
npm test unter Node 22 ausführen, z. B. npx -y -p node@22 node --test "plugins/**/*.test.js".
npm run typecheck.
npm run build ohne neue Warnungen, dann npm run serve. Prüfen: Posts mit 0, 1, 2 und 3 Related Posts; Hover auf allen vier Share-Buttons; „Copy link“; Breiten unter 768 px, 769–1200 px und über 1200 px.
Pagefind-Suche: Titel aus „Related Articles“ dürfen nicht als Treffer im aktuellen Post erscheinen.

padding: 0.35rem 0.75rem;
font-size: 0.85rem;
cursor: pointer;
text-decoration: none;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code meint:

Share-Buttons sehen uneinheitlich aus — [ShareButtons/styles.module.css:19] .button { text-decoration: none } (Spezifität 0,1,0) verliert gegen das globale a:hover { text-decoration: underline } in [custom.css:112] (0,1,1). „LinkedIn“ und „X“ werden beim Hover unterstrichen, die beiden nicht. Zudem erben die Seitenschrift nicht. → Fix: Die Infima-Klassen button button--sm button--outline button--secondary verwenden. Sie stylen und identisch und unterdrücken die Unterstreichung. Der Großteil des CSS-Moduls entfällt dann. Alternativ font: inherit setzen und text-decoration: none in .button:hover aufnehmen.

return (
<section className={styles.relatedArticles} data-pagefind-ignore>
<h2 className={styles.heading}>Related Articles</h2>
<BlogPostGrid items={items} stretchFewItems titleAs="h3" />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das Grid nutzt repeat(auto-fill, minmax(300px, 1fr)) bzw. 2 Spalten zwischen 769 und 1200 px. Die Hauptspalte (col--9) ist auf dem Desktop nur etwa 800–934 px breit, also zu schmal für 3 × 300 px plus Abstände. Die dritte Karte steht dann allein in der zweiten Zeile. stretchFewItems behandelt nur 1 oder 2 Items. → Fix: Entweder MAX_RELATED_POSTS = 2 setzen (passt zum vorhandenen --pair-Layout) oder für die Related-Section ab 769 px ein eigenes repeat(3, 1fr) vorgeben. Das ist eine Designentscheidung des Autors.

setCopied(true);
clearTimeout(copiedTimeout.current);
copiedTimeout.current = setTimeout(() => setCopied(false), 2000);
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In einem unsicheren Kontext (http-Preview) oder bei verweigerter Berechtigung passiert beim Klick sichtbar nichts. Auch „Copied!“ wird ohne aria-live nicht vorgelesen. → Fix: Ein Element mit aria-live="polite" ergänzen und im Fehlerfall z. B. „Copy failed“ anzeigen.

async function handleNativeShare() {
try {
await navigator.share({ title, url });
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.share verschluckt alle Fehler — ShareButtons/index.tsx:35 Nur AbortError (Abbruch durch den Nutzer) sollte ignoriert werden. NotAllowedError, DataError usw. gehen sonst verloren.

<a className={styles.button} href={linkedInHref} target="_blank" rel="noopener noreferrer">
LinkedIn
</a>
<a className={styles.button} href={xHref} target="_blank" rel="noopener noreferrer">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Share-Links ohne sprechenden Namen — ShareButtons/index.tsx:56 Ein Screenreader liest nur „Link, X“ vor. Es fehlt der Hinweis, dass der Link teilt und einen neuen Tab öffnet. → Fix: z. B. aria-label="Share on X (opens in new tab)".

@@ -15,6 +17,8 @@ export default function BlogPostItemWrapper(props: Props): React.JSX.Element {
<>
<img src={assets.image}></img>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hat kein alt, und es wird nicht geprüft, ob überhaupt ein Bild existiert. Das gehört nicht zu diesem Commit, ist aber leicht im selben Zug zu beheben.

readonly relatedItems?: RelatedItems;
};

export default function BlogPostPageWrapper({ relatedItems = [], ...props }: Props): ReactNode {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das Plugin könnte relatedItems in route.context legen, und RelatedArticles liest es mit useRouteContext(). So macht es das Blog-Plugin bereits für blogMetadata. Damit entfielen [BlogPostPage/index.tsx] und [RelatedArticles/context.tsx]. Vor dem Umbau zu prüfen: ob __import-Arrays im Route-Context genauso aufgelöst werden wie in modules.

import type { Content } from '@theme/BlogPostPage';

/** Same shape as the `items` passed to the blog list, tag and author pages. */
export type RelatedItems = readonly { readonly content: Content }[];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RelatedItems kopiert den Typ der items-Prop von BlogPostGrid. Besser den Typ aus BlogPostGrid exportieren und wiederverwenden.

* @param {BlogPost} candidate
*/
function computeScore(post, candidate) {
const tags = new Set(getTagPermalinks(post));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die Tag- und Autoren-Sets des aktuellen Posts werden für jeden Kandidaten neu gebaut (O(N²) Allokationen). Bei der Größe des Blogs ist das irrelevant. Sauberer wäre es, die Sets einmal in getRelatedPosts zu bauen.

Comment thread package.json
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
"typecheck": "tsc",
"test": "node --test plugins"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node --test plugins übergibt ein Verzeichnis. Nur Node 20 durchsucht ein solches Argument rekursiv. Ab Node 21 gilt das Argument als Glob-Muster und trifft nur den Ordner selbst. Node versucht dann, plugins als Modul zu laden, und meldet „Cannot find module“. Die Tests laufen gar nicht erst. engines erlaubt >=20.0, und Node 20 ist seit April 2026 EOL. Lokal nicht reproduziert, weil hier nur Node 20 installiert ist. → Fix: "test": "node --test "plugins/**/*.test.js"". Gleichzeitig engines auf >=22 und die CI-Workflows auf node-version: 22 heben, denn Globs werden erst ab Node 21 unterstützt.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants