Skip to content

feat(articles): long-form text expression — compose, publish, read - #424

Merged
catomean merged 1 commit into
mainfrom
feat/articles-long-form
Jul 22, 2026
Merged

feat(articles): long-form text expression — compose, publish, read#424
catomean merged 1 commit into
mainfrom
feat/articles-long-form

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

The north star: OrangeCat as the best place to express thoughts via text

An article is a long-form POST on timeline_events (metadata.is_article), not a new ENTITY_REGISTRY entity. One row, free feed distribution, ~¼ the code of a new entity — reuses the createEventWithVisibility pipeline verbatim (no migration, no RPC change; metadata carries slug / cover / reading-time / markdown body).

What's in it

  • config/articles.ts — SSOT (limits, slug + reading-time helpers, copy)
  • services/articles/*publishArticle (client) + getArticleBySlug / listPublishedArticles (server)
  • /articles — index of published articles
  • /articles/[slug] — x.ai/Substack-grade reader: design-tokens only, byline→profile, cover, reading time, OG + JSON-LD; private/followers gated to author
  • /articles/new — focused composer with live Write/Preview, visibility, cover
  • PostContent.tsx — feed posts flagged is_article render a "Read article" card → reader
  • routes.tsARTICLES / ARTICLES_NEW / ARTICLE(slug)

Deliberate calls

  • Markdown safety: reader uses react-markdown + rehype-sanitize + remark-gfm, not next-mdx-remote — MDX parses arbitrary user < as JSX (unsafe/fragile for user content). react-markdown never executes JSX and sanitizes HTML.
  • No migration: the feed RPC only persists metadata verbatim (it derives content from description and can't set event_subtype), so the article payload rides in metadata. A slug→indexed column can be promoted later via expand/contract if volume warrants.

Verification

  • type-check + lint clean; production build green (routes: /articles ƒ, /articles/[slug] ƒ, /articles/new ○); 777 unit tests pass; SSR + graceful empty-state smoke.
  • Authed publish + community-feed appearance to be verified post-deploy via UI (project convention for DB-touching changes).

🤖 Generated with Claude Code

The north-star text surface: articles as long-form POSTS on timeline_events
(metadata.is_article), NOT a new ENTITY_REGISTRY entity. One row, free feed
distribution, ~¼ the code of a new entity — reuses the createEventWithVisibility
pipeline verbatim (no migration, no RPC change; metadata carries slug/cover/
reading-time/markdown body).

- config/articles.ts: SSOT (limits, slug + reading-time helpers, copy)
- services/articles: publishArticle (client) + getArticleBySlug/listPublishedArticles (server)
- /articles: index of published articles
- /articles/[slug]: x.ai/Substack-grade reader — safe markdown via react-markdown
  + rehype-sanitize + remark-gfm (never executes JSX on user input), design
  tokens only, byline→profile, cover, reading time, OG + JSON-LD, private/
  followers gated to author
- /articles/new: focused composer with live Write/Preview, visibility, cover
- PostContent: feed posts flagged is_article render a "Read article" card → reader
- ROUTES: ARTICLES / ARTICLES_NEW / ARTICLE(slug)

Verified: type-check + lint clean, production build green (all 3 routes), SSR
render + graceful empty-state smoke. Authed create + feed appearance to verify
post-deploy via UI (project convention for DB-touching changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@catomean
catomean merged commit b4c74fb into main Jul 22, 2026
4 of 5 checks passed
@catomean
catomean deleted the feat/articles-long-form branch July 22, 2026 15:16
{coverImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={coverImage}
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