DualFroz: a developer's site without a framework, with live data
The personal site of Michał "DualFroz" Fox, founder of VulCode. A PHP router prepares the HTML with the right title and language addresses, the views are plain JavaScript, and the GitHub numbers come from our own API.
A candy jar and a live Discord card
Two things flank the home page and change without the owner touching them: a jar the visitor draws a candy from, and a profile card with data straight from Discord.
The candy jar
- The drawThe visitor clicks "Take a candy", and the server draws a flavour and adds a short note from Michał to it.
- Golden and seasonal candiesSome notes are golden candies, and for Halloween the jar gets flavours of its own and a cobweb.
- Bot protectionEvery draw passes a Cloudflare Turnstile check and per-address limits, and the counter under the jar shows how many candies have been drawn.
- The Candy Jar appOn Android: the last candy, the flavour ranking, recent draws, home screen widgets and alerts for every candy or only a chosen one.
The profile card
- A profile from DiscordThe avatar, badges, server tag and status come live from Discord, and the card refreshes every 30 seconds without reloading the page.
- Links to the brandsDualFroz, VulCode, Aphrodi, Flaem, Plazer and Lunatic Studios. Every link gets a referral code that tells the target site the visitor came from dualfroz.com.
- Listening toA Spotify track with its cover and a progress bar that moves every second, taken straight from the Discord status.
- A conversationBelow, the date he joined Discord and a button that opens his profile to message him right away.
One site, two languages, two addresses
Every view has a Polish and an English translation, and every language has its own address. The server decides which version answers before the browser runs any script.
router.php
- An address with a languagePages have /pl/ and /en/ versions. The Polish home page is dualfroz.com, the English one dualfroz.com/en.
- An address without oneAn old address goes by a 301 redirect to the version with a language, and the home page opens in the last chosen language.
- Versions in the headhreflang tags link both versions. A blog post without a translation has the Polish one only.
The first HTML, ready on the server
The site has no build step and no npm packages. The PHP router assembles the first HTML with everything a search engine and a link preview need, as in the blog head below.
- The head on the server
The title, description, canonical address, language versions and Open Graph are in the first HTML, so a search engine and a link preview see them at once.
- Ready content for crawlers
The content of every page in both languages is saved beforehand as HTML and the server puts it into the page, so a crawler does not have to run any scripts.
- A view without waiting
The view's template, styles and translation arrive in the same HTML, and the browser router builds the page from them and then switches views without a reload.
- Files with a deployment number
Script and style addresses carry the deployment number, so the browser keeps them cached for a year and fetches the new ones right after a deployment.
Our own GitHub statistics API
The code numbers on dualfroz.com, on vulcode.com, in the GitHub profile and on Discord come from one service at ghapi.dualfroz.com, written in plain PHP, without a framework.
- Collects
The collector runs every hour and asks GitHub about the repositories of the tracked accounts and organizations. Only it holds the GitHub token.
- repositories, branches and stars
- commits and lines of code by language
- pull requests to other people's projects
- Stores
After every run it writes ready answers, and once a day it recounts everything from scratch.
- a public version without private repository names
- full data only with an API key
- a summary on Discord after every full recount
- Serves
The API reads nothing but the stored answers, so no site waits for GitHub.
- JSON: summary, languages, frameworks, tests, open source
- SVG cards for the profile README
- a 304 response when the data has not changed
Who uses the numbers
- dualfroz.com
Numbers on the About page, pull requests on the Open Source page and the Discord profile card.
- vulcode.com
Code and open source numbers on the company pages, fetched on every build of the site.
- GitHub profile
A live Discord profile card and a language chart in the profile README.
- Discord
An app with /summary, /langs, /oss and other commands, in any conversation, in Polish or in English.
A blog written on Discord
A post starts as a message in a channel, and the owner's reaction approves it for publishing. Without it nothing reaches the site.
- A message in the channelThe first line is the title, the rest is the body, and images go as attachments.
- The owner's reactionThe bot puts the reaction under the message itself, but only the owner's click publishes the post.
- A thread under the postThe bot opens a thread for the English version, and a pinned message becomes a pinned post.
- A sync every minuteA systemd timer runs a Python script that fetches the approved posts, images and emoji through the Discord API.
- A post in two languagesIt lands on the blog, in the sitemap and in the RSS feed, with hreflang linking the language versions.
- Edits and removalEditing the message changes the text without changing the address, and taking the reaction back removes the post along with its images.
What the site sends to Discord
A message from the form, a visit from a referral link and the statistics summary arrive on Discord as separate messages, as soon as they happen.
The contact form
The form is guarded by Cloudflare Turnstile and a message limit. Discord gets the name, subject, message and reply address, and a copy stays on the server.
Referral links
A short /ref/code address or a ?ref= parameter records where the visitor came from. Every use is a message with sessions, views, whether the visitor is new and the three most used links.
The statistics summary
After every full recount the statistics API sends a summary with the change since the previous recount next to every number, and the title links to the public summary in the API.
When this setup makes sense for a client
PHP with plain JavaScript is not the answer to everything. A comparison with what we usually propose for a company site.
PHP and plain JS, as here
- runs on any server with PHP
- no build step
- zero npm dependencies on the site side
React with prerender, as vulcode.com
- components and types keep it tidy
- the natural habitat of many interactive screens
- static files and Node to build
A one-person or small business site
Outweighs: PHP and plain JS, as here. React works too, but it takes more tooling to build the same thing.
Hosting without Node
Outweighs: PHP and plain JS, as here. An ordinary server with PHP is enough, with no build before every upload.
A larger team
Outweighs: React with prerender, as vulcode.com. Components and types keep order when many people change the same code.
Many interactive screens
Outweighs: React with prerender, as vulcode.com. In plain JavaScript every one of them is written by hand.
Questions about this project
Read next
The other case studies and the services that draw on this work.
Is your project like this one?
Describe what you want to build. We reply within an hour with questions and a free quote.


