Skip to content

Add article deletion support and 404 page#224

Draft
malkoG wants to merge 7 commits into
hackers-pub:mainfrom
malkoG:feature/article-delete-web-next
Draft

Add article deletion support and 404 page#224
malkoG wants to merge 7 commits into
hackers-pub:mainfrom
malkoG:feature/article-delete-web-next

Conversation

@malkoG

@malkoG malkoG commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires up article deletion on the detail page and adds a proper 404 view for deleted/missing articles.

The PostActionMenu delete button (which uses the existing deletePost mutation) now passes an onDeleted callback that navigates to the author's profile page after successful deletion. Visiting a deleted article's URL shows a "Page Not Found" message instead of a blank page.

Stacks onto #223.

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cae5486-8332-4702-a865-8e91fd864ed6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances article management and user experience by introducing article deletion capabilities and a proper 404 page for missing content. It also provides a dedicated article editing interface and improves the note composition experience by allowing users to easily quote existing posts.

Highlights

  • Article Deletion: Implemented functionality to delete articles directly from the detail page, with a callback to navigate to the author's profile upon successful deletion.
  • 404 Page for Missing Articles: Introduced a dedicated 404 'Page Not Found' view for articles that are deleted or do not exist, improving user experience.
  • Article Editing Page: Added a new dedicated page for editing articles, allowing authors to modify title, content, tags, language, and LLM translation settings.
  • Post Quoting in Note Composer: Enhanced the note composer to support quoting existing posts by pasting their URLs, with a preview of the quoted content.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant new functionality, including article deletion, a 404 page for missing articles, and an article editing page. It also adds the ability to quote posts. The changes are extensive, touching both the GraphQL backend and the SolidJS frontend. The code is generally well-structured, with good separation of concerns (e.g., moving post lookup logic to a dedicated lookup.ts file). My review focuses on a few areas for improvement regarding performance, error handling, and code duplication in the frontend.

Comment thread graphql/lookup.ts
Comment on lines +46 to +48
} catch {
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The catch block is empty. While returning null is the correct behavior for a failed lookup, it's good practice to log the error. This will be very helpful for debugging federation issues in the future. The repository style guide (lines 74-75) recommends using LogTape for structured logging.

Suggested change
} catch {
return null;
}
} catch (error) {
// TODO: Use the app's logger instance if available in the context.
console.error(`Failed to lookup remote object at ${url}`, error);
return null;
}
References
  1. The style guide requires using structured logging via LogTape and including context in error details. The current empty catch block does not log any error information. (link)

Comment thread graphql/post.ts
Comment on lines +373 to +385
toc: t.field({
type: "JSON",
description: "Table of contents for the article content.",
select: {
columns: { content: true },
},
async resolve(content, _, ctx) {
const rendered = await renderMarkup(ctx.fedCtx, content.content, {
kv: ctx.kv,
});
return rendered.toc;
},
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The renderMarkup function is called here to get the table of contents, and it's also called in the content field resolver on lines 356-361. Since renderMarkup can be an expensive operation, calling it twice for the same content is inefficient. Consider caching the result of renderMarkup for the duration of the request to avoid redundant processing. You could potentially attach the result to the content object after the first call and reuse it.

Comment on lines +202 to +361
<Show
when={article().actor.local}
fallback={
<a
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
lang={article().contents?.[0]?.language ??
article().language ?? undefined}
hreflang={article().contents?.[0]?.language ??
article().language ?? undefined}
target="_blank"
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="block p-4"
>
{article().contents?.[0]?.title ?? article().name}
</a>
}
>
{article().contents?.[0]?.title ?? article().name}
</a>
<InternalLink
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
internalHref={`/@${article().actor.username}/${article().publishedYear}/${article().slug}`}
lang={article().contents?.[0]?.language ??
article().language ?? undefined}
hreflang={article().contents?.[0]?.language ??
article().language ?? undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="block p-4"
>
{article().contents?.[0]?.title ?? article().name}
</InternalLink>
</Show>
</h1>
</Show>
<Show
when={article().contents?.[0]?.summary ?? article().summary}
fallback={
<a
href={article().url ?? article().iri}
lang={article().language ?? undefined}
hreflang={article().language ?? undefined}
target={article().contents?.[0]?.url == null
? "_blank"
: undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="px-4 pb-4"
<Show
when={article().actor.local}
fallback={
<a
href={article().url ?? article().iri}
lang={article().language ?? undefined}
hreflang={article().language ?? undefined}
target="_blank"
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="px-4 pb-4"
>
<div
innerHTML={article().content}
class="line-clamp-4 overflow-hidden"
/>
</a>
}
>
<div
innerHTML={article().content}
class="line-clamp-4 overflow-hidden"
/>
</a>
<InternalLink
href={article().url ?? article().iri}
internalHref={`/@${article().actor.username}/${article().publishedYear}/${article().slug}`}
lang={article().language ?? undefined}
hreflang={article().language ?? undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="px-4 pb-4"
>
<div
innerHTML={article().content}
class="line-clamp-4 overflow-hidden"
/>
</InternalLink>
</Show>
}
>
{(summary) => (
<Show
when={article().actor.local}
fallback={
<a
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
innerHTML={summary()}
lang={article().contents?.[0]?.language ??
article().language ?? undefined}
hreflang={article().contents?.[0]?.language ??
article().language ?? undefined}
target="_blank"
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
data-llm-summary-label={t`Summarized by LLM`}
class="prose dark:prose-invert break-words overflow-wrap px-4 pb-4 before:content-[attr(data-llm-summary-label)] before:mr-1 before:text-sm before:bg-muted before:text-muted-foreground before:p-1 before:rounded-sm before:border"
classList={{
"before:border-transparent": !props.hover?.(),
}}
/>
}
>
<InternalLink
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
internalHref={`/@${article().actor.username}/${article().publishedYear}/${article().slug}`}
innerHTML={summary()}
lang={article().contents?.[0]?.language ??
article().language ?? undefined}
hreflang={article().contents?.[0]?.language ??
article().language ?? undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
data-llm-summary-label={t`Summarized by LLM`}
class="prose dark:prose-invert break-words overflow-wrap px-4 pb-4 before:content-[attr(data-llm-summary-label)] before:mr-1 before:text-sm before:bg-muted before:text-muted-foreground before:p-1 before:rounded-sm before:border"
classList={{
"before:border-transparent": !props.hover?.(),
}}
/>
</Show>
)}
</Show>
<Show
when={article().actor.local}
fallback={
<a
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
innerHTML={summary()}
lang={article().contents?.[0]?.language ?? article().language ??
undefined}
hreflang={article().contents?.[0]?.language ??
article().language ??
undefined}
target={article().contents?.[0]?.url == null
? "_blank"
: undefined}
article().language ?? undefined}
target="_blank"
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
data-llm-summary-label={t`Summarized by LLM`}
class="prose dark:prose-invert break-words overflow-wrap px-4 pb-4 before:content-[attr(data-llm-summary-label)] before:mr-1 before:text-sm before:bg-muted before:text-muted-foreground before:p-1 before:rounded-sm before:border"
classList={{ "before:border-transparent": !props.hover?.() }}
/>
)}
</Show>
<a
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
hreflang={article().contents?.[0]?.language ?? article().language ??
undefined}
target={article().contents?.[0]?.url == null ? "_blank" : undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="block p-4 border-t bg-muted text-center group-last:rounded-b-xl"
classList={{
"text-muted-foreground": !props.hover?.(),
"text-accent-foreground": props.hover?.(),
"border-t-muted": !props.hover?.(),
"dark:border-t-black": props.hover?.(),
}}
class="block p-4 border-t bg-muted text-center group-last:rounded-b-xl"
classList={{
"text-muted-foreground": !props.hover?.(),
"text-accent-foreground": props.hover?.(),
"border-t-muted": !props.hover?.(),
"dark:border-t-black": props.hover?.(),
}}
>
{t`Read full article`}
</a>
}
>
{t`Read full article`}
</a>
<InternalLink
href={article().contents?.[0]?.url ?? article().url ??
article().iri}
internalHref={`/@${article().actor.username}/${article().publishedYear}/${article().slug}`}
hreflang={article().contents?.[0]?.language ??
article().language ?? undefined}
on:mouseover={() => props.setHover?.(true)}
on:mouseout={() => props.setHover?.(false)}
class="block p-4 border-t bg-muted text-center group-last:rounded-b-xl"
classList={{
"text-muted-foreground": !props.hover?.(),
"text-accent-foreground": props.hover?.(),
"border-t-muted": !props.hover?.(),
"dark:border-t-black": props.hover?.(),
}}
>
{t`Read full article`}
</InternalLink>
</Show>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There's significant code duplication in how links to articles are rendered. The logic to switch between an internal link (<InternalLink>) for local articles and a standard external link (<a>) for remote articles is repeated for the title, summary, and "Read full article" button. This makes the component harder to read and maintain.

Consider creating a dedicated component, e.g., ArticleLink, that encapsulates this logic. This component would accept the article data and render the appropriate link type, simplifying the ArticleCardInternal component considerably.

For example:

function ArticleLink(props) {
  const isLocal = () => props.article.actor.local;
  // ... other logic
  return (
    <Show when={isLocal()} fallback={<a {...commonProps} target="_blank">{props.children}</a>}>
      <InternalLink {...commonProps}>{props.children}</InternalLink>
    </Show>
  )
}

Then you could use it like <ArticleLink article={article()} ...>{...}</ArticleLink> in all the places where this logic is duplicated.

@malkoG malkoG force-pushed the feature/article-delete-web-next branch from 176583b to a2613d7 Compare April 10, 2026 08:32
@dahlia dahlia force-pushed the main branch 2 times, most recently from ff82b29 to 38a6e99 Compare May 5, 2026 14:12
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.

1 participant