Skip to content

Reader#2

Merged
ArifulProtik merged 9 commits intomainfrom
reader
Feb 8, 2026
Merged

Reader#2
ArifulProtik merged 9 commits intomainfrom
reader

Conversation

@ArifulProtik
Copy link
Owner

@ArifulProtik ArifulProtik commented Feb 8, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Article editing and updating with live metadata preview.
    • Article sharing menu with social platform options.
    • Action menu for articles with follow, report, and edit options.
    • HTML content sanitization for secure article display.
    • Code block syntax highlighting in articles.
  • Style

    • Modernized font system with variable typefaces.
    • Redesigned color palette and theme tokens.
    • Enhanced editor typography and spacing.
  • Chores

    • Added dependencies for enhanced editor and utilities.
    • Updated development configurations.

This commit introduces several new font packages to the project:
- `@fontsource-variable/inter`
- `@fontsource-variable/jetbrains-mono`
- `@fontsource-variable/source-serif-4`

Additionally, it includes a `SafeHtmlRenderer` component to sanitize and
render HTML content safely, and integrates this into the article view to
prevent potential XSS vulnerabilities. The `min-w-[200px]` class in
`editor-bubble-menu.tsx` has been shortened to `min-w-50` for a more
concise style. The `devtools` plugin in `vite.config.ts` is now
conditionally enabled only in development mode to prevent hydration
mismatches.
Introduces `ArticleMoreButton` and `ArticleShareButton` for article
interactions.
Refactors `ContentBar` to use these new components and the
`useToggleArticleLike` hook.
Updates `ArticleView` to pass necessary props to `ContentBar`.
Adds article query options and related utilities for data fetching and
management.
Implements route context for `QueryClient` and user data.
Sets up route-based data fetching and error handling for the article
page.
This commit introduces significant changes to the article writing and
editing experience.

Key changes include:

- **Renamed "Story" to "Article"**: The terminology has been updated for
  consistency across the application.
- **New Edit Component**: A new `EditComponent` has been created to
  manage the article editing interface.
- **Edit Navbar and Update Button**: Introduced `EditNavbar` and
  `UpdateButton` components to facilitate saving and updating article
  drafts.
- **Refactored Publish Button**: The `PublishButton` has been refactored
  to use a dedicated `usePublishArticle` hook for better data
  management.
- **New Update Article Hook**: Added `useUpdateArticle` hook for
  handling article updates.
- **New Edit Route**: Created a new route `/article/edit/$slug` for
  editing existing articles, including author and authentication checks.
- **Placeholder Text Update**: The placeholder text in the editor has
  been changed from "Tell your story..." to "Write your article...".
- **Image Upload Prompt Update**: The prompt for image uploads has been
  updated to refer to "article" instead of "story".
@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR introduces optional authentication for article viewing, HTML content sanitization, article liking functionality with user-aware state tracking, new article management components (view/edit/share), route restructuring to separate article view and edit paths, updated styling with new font sources, and dependency additions. Most backend files are normalized to use single quotes for consistency.

Changes

Cohort / File(s) Summary
Backend Configuration & Setup
backend/drizzle.config.ts, backend/eslint.config.js, backend/eslint.config.mjs, backend/package.json
Drizzle config now uses satisfies Config type constraint; ESLint config migrated from .mjs to .js with antfu preset and custom rules; .mjs variant removed; dependencies added for elysia, TypeScript ESLint, and HTML sanitization utilities.
Backend Database Schema
backend/src/db/schema/article.ts, backend/src/db/schema/auth.ts, backend/src/db/schema/base.ts, backend/src/db/schema/index.ts, backend/src/db/index.ts
Quote normalization to single quotes across all schema files and database connection configuration; auth.ts adds defaultNow() and onUpdate() for timestamp handling on user/session/account/verification tables.
Backend Middleware & Core Services
backend/src/middlewares/auth.middleware.ts, backend/src/lib/auth.ts, backend/src/lib/sanitize-html.ts, backend/src/services/error.service.ts
New isAuthOptional middleware for optional authentication (returns user or null without throwing); new sanitizeHtml() utility using isomorphic-dompurify with configured allowed tags/attributes; quote normalization in error service; auth.ts public schema field quotes updated.
Backend Controllers & Routes
backend/src/controllers/article.controller.ts, backend/src/controllers/comment.controller.ts, backend/src/controllers/like.controller.ts, backend/src/controllers/upload.controller.ts, backend/src/controllers/controller.ts
Quote normalization across imports and route paths; article controller updated to call GetPostBySlug(slug, user) with isAuthOptional: true flag; PUT/DELETE routes modified to /id/:id path structure.
Backend Services
backend/src/services/article.service.ts, backend/src/services/comment.service.ts, backend/src/services/like.service.ts, backend/src/services/upload.service.ts, backend/src/services/redis.ts
article.service.ts adds HTML sanitization to post creation via sanitizeHtml(content) and extends GetPostBySlug(slug, user) to return isLikedByUser boolean when user provided; other services updated with quote normalization.
Backend Shared Models & Config
backend/src/config.ts, backend/src/shared/article.model.ts, backend/src/shared/comment.model.ts, backend/src/shared/like.model.ts, backend/src/shared/models.ts, backend/src/server.ts, backend/src/index.ts, backend/src/lib/cloudinary.ts
Quote normalization across environment config, validation models, server configuration, and Cloudinary setup; cloudinary.ts adds secure: true flag.
Frontend Type System & API
ui/src/lib/types.ts, ui/src/lib/api.ts, ui/src/lib/query-client.tsx, ui/src/lib/dyajs.ts, ui/src/data/queries/query-keys.ts, ui/src/data/queries/article.ts, ui/src/hooks/use-upload-image.ts
New types for UserPublic, Article, and SingleArticleResponse; API client renamed from api to client with dynamic base URL and request headers hook; new query utilities for article fetch, like toggle, publish, and update mutations with cache invalidation; dayjs date formatting utilities; query client creation helper.
Frontend Article View Components
ui/src/components/article/article-view.tsx, ui/src/components/article/article-more-button.tsx, ui/src/components/article/article-share-button.tsx, ui/src/components/article/content-bar.tsx, ui/src/components/article/safe-html-renderer.tsx, ui/src/components/article/comment-view.tsx
New components for displaying articles (ArticleView), rendering sanitized HTML with syntax highlighting (SafeHtmlRenderer), sharing articles via social platforms (ArticleShareButton), article actions menu (ArticleMoreButton), interactive content bar with likes/comments/share/options (ContentBar), and comment section placeholder (CommentView).
Frontend Write Components
ui/src/components/write/edit-component.tsx, ui/src/components/write/edit-navbar.tsx, ui/src/components/write/update-button.tsx, ui/src/components/write/publish-button.tsx, ui/src/components/write/image-upload.tsx, ui/src/components/write/write-component.tsx, ui/src/components/write/write-navbar.tsx
New edit flow components for article modification; publish-button.tsx refactored to use usePublishArticle() hook with article terminology; update-button.tsx new component for editing article metadata with validation schema and image upload; import path normalizations to kebab-case conventions.
Frontend Editor & Components
ui/src/components/Editor/use-editor.tsx, ui/src/components/Editor/editor-bubble-menu.tsx, ui/src/components/Editor/editor-block-menu.tsx, ui/src/components/Editor/Editor.tsx, ui/src/components/ui/button.tsx, ui/src/components/home/home-component.tsx, ui/src/components/layout/main-header.tsx
Editor hook adds SSR prop support with immediatelyRender control and post-init editable synchronization; import path normalizations; button component adds cursor-pointer utility; UI text updates from "story" terminology to "article"; header text updated to "Our Article".
Frontend Router & Routes
ui/src/routeTree.gen.ts, ui/src/router.tsx, ui/src/routes/__root.tsx, ui/src/routes/_main/article/$slug.tsx, ui/src/routes/article.edit.$slug.tsx, ui/src/routes/article/$slug.tsx, ui/src/routes/_main/route.tsx, ui/src/routes/_main/index.tsx, ui/src/routes/write.tsx
Route tree restructured to separate article view (/_main/article/$slug) and edit (/article/edit/$slug) paths; old /article/$slug route removed; router context enhanced with AppRouteContext including queryClient; loader-based article fetching with 404 handling; edit route with auth enforcement; root route uses context-aware creation; import path updates to lowercase conventions.
Frontend Styling & Config
ui/src/styles.css, ui/src/components/write/image-upload.tsx, ui/package.json, ui/vite.config.ts
Major CSS restructuring: removes @fontsource-variable/noto-sans, adds source-serif-4, inter, jetbrains-mono; introduces font-family CSS variables (sans/serif/mono); updates color token system; adds TipTap editor styling with headings, lists, code blocks, syntax highlighting; new bubble menu styles; Vite config changed to function-based export with conditional devtools plugin in dev mode only; image-upload text updated from "story" to "article".
Dependencies
backend/package.json, ui/package.json
Backend adds: elysia ^1.4.22, @typescript-eslint/eslint-plugin ^8.54.0, @typescript-eslint/parser ^8.54.0, isomorphic-dompurify 2.16.0. Frontend adds: @fontsource-variable/inter, @fontsource-variable/jetbrains-mono, @fontsource-variable/source-serif-4, dayjs.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Browser Client
    participant Router as TanStack Router
    participant QueryClient as React Query
    participant API as Backend API
    participant DB as Database

    User->>Client: Navigate to article slug
    Client->>Router: Route to /_main/article/$slug
    Router->>QueryClient: loader calls GetArticleQueryOptions(slug)
    QueryClient->>API: GET /api/v1/articles/{slug}
    API->>DB: Query article by slug
    DB-->>API: Return article data
    API-->>QueryClient: Return article + isLikedByUser
    QueryClient-->>Router: Cache article data
    Router-->>Client: Data loaded
    Client->>Client: RouteComponent renders
    Client->>Client: useSuspenseQuery(GetArticleQueryOptions(slug))
    Client->>Client: Obtain user from route context
    Client->>Client: Render ArticleView(article, user)
    Client-->>User: Display article with like state

    User->>Client: Click like button
    Client->>QueryClient: useToggleArticleLike mutation
    QueryClient->>API: POST /api/v1/likes
    API->>DB: Insert/delete like record
    DB-->>API: Success
    API-->>QueryClient: Return response message
    QueryClient->>QueryClient: Invalidate article queries
    QueryClient-->>Client: Refetch article data
    Client-->>User: Update like count + heart icon
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Writing Article Completed  #1: Related through article schema and model updates; this PR extends article functionality with liking and optional user context while #1 adds tags column to the same schema.

Poem

🐰 Hop through the sanitized halls, where articles dance in their finest threads—
Optional auth whispers "you may enter," while likes bloom bright and red.
New fonts pirouette gracefully, code blocks gleam in highlighted arrays,
Edit, share, and view with delight in these refactored days. 🎨✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Reader' is vague and generic, failing to convey meaningful information about the substantial changes in the changeset. Use a more descriptive title that captures the main change, such as 'Add article reading and editing features with HTML sanitization' or 'Implement article view, edit, and sharing functionality'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch reader

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/services/article.service.ts (1)

178-185: ⚠️ Potential issue | 🟠 Major

Content is not sanitized on update, unlike on create.

CreatePost sanitizes content via sanitizeHtml(content) (Line 37), but UpdatePost spreads body directly into .set() without sanitizing. Since UpdatePostBody = CreatePostBody.partial(), the content field can be present in an update and will be stored unsanitized, bypassing the protection added in CreatePost.

🔒 Proposed fix
     const [updatedPost] = await db
       .update(article)
       .set({
-        ...body,
+        ...body,
+        ...(body.content ? { content: sanitizeHtml(body.content) } : {}),
         updatedAt: new Date(),
       })
       .where(eq(article.id, id))
       .returning()

You'll also need to add sanitizeHtml to the imports used by UpdatePost (it's already imported at Line 10).

backend/src/controllers/article.controller.ts (1)

32-46: ⚠️ Potential issue | 🟡 Minor

PUT endpoint already updated to new path; DELETE feature not yet implemented in frontend.

The PUT route change to /article/id/:id is confirmed handled—the frontend mutation at ui/src/data/queries/article.ts (lines 66–68) correctly uses .id({ id }).put(data). However, the DELETE endpoint exists in the backend but lacks a corresponding mutation hook in the frontend. The handleDelete function in ui/src/components/article/article-more-button.tsx (line 46) is currently unimplemented (only logs to console). Implement the delete mutation before the DELETE route becomes a breaking change for actual API consumers.

🤖 Fix all issues with AI agents
In `@backend/src/lib/cloudinary.ts`:
- Line 16: The Cloudinary config currently sets secure: false which forces HTTP;
update the configuration in backend/src/lib/cloudinary.ts (the Cloudinary config
object / cloudinary.v2.config call) to set secure: true so uploads/URLs use
HTTPS, and verify any related environment-driven value (e.g., CLOUDINARY_SECURE)
or configuration loader reflects true in production; run a quick test to confirm
generated URLs are https.

In `@backend/src/lib/sanitize-html.ts`:
- Line 35: The ALLOWED_URI_REGEXP constant in sanitize-html.ts is overly
permissive and can bypass DOMPurify's default URI protections; remove the custom
ALLOWED_URI_REGEXP entry from the sanitizer config so DOMPurify's built-in URI
filtering is used, or if you explicitly need only certain schemes replace the
value with a strict allowlist regex that only permits
^(?:https?|mailto|tel|ftp): and nothing else (update any references to
ALLOWED_URI_REGEXP accordingly, e.g., where sanitizeHtml or sanitizerConfig is
constructed).
- Line 30: The project depends on a pre-release of isomorphic-dompurify
(currently ^3.0.0-rc.2); update the dependency to a stable, pinned version
(e.g., "isomorphic-dompurify": "2.35.0") in package.json and regenerate the
lockfile so sanitize functions in backend/src/lib/sanitize-html.ts import a
stable release; if v3 is required, pin the exact v3 RC tag instead of using a
caret range to avoid accidental upgrades.

In `@package.json`:
- Around line 15-18: Add "elysia" as a direct dependency in the backend
package.json (so backend code that imports elysia in files like
backend/src/server.ts and controllers/middleware/services resolves correctly)
and remove the unnecessary "marklink-monorepo": "." entry from the root
package.json; update backend/package.json's "dependencies" to include the
appropriate elysia semver and delete the root-level "marklink-monorepo"
dependency entry to avoid the implicit hoisting reliance.

In `@ui/src/components/article/article-share-button.tsx`:
- Around line 27-30: The shareUrl is being built from articleID which yields
broken links because routing uses slug; update the shareUrl construction in
article-share-button (the shareUrl constant) to use the article's slug instead
of articleID (or accept a slug prop if the component currently only receives
articleID). Locate where articleID is passed into the component and replace
usage in shareUrl with slug (or add a new slug prop and use
`${window.location.origin}/article/${slug}` with the same window check) so
shared links point to the routed slug path.

In `@ui/src/components/article/safe-html-renderer.tsx`:
- Around line 35-44: The SafeHtmlRenderer currently injects htmlContent directly
via dangerouslySetInnerHTML; add a client-side DOMPurify sanitization step as a
last line of defense: import DOMPurify, compute a memoized safeHtml =
DOMPurify.sanitize(htmlContent) (e.g., via useMemo) inside the SafeHtmlRenderer
component, and replace dangerouslySetInnerHTML={{ __html: htmlContent }} with
dangerouslySetInnerHTML={{ __html: safeHtml }}; ensure you keep the existing
containerRef and className usage and add a short JSDoc on the htmlContent prop
stating it should be sanitized upstream as well.

In `@ui/src/components/Editor/use-editor.tsx`:
- Line 17: The mapping for the editor option is inverted: currently you pass
immediatelyRender: ssr which sets immediatelyRender true during SSR and causes
hydration mismatches; update the code that builds the Tiptap options in
useEditor (the place where ssr?: boolean is declared and passed) to set
immediatelyRender: !ssr so that SSR (ssr === true) results in immediatelyRender
false; locate the useEditor initialization where immediatelyRender is assigned
and flip the boolean expression from ssr to !ssr.

In `@ui/src/data/queries/query-keys.ts`:
- Around line 1-3: QUERY_KEYS.GET_ARTICLE is currently a static array which
causes nested arrays when used as [QUERY_KEYS.GET_ARTICLE, slug] and breaks
TanStack Query caching; change QUERY_KEYS.GET_ARTICLE into a function factory
that returns a flat tuple (e.g., GET_ARTICLE: (slug) => ['article', slug] as
const) and update the consumer in article query to pass the slug by calling
QUERY_KEYS.GET_ARTICLE(slug) for the queryKey while keeping existing
invalidation calls unchanged.

In `@ui/src/lib/api.ts`:
- Line 5: The client creation uses a hardcoded base URL; update the call to
treaty in client (the exported constant client created via treaty<App>) to read
the base URL from environment/config instead of "http://localhost:3000" (e.g.,
use your build-time env like import.meta.env.VITE_API_URL or
process.env.REACT_APP_API_URL depending on the app) and provide a sensible
fallback (or throw a clear error) so staging/production deploys pick up the
correct endpoint; ensure you update any type or usage that expects the string so
treaty<App>(baseUrl, {...}) still receives a string from the env variable.

In `@ui/src/styles.css`:
- Around line 170-175: The .tiptap pre/code rules are duplicated and
conflicting: remove the pre { ... } (and nested code { ... }) block from inside
the `@layer` base definition so there is a single .tiptap pre/code rule (the
external syntax-highlighting block) that acts as the source of truth; then, in
the external .tiptap pre and .tiptap pre code selectors (the syntax-highlighting
block), add any missing utilities from the removed base version (bg-muted or
bg-muted/50 as desired, text-[15px] or text-[14px], and code text-[14px], plus
any border/font utilities) so all intended properties are consolidated in that
one rule.
- Around line 283-300: Add dark-mode overrides for all remaining highlight token
selectors so their lightness is increased to ~0.75 against the dark pre
background; specifically update the .dark .tiptap pre block to include selectors
such as .hljs-attr, .hljs-built_in, .hljs-type, .hljs-params, .hljs-bullet,
.hljs-symbol, .hljs-link, .hljs-meta, .hljs-number, .hljs-literal, .hljs-doctag,
.hljs-regexp, .hljs-addition, and .hljs-selector-tag with oklch colors matching
the existing pattern (e.g., oklch(0.75 0.15 <hue>)) so all token groups use
higher lightness (~0.75) and maintain consistent hues with the already defined
.hljs-keyword/.hljs-string/.hljs-title/.hljs-variable entries.
🟡 Minor comments (17)
backend/src/services/comment.service.ts-46-48 (1)

46-48: ⚠️ Potential issue | 🟡 Minor

InternalServerError at Line 28 gets double-wrapped in the catch block.

The InternalServerError thrown on Line 28 (when newComment is null) will be caught here and re-wrapped in another InternalServerError. Unlike UpdateComment and DeleteComment which re-throw known error types, this catch block doesn't guard against already-handled errors.

Suggested fix
   catch (error) {
+    if (error instanceof InternalServerError) {
+      throw error
+    }
     throw new InternalServerError('Failed to create comment', error)
   }
ui/package.json-17-20 (1)

17-20: ⚠️ Potential issue | 🟡 Minor

New font and utility dependencies look reasonable.

The font packages and dayjs are well-established libraries appropriate for the design system and date formatting needs.

However, the dayjs utility file has a typo in its filename: ui/src/lib/dyajs.ts should be renamed to ui/src/lib/dayjs.ts to match the package name and improve clarity.

ui/src/lib/dyajs.ts-1-21 (1)

1-21: ⚠️ Potential issue | 🟡 Minor

Filename typo: dyajs.ts should be dayjs.ts.

The file is named dyajs.ts (transposed letters) instead of dayjs.ts. This will cause confusion for anyone searching for or importing this module.

ui/src/lib/dyajs.ts-10-13 (1)

10-13: ⚠️ Potential issue | 🟡 Minor

Future timestamps will pass the < 7 check since diff returns a negative value.

If timestamp is in the future, diffInDays will be negative (e.g., -2), which satisfies < 7, causing fromNow() to return strings like "in 2 days". If this is unintended, use Math.abs(diffInDays) or add a guard.

backend/package.json-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor

ESLint packages belong in devDependencies, not dependencies.

@typescript-eslint/eslint-plugin and @typescript-eslint/parser are development-time linting tools. Placing them in dependencies means they'll be installed in production builds, increasing image/bundle size for no benefit.

Proposed fix
  "dependencies": {
    "@bogeychan/elysia-logger": "^0.1.10",
    "@elysiajs/cors": "^1.4.1",
    "@elysiajs/openapi": "^1.4.14",
    "@elysiajs/server-timing": "^1.4.0",
-   "@typescript-eslint/eslint-plugin": "^8.54.0",
-   "@typescript-eslint/parser": "^8.54.0",
    ...
  },
  "devDependencies": {
    "@antfu/eslint-config": "^7.2.0",
+   "@typescript-eslint/eslint-plugin": "^8.54.0",
+   "@typescript-eslint/parser": "^8.54.0",
    ...
  }
ui/src/components/write/publish-button.tsx-111-112 (1)

111-112: ⚠️ Potential issue | 🟡 Minor

Note: The usePublishArticle hook's error toast still says "Failed to publish story".

In ui/src/data/queries/article.ts (Line 55), the onError handler uses toast.error('Failed to publish story') which is inconsistent with the "story → article" terminology update in this file.

ui/src/components/article/article-share-button.tsx-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

XingIcon is the icon for Xing (a social network), not X (formerly Twitter).

Xing is a German professional networking site — this icon won't represent X/Twitter correctly. You likely need a different icon, or an inline SVG for the X logo similar to what was done for Facebook on Line 80.

Also applies to: 85-88

ui/src/lib/types.ts-19-20 (1)

19-20: ⚠️ Potential issue | 🟡 Minor

Change createdAt and updatedAt types to string.

The API response deserializes dates as ISO string values, not Date objects. The @elysiajs/eden client doesn't include response transformers, and the query function doesn't convert these fields. While formatSmartTime() safely handles strings through dayjs(), the type definition should accurately reflect the runtime type to prevent misuse elsewhere.

ui/src/components/article/content-bar.tsx-41-47 (1)

41-47: ⚠️ Potential issue | 🟡 Minor

Hardcoded text-gray-900 fill-gray-600 will break in dark mode.

The rest of the UI uses semantic tokens (text-foreground, text-muted-foreground). Gray-900 is near-black and will be invisible on dark backgrounds. Use theme-aware tokens instead.

💡 Suggested fix
                <HugeiconsIcon
                  className={cn(
                    'size-5',
-                   isLiked && 'text-gray-900 fill-gray-600',
+                   isLiked && 'text-foreground fill-foreground',
                  )}
                  icon={FavouriteIcon}
                />

Adjust the fill token to match your design intent (e.g., fill-muted-foreground for a subtler look).

ui/src/data/queries/article.ts-36-37 (1)

36-37: ⚠️ Potential issue | 🟡 Minor

toast.success(data.data?.message) may display an empty toast.

If data.data is undefined, message will also be undefined, resulting in a toast with no visible text. Consider providing a fallback.

💡 Proposed fix
-      toast.success(data.data?.message)
+      toast.success(data.data?.message ?? 'Like updated')
ui/src/components/write/update-button.tsx-148-179 (1)

148-179: ⚠️ Potential issue | 🟡 Minor

Labels are not associated with their form controls (accessibility).

The <label> elements lack htmlFor attributes and the <Textarea> elements lack corresponding id attributes. Screen readers won't associate the labels with their inputs.

♿ Proposed fix
-                <label
-                  className="text-sm font-medium text-muted-foreground mb-2
-                    block"
-                >
+                <label
+                  htmlFor="update-preview-title"
+                  className="text-sm font-medium text-muted-foreground mb-2
+                    block"
+                >
                   Title
                 </label>
                 <Textarea
+                  id="update-preview-title"
                   value={previewTitle}

Apply similarly for the "Preview Text" label/textarea pair.

ui/src/data/queries/article.ts-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Remove redundant client-side sanitization in GetArticleQueryOptions.

The backend already sanitizes article content on write (in CreatePost at backend/src/services/article.service.ts:37), storing sanitized content in the database. Re-sanitizing the same content on every read in the frontend (line 24) is unnecessary and adds no additional security benefit. Remove the sanitizeHtml(res.data.content) call and return the content as-is since it's already sanitized by the backend.

Relevant code
queryFn: async () => {
  const res = await client.api.v1.article({ slug }).get()
  if (!res.data) throw new Error('Not found')

  return {
    ...res.data,
    content: res.data.content,  // Already sanitized by backend
  }
}
backend/src/db/schema/auth.ts-37-39 (1)

37-39: ⚠️ Potential issue | 🟡 Minor

Add .defaultNow() to session.updatedAt and account.updatedAt for consistency and robustness.

session.updatedAt (line 37) and account.updatedAt (line 66) lack .defaultNow(), while user.updatedAt (line 24) and verification.updatedAt (line 81) include it. All four have .$onUpdate(...).notNull(), but only user and verification provide database-level defaults. Currently, better-auth's drizzleAdapter manages session and account inserts and supplies the value, so there's no immediate runtime issue. However, this inconsistency is fragile: if the schema is later used outside better-auth's control or the integration changes, inserts will fail without explicit updatedAt values. Add .defaultNow() to session and account to match the other tables and ensure the schema is self-contained and defensive.

ui/src/routes/_main/article/$slug.tsx-8-13 (1)

8-13: ⚠️ Potential issue | 🟡 Minor

Catch-all swallows non-404 errors as notFound().

Network failures, 500s, or auth errors from ensureQueryData will all be converted into a "Not found" page. Consider catching only the expected "Not found" error (e.g., check error.message or status code) and re-throwing unexpected errors so they surface properly.

ui/src/routes/__root.tsx-41-41 (1)

41-41: ⚠️ Potential issue | 🟡 Minor

Typo in page title: "Wrire" → "Write".

Proposed fix
-        title: 'MarkLink - Read Wrire Share',
+        title: 'MarkLink - Read Write Share',
ui/src/components/article/article-view.tsx-48-52 (1)

48-52: ⚠️ Potential issue | 🟡 Minor

bg-cover has no effect on <img>; use object-cover.

bg-cover controls background-size, which doesn't apply to <img> elements. You likely want object-cover to maintain aspect ratio while filling the container.

Proposed fix
-          className="aspect-video bg-cover h-78 w-full"
+          className="aspect-video object-cover h-78 w-full"
ui/src/components/article/article-view.tsx-32-36 (1)

32-36: ⚠️ Potential issue | 🟡 Minor

Empty string src causes a spurious HTTP request.

When article.author.image is falsy, src="" makes the browser request the current page URL as an image. Use a placeholder image URL or conditionally render the <img>.

Proposed fix
-            <img
-              src={article.author.image || ''}
-              alt={article.author.name}
-              className="w-10 h-10 rounded-full object-cover"
-            />
+            {article.author.image ? (
+              <img
+                src={article.author.image}
+                alt={article.author.name}
+                className="w-10 h-10 rounded-full object-cover"
+              />
+            ) : (
+              <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-sm font-medium">
+                {article.author.name?.charAt(0)}
+              </div>
+            )}
🧹 Nitpick comments (22)
backend/src/lib/cloudinary.ts (1)

7-11: Consider using the URL API for more robust parsing.

While the quote style changes are fine, the manual string parsing with split('@') and split(':') is fragile and lacks validation. The built-in URL API would handle edge cases more reliably.

♻️ Proposed refactor using URL API
-  if (url.startsWith('cloudinary://')) {
-    const withoutProtocol = url.replace('cloudinary://', '')
-    const [auth, cloudName] = withoutProtocol.split('@')
-    if (auth && cloudName) {
-      const [apiKey, apiSecret] = auth.split(':')
+  if (url.startsWith('cloudinary://')) {
+    try {
+      const parsed = new URL(url)
+      const cloudName = parsed.hostname
+      const apiKey = parsed.username
+      const apiSecret = parsed.password
+      if (apiKey && apiSecret && cloudName) {
       cloudinary.config({
         cloud_name: cloudName,
         api_key: apiKey,
         api_secret: apiSecret,
         secure: false,
         upload_preset: 'ml_default',
       })
+      }
+    } catch (error) {
+      console.error('Failed to parse CLOUDINARY_URL:', error)
     }
   }
ui/src/styles.css (1)

116-190: Blanket !important on nearly every utility inside .tiptap.

Every @apply line uses the ! modifier. While this is common for overriding TipTap/ProseMirror defaults, applying !important to everything (margins, padding, font-size, etc.) makes future style adjustments and component-level overrides difficult. Consider whether a more targeted approach — e.g., increasing selector specificity with .tiptap.ProseMirror or using @layer ordering — could replace the blanket !important for at least some properties.

Not blocking, just something to be mindful of as the editor styles grow.

ui/src/components/layout/main-header.tsx (1)

2-3: Nit: AvaterBtn appears to be a typo for AvatarBtn.

The component name and file avater-btn should likely be avatar-btn. This pre-dates this PR but would be good to fix while renaming files.

ui/src/components/Editor/Editor.tsx (1)

1-23: Import path normalization looks fine, but note partial naming inconsistency.

The sibling files have been renamed to lowercase-hyphenated (editor-bubble-menu, editor-block-menu), but this file and its parent directory retain PascalCase (Editor/Editor.tsx). Consider renaming the directory and file to match for full consistency (e.g., editor/editor.tsx).

ui/src/components/article/comment-view.tsx (1)

1-9: Stub component renders raw article_id to the DOM.

This appears to be a placeholder. Rendering the raw ID is fine for development, but ensure it's not shipped to production as-is — users shouldn't see internal IDs.

backend/src/middlewares/auth.middleware.ts (1)

16-40: Consider handling getSession failures distinctly from "no session".

If auth.api.getSession() throws (e.g., session store unavailable), the exception will propagate as a 500 for both macros. In isAuthOptional, this means a backend failure looks like an auth failure to upstream handlers that expect user: null for unauthenticated users. A try/catch returning { user: null } on infrastructure errors would silently swallow outages, so at minimum ensure there's observability (logging) around session lookup failures.

backend/src/services/error.service.ts (1)

77-77: Rename misspelled function: SetupOnErorrSetupOnError.

The function name has a transposed letter ("Erorr" instead of "Error"). This requires updating the definition, import, and usage across two files:

  • backend/src/services/error.service.ts:77 (definition)
  • backend/src/controllers/controller.ts:1 (import)
  • backend/src/controllers/controller.ts:13 (usage)
Proposed changes

backend/src/services/error.service.ts:

-export const SetupOnErorr = (error: unknown, set: Context['set']) => {
+export const SetupOnError = (error: unknown, set: Context['set']) => {

backend/src/controllers/controller.ts:

-import { HttpError, SetupOnErorr } from '@backend/services/error.service.ts'
+import { HttpError, SetupOnError } from '@backend/services/error.service.ts'
-        return SetupOnErorr(error, set)
+        return SetupOnError(error, set)
backend/src/db/schema/article.ts (1)

39-39: Misleading DB column name: liker_id maps to 'author_id' in the database.

The JS field is liker_id but the underlying Postgres column is author_id. This creates a confusing mismatch — a "liker" is semantically different from an "author." Consider aligning the DB column name to liker_id (with a migration) or renaming the JS field to author_id for consistency.

backend/eslint.config.js (2)

19-19: Remove or fix the commented-out rule — it contains a typo (newlicenewline).

Stale commented-out code with a typo adds confusion. Either remove it or correct and enable it.


28-28: Empty plugins: {} is unnecessary and can be removed.

ui/src/lib/types.ts (1)

8-21: Inconsistent property naming convention — mix of snake_case and camelCase.

preview_image, preview_text, author_id use snake_case while likesCount, createdAt, updatedAt use camelCase. Pick one convention (typically camelCase for TypeScript) and transform at the API boundary.

ui/src/components/article/article-more-button.tsx (2)

34-48: Placeholder handlers — follow, report, and delete actions are console.log stubs.

These will need actual implementations. Consider tracking with TODO comments or issues.

Would you like me to open issues to track implementing these actions?


32-32: Component is tightly coupled to route /_main/article/$slug via useParams.

The slug is only used for the edit navigation. Consider passing slug as a prop instead, which would make this component reusable outside this specific route.

ui/src/components/write/publish-button.tsx (1)

140-153: Duplicated dynamic-height logic between onInput and onFocus handlers.

The same height-auto-then-scrollHeight calculation appears in both handlers. Extract to a helper to reduce duplication.

Proposed refactor
+  const autoResize = (target: HTMLTextAreaElement, maxHeight = 120) => {
+    target.style.height = 'auto'
+    target.style.height = `${Math.min(target.scrollHeight, maxHeight)}px`
+  }
+
   <Textarea
     value={previewTitle}
     onChange={(e) => setPreviewTitle(e.target.value)}
-    onInput={(e) => {
-      const target = e.target as HTMLTextAreaElement
-      target.style.height = 'auto'
-      target.style.height = `${Math.min(target.scrollHeight, 120)}px`
-    }}
-    onFocus={(e) => {
-      const target = e.target as HTMLTextAreaElement
-      target.style.height = 'auto'
-      target.style.height = `${Math.min(target.scrollHeight, 120)}px`
-    }}
+    onInput={(e) => autoResize(e.target as HTMLTextAreaElement)}
+    onFocus={(e) => autoResize(e.target as HTMLTextAreaElement)}
     placeholder="Enter your article title"
     rows={1}
     className="text-lg min-h-9 resize-none overflow-hidden"
   />
ui/src/data/queries/article.ts (1)

42-44: Hardcoded "You are not authorized" for all like errors is misleading.

Network failures, server errors, and rate limits will all show an auth error message. Consider either inspecting the error status/type or using a generic message like 'Failed to update like'.

ui/src/components/write/update-button.tsx (1)

16-36: Significant duplication with PublishButton.

The validation schema, dialog layout, state management pattern, and image upload flow closely mirror PublishButton.tsx. If these diverge over time, bugs fixed in one won't be fixed in the other. Consider extracting a shared ArticleMetadataDialog component or at least a shared schema/validation utility when convenient.

Also applies to: 56-92

ui/src/components/article/content-bar.tsx (1)

28-28: Like button has no guard against rapid clicks.

useToggleArticleLike doesn't expose isPending, so the button isn't disabled during the in-flight mutation. Rapid clicks will fire multiple toggle requests, potentially racing against each other and leaving the like state non-deterministic until the final invalidation settles. Consider destructuring isPending and disabling the button while the mutation is in flight.

💡 Suggested fix
-  const { mutate: toggleLike } = useToggleArticleLike()
+  const { mutate: toggleLike, isPending: isToggling } = useToggleArticleLike()
             <Button
               onClick={() => toggleLike(articleID)}
+              disabled={isToggling}
               variant="link"

Also applies to: 35-48

backend/src/services/article.service.ts (1)

110-158: GetPostBySlug: isLikedByUser check could be folded into the main query.

The separate DB round-trip to check whether the user liked the article works correctly, but for high-traffic read paths this adds latency. Consider using a conditional left join or a correlated subquery in the main query to retrieve this in a single round-trip. Not blocking, but worth considering.

ui/src/components/article/safe-html-renderer.tsx (1)

19-33: Simplify: replace dual useLayoutEffect (no deps) with a single one keyed on htmlContent.

Both useLayoutEffect hooks run on every render because they have no dependency arrays. The version-tracking refs exist solely to deduplicate, but a single effect with [htmlContent] as the dependency achieves the same result more simply and avoids unnecessary work on unrelated re-renders.

♻️ Proposed simplification
-  const contentVersionRef = useRef(0)
-  const lastHighlightedVersionRef = useRef(-1)
-
-  useLayoutEffect(() => {
-    contentVersionRef.current += 1
-  })
-
-  useLayoutEffect(() => {
-    if (
-      containerRef.current &&
-      lastHighlightedVersionRef.current !== contentVersionRef.current
-    ) {
-      containerRef.current.querySelectorAll('pre code').forEach((block) => {
-        hljs.highlightElement(block as HTMLElement)
-      })
-      lastHighlightedVersionRef.current = contentVersionRef.current
-    }
-  })
+  useLayoutEffect(() => {
+    if (containerRef.current) {
+      containerRef.current.querySelectorAll('pre code').forEach((block) => {
+        hljs.highlightElement(block as HTMLElement)
+      })
+    }
+  }, [htmlContent])
ui/src/routes/article.edit.$slug.tsx (1)

8-21: Silent redirect on unauthorized access may confuse users.

Both the unauthenticated case (Line 11) and the non-author case (Line 20) silently redirect to / with no indication of why. Consider using redirect with a search param (e.g., ?error=unauthorized) or using a toast/flash mechanism so the user understands they were redirected.

ui/src/routes/_main/article/$slug.tsx (1)

23-23: Misleading prop name: user passed as author.

The route context's user is the currently authenticated user, not the article's author. Passing it as author is confusing since ArticleView also accesses article.author for the actual author display. The prop is used for ContentBar's userID (i.e., the viewer), so naming it currentUser or viewer in ArticleViewProps would be clearer.

ui/src/components/article/article-view.tsx (1)

53-61: Consider extracting shared ContentBar props to reduce duplication.

Both ContentBar instances receive identical props. A small variable would keep them in sync and reduce the risk of future drift.

Example
+  const contentBarProps = {
+    likesCount: article.likesCount,
+    authorID: article.author_id,
+    articleTitle: article.title,
+    userID: author?.id,
+    onCommentClick: scrollToComments,
+    isLiked: article.isLikedByUser,
+    articleID: article.id,
+  }
+
   ...
-        <ContentBar
-          likesCount={article.likesCount}
-          authorID={article.author_id}
-          ...
-        />
+        <ContentBar {...contentBarProps} />
         <SafeHtmlRenderer htmlContent={article.content} />
-        <ContentBar
-          likesCount={article.likesCount}
-          ...
-        />
+        <ContentBar {...contentBarProps} />

Also applies to: 64-72

Comment on lines +283 to 300
/* Dark Mode Adjustments for Code */
.dark .tiptap pre {
background-color: oklch(0.2 0 0); /* Slightly lighter than pure black */
color: oklch(0.9 0 0); /* Off-white text */

.hljs-strong {
font-weight: 700;
}
.hljs-keyword {
color: oklch(0.75 0.15 300);
}
.hljs-string {
color: oklch(0.75 0.15 150);
}
.hljs-title {
color: oklch(0.75 0.15 250);
}
.hljs-variable {
color: oklch(0.75 0.15 30);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incomplete dark-mode overrides — several token groups will have poor contrast.

The dark-mode block only adjusts 4 token classes (.hljs-keyword, .hljs-string, .hljs-title, .hljs-variable), but the light-mode block defines 8 distinct color groups. The un-overridden groups retain their light-mode colors (lightness 0.45–0.6) against the dark oklch(0.2 0 0) background, which will produce poor contrast:

Missing dark override Light-mode lightness Readability on dark bg
.hljs-attr, .hljs-built_in, .hljs-type, .hljs-params 0.50 borderline
.hljs-bullet, .hljs-symbol, .hljs-link, .hljs-meta, … 0.45 poor
.hljs-number, .hljs-literal, .hljs-doctag, .hljs-regexp 0.60 marginal
.hljs-addition, .hljs-selector-tag 0.55 marginal

Add dark-mode overrides for all token groups, raising lightness to ~0.75 as done for the four already covered.

🎨 Proposed dark-mode additions
 .dark .tiptap pre {
   background-color: oklch(0.2 0 0);
   color: oklch(0.9 0 0);

   .hljs-keyword {
     color: oklch(0.75 0.15 300);
   }
   .hljs-string {
     color: oklch(0.75 0.15 150);
   }
   .hljs-title {
     color: oklch(0.75 0.15 250);
   }
   .hljs-variable {
     color: oklch(0.75 0.15 30);
   }
+  .hljs-number,
+  .hljs-literal,
+  .hljs-doctag,
+  .hljs-regexp {
+    color: oklch(0.75 0.15 150);
+  }
+  .hljs-attr,
+  .hljs-built_in,
+  .hljs-type,
+  .hljs-params {
+    color: oklch(0.7 0.12 40);
+  }
+  .hljs-bullet,
+  .hljs-symbol,
+  .hljs-link,
+  .hljs-meta,
+  .hljs-selector-attr,
+  .hljs-selector-pseudo {
+    color: oklch(0.72 0.14 340);
+  }
+  .hljs-addition,
+  .hljs-selector-tag {
+    color: oklch(0.75 0.15 300);
+  }
+  .hljs-name,
+  .hljs-section,
+  .hljs-selector-id,
+  .hljs-selector-class {
+    color: oklch(0.75 0.15 250);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* Dark Mode Adjustments for Code */
.dark .tiptap pre {
background-color: oklch(0.2 0 0); /* Slightly lighter than pure black */
color: oklch(0.9 0 0); /* Off-white text */
.hljs-strong {
font-weight: 700;
}
.hljs-keyword {
color: oklch(0.75 0.15 300);
}
.hljs-string {
color: oklch(0.75 0.15 150);
}
.hljs-title {
color: oklch(0.75 0.15 250);
}
.hljs-variable {
color: oklch(0.75 0.15 30);
}
}
/* Dark Mode Adjustments for Code */
.dark .tiptap pre {
background-color: oklch(0.2 0 0); /* Slightly lighter than pure black */
color: oklch(0.9 0 0); /* Off-white text */
.hljs-keyword {
color: oklch(0.75 0.15 300);
}
.hljs-string {
color: oklch(0.75 0.15 150);
}
.hljs-title {
color: oklch(0.75 0.15 250);
}
.hljs-variable {
color: oklch(0.75 0.15 30);
}
.hljs-number,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: oklch(0.75 0.15 150);
}
.hljs-attr,
.hljs-built_in,
.hljs-type,
.hljs-params {
color: oklch(0.7 0.12 40);
}
.hljs-bullet,
.hljs-symbol,
.hljs-link,
.hljs-meta,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: oklch(0.72 0.14 340);
}
.hljs-addition,
.hljs-selector-tag {
color: oklch(0.75 0.15 300);
}
.hljs-name,
.hljs-section,
.hljs-selector-id,
.hljs-selector-class {
color: oklch(0.75 0.15 250);
}
}
🤖 Prompt for AI Agents
In `@ui/src/styles.css` around lines 283 - 300, Add dark-mode overrides for all
remaining highlight token selectors so their lightness is increased to ~0.75
against the dark pre background; specifically update the .dark .tiptap pre block
to include selectors such as .hljs-attr, .hljs-built_in, .hljs-type,
.hljs-params, .hljs-bullet, .hljs-symbol, .hljs-link, .hljs-meta, .hljs-number,
.hljs-literal, .hljs-doctag, .hljs-regexp, .hljs-addition, and
.hljs-selector-tag with oklch colors matching the existing pattern (e.g.,
oklch(0.75 0.15 <hue>)) so all token groups use higher lightness (~0.75) and
maintain consistent hues with the already defined
.hljs-keyword/.hljs-string/.hljs-title/.hljs-variable entries.

- Update `isomorphic-dompurify` to the latest version.
- Enable `secure: true` for Cloudinary uploads.
- Restrict `ALLOWED_URI_REGEXP` in `sanitize-html.ts` to
  `https?|mailto|tel|ftp`.
- Correct the `useArticleEditor` hook to render when not SSR.
- Use `slug` instead of `articleID` for sharing and article queries.
- Refactor `QUERY_KEYS.GET_ARTICLE` to use a function for dynamic key
  generation.
- Implement a more robust base URL determination for the API client.
- Add client-side DOMPurify sanitization in `safe-html-renderer.tsx` as
  a fallback.
- Update toasts to correctly refer to "article" instead of "story".
- Remove redundant code block styling from `styles.css`.
@ArifulProtik ArifulProtik merged commit 0b133d9 into main Feb 8, 2026
1 check was pending
@ArifulProtik ArifulProtik deleted the reader branch February 8, 2026 14:51
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