<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Huvanti]]></title><description><![CDATA[Huvanti]]></description><link>https://huvanti.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a97eeed33f3f8a1bdfaa43e/8b9f3360-f30f-4f77-b60d-09a6083dbe10.png</url><title>Huvanti</title><link>https://huvanti.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 04 Sep 2026 23:05:57 GMT</lastBuildDate><atom:link href="https://huvanti.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why I wrote my own CMS instead of installing another plugin]]></title><description><![CDATA[Every developer who has run a content site long enough arrives at the same fork in the road. Something breaks, you diagnose it, and the fix turns out to be a fourth plugin that patches the interaction]]></description><link>https://huvanti.hashnode.dev/why-i-built-my-own-laravel-cms</link><guid isPermaLink="true">https://huvanti.hashnode.dev/why-i-built-my-own-laravel-cms</guid><category><![CDATA[Laravel]]></category><category><![CDATA[PHP]]></category><category><![CDATA[material ui]]></category><category><![CDATA[cms]]></category><category><![CDATA[cms development]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[open source]]></category><dc:creator><![CDATA[Pritam Sarkar]]></dc:creator><pubDate>Wed, 02 Sep 2026 09:54:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a97eeed33f3f8a1bdfaa43e/54ccf0e4-c7ea-4cd0-9d60-5e4f9b2c1f59.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every developer who has run a content site long enough arrives at the same fork in the road. Something breaks, you diagnose it, and the fix turns out to be a fourth plugin that patches the interaction between the second and the third. At that point you either accept the stack as permanent weather, or you build.</p>
<p>I built. The result is Huvanti, a self hosted publishing platform on Laravel 13, PHP 8.3, Tailwind CSS 4 and Vite. It runs my live publication at <a href="https://huvanti.com"><strong>https://huvanti.com</strong></a> every day, and the source is public at <a href="https://github.com/pritamsarkar711/project15"><strong>https://github.com/pritamsarkar711/project15</strong></a>.</p>
<p>This post is the engineering account, not the marketing one. Architecture decisions, the tradeoffs behind them, the things I would defend in code review, and the two choices I know a lot of you will disagree with.</p>
<h2>The shape of the system</h2>
<p>One Laravel codebase serves three surfaces.</p>
<p>A public blog front end for readers, server rendered Blade with Tailwind, no client side framework. An admin CMS at a prefixed route group covering posts, categories, pages, navigation, comments, contacts, feedback, ads and settings. An author dashboard, a separate surface with its own controller and its own permissions, where contributors draft and submit but never publish directly.</p>
<p>The important structural decision is that the author dashboard is not the admin panel with buttons hidden by a role check. It is a distinct route group with a distinct controller, because permission by CSS visibility is not permission. Anything an author can reach is reachable because that controller exposes it, not because a Blade conditional forgot to run.</p>
<h2>The SEO analyzer, and why it exists twice</h2>
<h3>The problem with client side scoring</h3>
<p>Most editor SEO scores live entirely in JavaScript. They are useful while you type and worthless the moment the tab closes. You cannot query them, sort by them, or audit them.</p>
<h3>How Huvanti handles it?</h3>
<p>The analyzer exists in two places on purpose. A JavaScript module runs in the editor for instant feedback while typing. A PHP service mirrors that logic one to one on the server, runs on save, and persists the result to a column on the post row.</p>
<p>That mirror is the whole point. The score becomes a queryable field. It appears as a badge in list views. Six months later you can answer why an article shipped at 62 by reading a row instead of interrogating a colleague.</p>
<h3>What it grades?</h3>
<p>Sixteen checks across four weighted blocks. Title and meta carries the most: focus keyword in the post title, the SEO title, the meta description and the slug, plus length bands of 30 to 65 characters for the title, 30 to 60 for the SEO title, and 120 to 165 for the meta description. Then keyword placement: keyword inside the first ten percent of the body, density between 0.5 and 3 percent, keyword present in at least one H2 or H3. Then depth: 600 words minimum, two subheadings minimum. Then links and media: at least one internal link, at least one external link, alt text on every image.</p>
<p>Two implementation details worth stealing. Density counting distinguishes single word keywords from phrases, using a word boundary regex for the former and substring counting for the latter, because naive substring matching on a single word inflates density badly on common terms. And when no focus keyword is set, the displayed score is capped at 24 even if other checks pass, so an unoptimised draft can never wear a respectable number.</p>
<h2>IndexNow, and a failure policy I would defend anywhere</h2>
<p>An Ahrefs audit on my old site flagged that changed pages were never submitted to IndexNow. New articles waited on crawler mood, which meant days.</p>
<p>Now every publish, update and delete submits the affected URL to the shared IndexNow endpoint, which fans out to Bing, Yandex, Seznam and Naver. Admins and authors both get manual submit buttons, single and bulk, for migrations.</p>
<p>The policy around it matters more than the feature. The HTTP call has a two second connect timeout and a three second total timeout, sits inside a try catch, and logs a warning on failure. It never throws, never retries inline, and never surfaces an error to the user.</p>
<p>The reasoning is simple. Indexing notification is a side effect. Saving the post is the transaction. A side effect that can fail the transaction is a bug waiting for a bad network day. If you take one thing from this post into your own codebase, take that.</p>
<h2>Social automation without the duplicate post disaster</h2>
<p>Publishing pushes the post to X, Facebook Pages, LinkedIn, Instagram, Telegram and Pinterest. No SDKs, no packages, direct HTTP against each API, including a hand rolled OAuth 1.0a signature for X because that is what X still requires.</p>
<p>The constraint that took the most thought was idempotency. An author edits a published post at 9pm. Should that trigger another share? Obviously not, but the naive implementation does exactly that.</p>
<p>Huvanti keeps a row per post per network. A successful delivery is recorded and never repeated unless an admin explicitly forces it. Each attempt logs its error text and offers a retry. Every network call is isolated, so a Pinterest token expiring cannot stop the LinkedIn post, and none of them can stop the save.</p>
<p>Credentials are AES 256 GCM encrypted at rest, masked in the UI, and every network has a one click test connection action. A silent auth failure is worse than no automation, because it teaches you to trust something that is not working.</p>
<h2>The editor decision, stated honestly</h2>
<p>I wrote the rich text editor from scratch. Around 1800 lines of vanilla JavaScript, zero dependencies. Formatting, colour and highlight pickers, lists, indentation, alignment, tables, superscript and subscript, find and replace, source view, full screen, keyboard shortcuts, autosave with crash recovery.</p>
<p>The argument against is real and I will state it fairly. Editor libraries are mature, well tested, and handle contenteditable edge cases that took me weeks. I reinvented a wheel.</p>
<p>The argument for is the one I still believe. The editor is the fastest moving dependency in any CMS and the single most common cause of breakage two years into a project. An upstream API change should never be able to break my publish button. I traded upfront cost for the ability to fix any bug myself in an afternoon.</p>
<p>Roughly half the developers who have looked at this think I chose wrong. I would rather say that out loud than pretend the decision was obvious.</p>
<h2>AI with a narrow, defensible scope</h2>
<p>The assistant drafts meta titles, meta descriptions, focus keyword candidates, excerpts and FAQ blocks. It does not write articles, and I will not ship that.</p>
<p>The client is provider agnostic against any OpenAI compatible endpoint: NVIDIA NIM on its free tier, Groq, OpenRouter, OpenAI. The admin lists models in priority order. A model that errors, times out or rate limits is marked down in cache for ten minutes and the next one takes over transparently. The last known good model is cached and tried first, which cuts latency noticeably.</p>
<p>Keys are AES 256 GCM encrypted, never sent to the browser, every call proxied server side, with per user daily quotas. The browser never learns the key, the base URL or the model list.</p>
<h2>Other things worth mentioning briefly</h2>
<p>Uploaded images convert to WebP at quality 78 and cap at 1600 pixels wide, with unicode filenames preserved. Four colour themes and three structural templates compose freely, driven from a single palette source, so restyling the entire product including both panels in both colour modes is a settings dropdown. Auth covers bcrypt, TOTP two factor, CSRF, throttled login and registration, and server side HTML sanitisation on all submitted content. Deployment targets shared hosting through Git auto deploy with a vendored dependency directory and committed build assets, so no composer or npm runs on the server.</p>
<h2>What it deliberately is not?</h2>
<p>Not a page builder. Not multisite. No plugin marketplace, and that absence is the entire thesis of the project. Not ecommerce. Every one of those is a decision, and each one is why the codebase is still legible to me.</p>
<h2>The takeaway</h2>
<p>Building your own CMS is bad advice for most people and it was right for me, and the difference is entirely about whether the thing you are replacing is core to your work or incidental to it. Publishing is my work. Owning it changed how often I ship, because the tax on shipping fell close to zero.</p>
<p>Every article on <a href="https://huvanti.com"><strong>https://huvanti.com</strong></a> is written, scored, indexed and distributed through this exact code, which is the only product endorsement I trust from anybody.</p>
<p>Code, issues and pull requests: <a href="https://github.com/pritamsarkar711/project15"><strong>https://github.com/pritamsarkar711/project15</strong></a><br />Live implementation: <a href="https://huvanti.com"><strong>https://huvanti.com</strong></a></p>
<p>If you have shipped a custom editor and regretted it, I would genuinely like to hear where it went wrong for you. That is the decision I am least certain about, and the comments are the cheapest place to learn.</p>
]]></content:encoded></item></channel></rss>