Skip to content

Conversation

@dkildar
Copy link
Collaborator

@dkildar dkildar commented Feb 1, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Full blog post publishing system with rich text editor
    • Tag management with validation and sanitization
    • Table creation and editing capabilities in posts
    • Automatic draft saving to browser storage
    • Markdown and HTML content support
  • Bug Fixes

    • Updated post creation URL routing
    • Improved sidebar layout positioning

✏️ Tip: You can customize this high-level summary in your review settings.

@dkildar dkildar requested a review from feruzm February 1, 2026 13:41
@dkildar dkildar self-assigned this Feb 1, 2026
@dkildar dkildar added the major Breaking changes (1.0.0 → 2.0.0), add this only if any packages/ have major changes in current PR label Feb 1, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive internal publishing workflow for the self-hosted app. It adds new publish-related components, hooks, and utilities while updating configuration and routing to support an internal /publish route instead of redirecting to an external URL. Dependencies are added for TipTap editor, markdown conversion, and tag sanitization.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
apps/self-hosted/config.template.json, apps/self-hosted/package.json
Updated createPostUrl from external Ecency URL to internal "/publish" path. Added 15+ new runtime dependencies (TipTap, DHive, DOMPurify, markdown converters) and 3 type definition packages.
Create Post Button
apps/self-hosted/src/features/auth/components/create-post-button.tsx
Refactored from anchor tag to Link component routing to "/publish". Removed className prop, added conditional rendering based on auth status and blog ownership, updated styling and text display.
Blog Layout
apps/self-hosted/src/features/blog/layout/blog-sidebar.tsx
Changed sticky positioning to fixed for sidebar containers. Standardized translation keys to double quotes and minor formatting adjustments.
Publish Components
apps/self-hosted/src/features/publish/components/...
Added 5 new components: PublishEditor (title + tags + editor), PublishEditorToolbar (formatting controls), PublishEditorTableToolbar (table editing), PublishActionBar (publish button & navigation), PublishTagsSelector (tag input with validation and sanitization).
Publish Hooks
apps/self-hosted/src/features/publish/hooks/...
Added 3 new hooks: usePublishState (localStorage-backed draft management), usePublishEditor (TipTap editor with HTML/Markdown conversion), usePublishPost (React Query mutation for Hive post creation).
Publish Utilities
apps/self-hosted/src/features/publish/utils/...
Added htmlToMarkdown and markdownToHtml converters with alignment and table handling. Added createPermlink utility for generating URL-friendly post slugs from titles.
Publish Feature Exports & Routing
apps/self-hosted/src/features/publish/index.ts, apps/self-hosted/src/routes/publish.tsx, apps/self-hosted/src/routeTree.gen.ts
Created barrel export for publish feature. Added new /publish route with access control (requires blog owner + auth enabled). Updated route tree types and declarations to include PublishRoute integration.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as PublishEditor UI
    participant State as usePublishState<br/>(localStorage)
    participant Editor as usePublishEditor<br/>(TipTap)
    participant Mutation as usePublishPost<br/>(React Query)
    participant Hive as Hive Blockchain

    User->>UI: Enter title, content, tags
    UI->>State: setTitleState, setContentState, setTagsState
    State->>State: Validate & persist to localStorage
    User->>Editor: Edit content in TipTap
    Editor->>State: Convert HTML to Markdown on change
    User->>UI: Click Publish button
    UI->>Mutation: Call usePublishPost with title, content, tags
    Mutation->>Mutation: Validate inputs & generate permlink
    Mutation->>Hive: Broadcast comment operation (post)
    Hive-->>Mutation: Success with permlink
    Mutation->>State: clearAll (clear draft from localStorage)
    Mutation->>User: Navigate to /blog?filter=posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature: Configurable, self host instances #595: Introduced the self-hosted app with initial CreatePostButton and external createPostUrl configuration; this PR replaces the external submit URL with an internal /publish route and associated publishing infrastructure.

Poem

🐰 A writer's dream, now built with care,
With TipTap's touch and formatting flair,
Drafts saved in localStorage's nest,
To Hive they fly, put to the test,
No more redirects to roam and roam! 📝✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Self-hosted editor' accurately describes the main change: implementing a complete editor feature for the self-hosted application with components, hooks, utilities, and routing.

✏️ 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 feature/self-hosted-editor

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
Contributor

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
apps/self-hosted/src/features/auth/components/create-post-button.tsx (1)

12-29: ⚠️ Potential issue | 🟠 Major

Use href prop for external URLs instead of to; when createPostUrl is external, to won't work correctly.

TanStack Link supports external URLs, but requires the href prop (not to). Since createPostUrl defaults to "https://ecency.com/submit" and is configurable, it may be external. The code should detect external URLs and use href instead of to:

const createPostUrl = InstanceConfigManager.getConfigValue(
  ({ configuration }) =>
    configuration.general.createPostUrl || "https://ecency.com/submit",
);
const isExternal = /^https?:\/\//i.test(createPostUrl);

// Only show for blog owner when auth is enabled
if (!isAuthEnabled || !isBlogOwner) {
  return null;
}

return (
  <Link
    {...(isExternal ? { href: createPostUrl } : { to: createPostUrl })}
    {...(isExternal && { target: "_blank", rel: "noopener noreferrer" })}
    className="fixed bottom-6 right-30 z-50 px-4 py-2 flex items-center text-sm !no-underline rounded-full border border-gray-400 dark:border-gray-600 !font-serif"
  >
    <UilPen className="w-4 h-4" />
    <span className="hidden sm:block">{t("create_post")}</span>
  </Link>
);
🤖 Fix all issues with AI agents
In `@apps/self-hosted/package.json`:
- Around line 39-54: Remove the unnecessary devDependency "@types/marked"
(marked ships its own types) by deleting it from devDependencies, and resolve
the speakingurl/type mismatch by either downgrading the "speakingurl" dependency
to a version that matches "@types/speakingurl@13.0.6" (e.g., speakingurl@13.x)
or updating/removing "@types/speakingurl" so types align with
"speakingurl@14.0.1"; update package.json accordingly and run a TypeScript build
to verify the change (look for entries "@types/marked" and "@types/speakingurl"
in devDependencies and the "speakingurl" entry in dependencies to make the
change).

In `@apps/self-hosted/src/features/publish/components/publish-tags-selector.tsx`:
- Around line 46-131: validateTag currently rejects tags with two hyphens
because tag.split("-").length > 2 is wrong, and handlePaste bypasses validateTag
allowing invalid tags; fix by changing the hyphen check in validateTag to count
hyphens (e.g. (tag.match(/-/g) || []).length > 2) so up to two hyphens are
allowed, and update handlePaste to validate each candidate before adding (call
validateTag for each sanitized part or reuse addTag) so pasted tags go through
the same validation and warning logic as manual input (references: validateTag,
handlePaste, addTag, sanitizeTagInput).

In `@apps/self-hosted/src/features/publish/hooks/use-publish-post.ts`:
- Around line 36-42: The first call to createPermlink(title) is dead code
because it's immediately overwritten by createPermlink(title, true); remove the
redundant call and initialize permlink only once (keep the existing
createPermlink(title, true) if you always want a random suffix), or if you need
conditional suffixing, replace with a single conditional call that uses
createPermlink(title) when no suffix is needed and createPermlink(title, true)
when a suffix is required; update the variable declaration around permlink
accordingly.
- Around line 48-49: The payload currently sets parent_author and
parent_permlink to empty strings; for top-level Hive posts parent_permlink must
be the first tag (community/category). In the hook (use-publish-post.ts) where
the post object is built (referencing parent_author and parent_permlink), set
parent_permlink to tags[0] (or tags?.[0]) when parent_author is empty/undefined
and tags exist; ensure you handle the case of no tags by falling back to an
empty string and normalize the tag (e.g., lowercase/trim) before assigning.

In `@apps/self-hosted/src/features/publish/utils/markdown.ts`:
- Around line 81-89: The image rule's replacement inserts alt and src directly
into markdown (in the .addRule("image" replacement function using variables alt
and src), which allows markdown-breaking chars; escape special characters in alt
(at least ']' and backslashes) by backslash-escaping them and ensure src is
safely wrapped or escaped (e.g., wrap the URL in angle brackets <...> or
percent-encode problematic chars like ')' and spaces) before returning
`![alt](src)` so markdown structure cannot be broken or injected into.

In `@apps/self-hosted/src/features/publish/utils/permlink.ts`:
- Around line 24-34: Truncate the base derived from slug before appending the
random suffix so the suffix from permlinkRnd() is preserved: compute
parts/permBase from slug (using parts, perm), if random generate rnd =
permlinkRnd().toLowerCase(), compute allowedBaseLen = 255 - (1 + rnd.length),
truncate permBase to allowedBaseLen (or keep full base if shorter), then set
perm = `${permBase}-${rnd}`; if not random simply ensure perm is no longer than
255 by truncating permBase to 255. Use the variables slug, perm, permBase,
random and function permlinkRnd() to locate and modify the logic.
🧹 Nitpick comments (8)
apps/self-hosted/package.json (1)

21-30: Consider standardizing TipTap package versions for consistency (optional improvement).

While @tiptap packages in the 2.9.x / 2.11.x / 2.12.x range are peer-compatible and won't cause conflicts, pinning all @tiptap/* packages to the same version is a best practice to prevent potential real-world mismatches during updates and to improve maintainability.

apps/self-hosted/src/features/publish/hooks/use-publish-state.ts (1)

12-13: Consider exporting MAX_TITLE_LENGTH to avoid duplication.

This constant is also defined in publish-editor.tsx (line 11). Exporting it from this file would ensure consistency and avoid drift if the value changes.

♻️ Proposed refactor to export constants
-const MAX_TITLE_LENGTH = 255;
-const MAX_TAG_LENGTH = 24;
+export const MAX_TITLE_LENGTH = 255;
+export const MAX_TAG_LENGTH = 24;

Then in publish-editor.tsx:

import { usePublishState, MAX_TITLE_LENGTH } from "../hooks/use-publish-state";
apps/self-hosted/src/features/publish/hooks/use-publish-post.ts (1)

54-55: Redundant check: tags.length > 0 already validated.

Lines 32-34 throw an error if tags.length is 0, so this ternary is always true.

♻️ Simplify
           json_metadata: JSON.stringify({
-            tags: tags.length > 0 ? tags : [],
+            tags,
             app: "ecency-selfhost/1.0",
apps/self-hosted/src/features/publish/hooks/use-publish-editor.ts (2)

67-72: Missing ESLint exhaustive-deps warning: setEditorContent and publishState.content not in dependency array.

While the comment indicates "Only on mount" intent, this pattern can cause subtle bugs if editor is recreated. Consider using a ref to track if initial load has occurred, or add a disable comment with justification.

♻️ Alternative using ref to track initialization
+import { useCallback, useEffect, useRef } from "react";
 
 export function usePublishEditor() {
   const publishState = usePublishState();
+  const initializedRef = useRef(false);

   // ... editor setup ...

   // Load content from state when editor is ready
   useEffect(() => {
-    if (editor && publishState.content) {
+    if (editor && publishState.content && !initializedRef.current) {
+      initializedRef.current = true;
       setEditorContent(publishState.content);
     }
-  }, [editor]); // Only on mount
+  }, [editor, publishState.content, setEditorContent]);

38-43: Consider debouncing content state updates.

onUpdate fires on every keystroke, triggering HTML-to-Markdown conversion and localStorage writes. For large documents, this could impact performance. Consider debouncing the state update.

apps/self-hosted/src/features/publish/components/publish-action-bar.tsx (1)

44-51: Missing to prop on Link component.

The Link component has search but to prop comes after. While this works, having to before search improves readability and follows typical usage patterns.

♻️ Reorder props for clarity
       <Link
+        to="/blog"
         search={{ filter: "posts" }}
         className="text-sm flex items-center gap-2 whitespace-nowrap"
-        to="/blog"
       >
apps/self-hosted/src/routes/publish.tsx (2)

15-30: Move usePublishEditor() below the guard check to avoid initializing the editor for unauthorized users.

The usePublishEditor() hook (line 18) initializes a TipTap editor with multiple extensions before the authorization guard (lines 28-30). For unauthorized users, this allocates resources that are immediately discarded upon redirect.

♻️ Proposed fix
 function RouteComponent() {
   const isBlogOwner = useIsBlogOwner();
   const isAuthEnabled = useIsAuthEnabled();
-  const { editor } = usePublishEditor();
   const navigate = useNavigate();

   // Redirect if auth is disabled or user is not blog owner
   useEffect(() => {
     if (!isAuthEnabled || !isBlogOwner) {
       navigate({ to: "/blog", search: { filter: "posts" } });
     }
   }, [isAuthEnabled, isBlogOwner, navigate]);

   if (!isAuthEnabled || !isBlogOwner) {
     return null;
   }

+  return <AuthorizedPublishContent />;
+}
+
+function AuthorizedPublishContent() {
+  const { editor } = usePublishEditor();
+
   return (
     <div className="min-h-screen bg-theme-primary">

11-13: Consider using TanStack Router's beforeLoad for route-level authorization.

The current useEffect-based redirect works but the component still mounts and hooks execute before the redirect. TanStack Router's beforeLoad can prevent unauthorized access at the route level, before the component renders.

♻️ Example using beforeLoad
export const Route = createFileRoute("/publish")({
  beforeLoad: ({ context }) => {
    // Access auth state from router context
    const { isAuthEnabled, isBlogOwner } = context;
    if (!isAuthEnabled || !isBlogOwner) {
      throw redirect({ to: "/blog", search: { filter: "posts" } });
    }
  },
  component: RouteComponent
});

This requires setting up the auth state in your router context, but provides cleaner separation of authorization from rendering logic.

Also applies to: 21-26

Comment on lines +36 to +42
// Generate permlink from title
let permlink = createPermlink(title);

// If permlink already exists or is too short, add random suffix
// In a real implementation, you might want to check if permlink exists
// For now, we'll add random suffix to ensure uniqueness
permlink = createPermlink(title, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Dead code: first createPermlink call is immediately overwritten.

Line 37 creates a permlink that is immediately replaced on line 42. The first call serves no purpose.

🧹 Proposed fix to remove dead code
-      // Generate permlink from title
-      let permlink = createPermlink(title);
-
-      // If permlink already exists or is too short, add random suffix
-      // In a real implementation, you might want to check if permlink exists
-      // For now, we'll add random suffix to ensure uniqueness
-      permlink = createPermlink(title, true);
+      // Generate permlink from title with random suffix for uniqueness
+      const permlink = createPermlink(title, true);
📝 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
// Generate permlink from title
let permlink = createPermlink(title);
// If permlink already exists or is too short, add random suffix
// In a real implementation, you might want to check if permlink exists
// For now, we'll add random suffix to ensure uniqueness
permlink = createPermlink(title, true);
// Generate permlink from title with random suffix for uniqueness
const permlink = createPermlink(title, true);
🤖 Prompt for AI Agents
In `@apps/self-hosted/src/features/publish/hooks/use-publish-post.ts` around lines
36 - 42, The first call to createPermlink(title) is dead code because it's
immediately overwritten by createPermlink(title, true); remove the redundant
call and initialize permlink only once (keep the existing createPermlink(title,
true) if you always want a random suffix), or if you need conditional suffixing,
replace with a single conditional call that uses createPermlink(title) when no
suffix is needed and createPermlink(title, true) when a suffix is required;
update the variable declaration around permlink accordingly.

Comment on lines +48 to +49
parent_author: "",
parent_permlink: "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

parent_permlink should be the first tag for top-level Hive posts.

For top-level posts on Hive, parent_permlink should be set to the first tag (community/category), not an empty string. An empty parent_permlink may cause the post to not be indexed or displayed correctly.

🐛 Proposed fix
       const postOp: Operation = [
         "comment",
         {
           parent_author: "",
-          parent_permlink: "",
+          parent_permlink: tags[0],
           author: user.username,
📝 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
parent_author: "",
parent_permlink: "",
const postOp: Operation = [
"comment",
{
parent_author: "",
parent_permlink: tags[0],
author: user.username,
🤖 Prompt for AI Agents
In `@apps/self-hosted/src/features/publish/hooks/use-publish-post.ts` around lines
48 - 49, The payload currently sets parent_author and parent_permlink to empty
strings; for top-level Hive posts parent_permlink must be the first tag
(community/category). In the hook (use-publish-post.ts) where the post object is
built (referencing parent_author and parent_permlink), set parent_permlink to
tags[0] (or tags?.[0]) when parent_author is empty/undefined and tags exist;
ensure you handle the case of no tags by falling back to an empty string and
normalize the tag (e.g., lowercase/trim) before assigning.

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

Labels

major Breaking changes (1.0.0 → 2.0.0), add this only if any packages/ have major changes in current PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants