When I rebuilt my video downloader for plain shared hosting, I lost the two things I had taken for granted: Node.js and a build step. The hosting plan ran PHP and nothing else. No Composer, no npm, no terminal on the server, no deployment pipeline. Just a file manager and SFTP.
My first instinct was to fight that. I looked for ways to sneak a build process in, or to run Composer locally and upload the vendor folder. Both work, technically. Both also add a layer of things that can break, and on a shared host I could not see why they broke. So I went the other way: I designed the project to need nothing the server does not already have.
The structure is deliberately boring. The site root holds the public pages — index.html, tiktok-downloader.html, instagram-downloader.html, and so on. Each page is a plain HTML file with its own CSS and a small script, because shared hosting serves static files fast and there is nothing to compile. One folder, api/, holds the PHP. Another, assets/, holds the shared JavaScript, icons, and the manifest. That is most of the project.
The PHP side has exactly three kinds of file. api/config.php holds the secrets: the RapidAPI keys and a few settings. It is blocked by .htaccess so the web server never serves it, and it is never committed anywhere public. api/resolve.php takes a pasted video link, calls the right provider, and returns the download options as JSON. api/download.php fetches the file and streams it to the visitor. Keeping resolve and download separate was the single best decision I made. One endpoint talks to APIs; the other moves bytes. When something breaks, I know which half to look at.
Shared files live in api/lib/, one file per job: a tiny HTTP helper built on cURL, a rate limiter that counts requests in a flat file, and a logger that writes provider failures to a text file without recording anything about the visitor. Every file starts with require_once lines at the top. No autoloader, no framework, no magic. If I open a file, I can see exactly what it depends on.
The frontend follows the same rule. There is one shared JavaScript engine, assets/tool.js, that every tool page loads. It handles the paste box, the resolve call, the quality list, and the error messages. Each page only supplies its own configuration — which platform it is, what the heading says. That way a fix to the error handling lands on all eight tool pages at once, and I never have to edit the same code in eight places.
Uploading is manual and that is fine. My checklist is short: assets first, then pages, then cache-bust the changed script with a ?v= query string so visitors get the new file instead of a stale cached one. I never touch api/ when I am only updating content pages, and I never upload the whole project when one file changed. There is no deploy script because the deploy is five minutes of dragging files, and a script would only hide mistakes.
The tradeoff is real. I write more boilerplate than a framework would. There is no type checking, no tests running in CI, no dependency updates to pull. But the site has run for weeks on a cheap shared plan with nothing to keep alive. If the host moves me to another server, I copy the folder and it works. If PHP gets updated, my code uses nothing exotic enough to break.
Not every project fits this shape. The moment I need a database, background jobs, or a team, I would reach for more tooling. But for a small tool that does one job — take a link, return a file — a folder of plain PHP, some flat files, and an upload checklist has been the most reliable setup I have used. The hosting forced the simplicity on me, and the simplicity turned out to be the point.
I run this on ramrom.io, and the structure above is the whole of it: HTML pages, a handful of PHP files, and no build step anywhere in sight.
Top comments (0)