VulCode

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.

DualFroz: the home page on a desktop
DualFroz: the home page on a phone

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 on the dualfroz.com home page

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 Discord profile card on the dualfroz.com home page

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.

Eight sections in one navigation

On a computer they all sit in the top menu. On a phone, Home, Projects and About are on the bottom bar, and More opens the rest.

  • Home

    The candy jar, the Discord profile card, the topics he lives by, featured projects, the stack and the latest blog posts.

  • Services

    Websites, apps, dashboards, shops and bots ordered through VulCode, and logos, identity, UI, graphics, video and 3D through Aphrodi.

  • Projects

    VulCode, Aphrodi, Lunatic Studios, Flaem, Plazer, Vulp, Nebula and more, each with his role and the state of the project.

  • Open Source

    Pull requests to other people's projects: the merged ones, in which repositories and languages, counted straight from GitHub.

  • About

    How he thinks, the path that led here, the languages measured across his repositories and the stack laid out by layer.

  • Beyond code

    A small library of short books with its own reader, chess, what he is listening to now and the desk he works at.

  • Blog

    Short posts about what he built and learned, written on Discord, in two languages and in an RSS feed.

  • Contact

    A form with bot protection whose messages land on Discord, an email address and social media profiles.

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.

dualfroz.com
The dualfroz.com home page in Polish on a phone, with the bottom navigation bar

router.php

  1. An address with a languagePages have /pl/ and /en/ versions. The Polish home page is dualfroz.com, the English one dualfroz.com/en.
  2. 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.
  3. Versions in the headhreflang tags link both versions. A blog post without a translation has the Polish one only.
dualfroz.com/en
The dualfroz.com home page in English on a phone, with the bottom navigation bar

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.

dualfroz.com/en/bloghtml
<html lang="en"><title>Blog - Michał &quot;DualFroz&quot; Fox</title><link rel="canonical" href="https://dualfroz.com/en/blog" /><link rel="alternate" hreflang="pl" href="https://dualfroz.com/pl/blog" /><link rel="alternate" hreflang="en" href="https://dualfroz.com/en/blog" /><link rel="alternate" hreflang="x-default" href="https://dualfroz.com/pl/blog" /><meta property="og:locale" content="en_US" /><meta property="og:locale:alternate" content="pl_PL" /><meta property="og:title" content="Blog - Michał &quot;DualFroz&quot; Fox" />
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  1. 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
  2. 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
  3. 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.

On Discord
  1. A message in the channelThe first line is the title, the rest is the body, and images go as attachments.
  2. The owner's reactionThe bot puts the reaction under the message itself, but only the owner's click publishes the post.
  3. A thread under the postThe bot opens a thread for the English version, and a pinned message becomes a pinned post.
On the site
  1. A sync every minuteA systemd timer runs a Python script that fetches the approved posts, images and emoji through the Discord API.
  2. A post in two languagesIt lands on the blog, in the sitemap and in the RSS feed, with hreflang linking the language versions.
  3. 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

PHP and plain JS, as hereReact with prerender, as vulcode.com

Outweighs: PHP and plain JS, as here. React works too, but it takes more tooling to build the same thing.

Hosting without Node

PHP and plain JS, as hereReact with prerender, as vulcode.com

Outweighs: PHP and plain JS, as here. An ordinary server with PHP is enough, with no build before every upload.

A larger team

PHP and plain JS, as hereReact with prerender, as vulcode.com

Outweighs: React with prerender, as vulcode.com. Components and types keep order when many people change the same code.

Many interactive screens

PHP and plain JS, as hereReact with prerender, as vulcode.com

Outweighs: React with prerender, as vulcode.com. In plain JavaScript every one of them is written by hand.

Questions about this project