Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Building an Arabic-first RTL news platform with Next.js 16 and Express

Majal Post (مجال بوست) is an Arabic-first news and media platform focused on Lebanese social, economic and environmental issues. I built it as a monorepo: a Next.js frontend, an Express/MongoDB REST API, and a shared Typ

Majal Post (مجال بوست) is an Arabic-first news and media platform focused on Lebanese social, economic and environmental issues. I built it as a monorepo: a Next.js frontend, an Express/MongoDB REST API, and a shared TypeScript types package. This post covers the decisions that mattered most, and what I'd still improve.

The stack

  • Frontend: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4
  • Backend: Node.js with Express 5 (ESM), TypeScript, MongoDB via Mongoose, JWT auth with bcrypt
  • Media: Cloudinary, Multer and Sharp for upload, resizing and thumbnails
  • Email: Nodemailer over SMTP, silently skipped when SMTP isn't configured so the rest of the app keeps working
  • Shared: a private @majalpost/shared package so the frontend and backend agree on types

The product side covers themed categories (Environment & Climate, Society, Economy, Education, Technology, Health), articles with slugs and tags, search, comments, an admin dashboard, and a contributor workflow: a public "become a contributor" application plus per-author "my articles" pages.

Arabic-first means RTL at the root

The simplest RTL decision was also the most important one: set it once, at the top of the tree.

<html lang="ar" dir="rtl">

With dir="rtl" on <html>, flexbox and text flow mirror automatically, so most layouts need no per-component handling. Fonts are loaded through next/font/google with the Arabic subset: Almarai (weights 300 to 800) and IBM Plex Sans Arabic (300 to 700), exposed as CSS variables.

Where it's not finished: I didn't use Tailwind's logical utilities (ms-*, me-*, ps-*, pe-*). A handful of physical ml-/mr-/pl-/pr- utilities remain, and the header uses space-x-reverse. They work today because the site is RTL-only, but logical properties would be the right move if I ever add a left-to-right locale.

An API that assumes the internet is hostile

The backend is where most of the hardening lives, all visible in index.ts and middleware/:

  • Helmet with a content security policy (defaultSrc: 'self', images allowed from self, data:, blob: and the frontend origin) and HSTS (one year, including subdomains, preload).
  • A CORS allowlist driven by FRONTEND_URL (comma-separated), rejecting any other origin.
  • Three rate limiters: 100 requests per 15 minutes on the general API, a stricter 20 per 15 minutes on auth that doesn't count successful requests, and 10 uploads per hour.
  • A 10 MB body limit on JSON and URL-encoded bodies.
  • Role-based access: authorize.ts provides requireAdmin and requireAdminOrEditor middleware that runs after JWT authentication.

Sanitize on the server, not just in the editor

Article bodies and comments are HTML, which is an XSS risk. Article routes sanitize on write: titles and excerpts go through sanitizeText (all markup stripped), and content goes through sanitizeHtml, a DOMPurify instance running on a jsdom window with an allowlist of tags (p, br, strong, em, h1-h6, lists, blockquote, a, img, code, pre) and attributes (href, src, alt, title, class). Comments are sanitized the same way.

This is covered by backend unit tests (run in CI): script tags and inline handlers are stripped, javascript: URLs are blocked, safe links and formatting survive, and Arabic text is preserved unchanged.

What's honestly not done

  • The test suite is small: 11 backend unit tests (sanitization and ObjectId validation). The frontend has none yet.
  • The frontend lint was failing with 11 errors until recently. They're fixed and lint is now a blocking CI step, but 45 warnings remain.
  • The auth rate limiter's comment notes the limit was raised for testing, and it's worth revisiting before treating it as final.

Run it

docker-compose up -d        # MongoDB 7 on localhost:27017
cd backend && npm install && npm run dev     # http://localhost:5000/api
cd frontend && npm install && npm run dev    # http://localhost:3000

Source: https://github.com/HaidarESBER/majalpost

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.