the site had "world.execute(me); Mili" hardcoded in the music widget. literally hardcoded. always that song, no matter what i was actually listening to. it was embarrassing. >_<
so i decided to fix it for real, wanted the widgets to reflect what i actually play and study in real time. simple to explain, less simple to do on a 100% static site on github pages with zero backend.
checking the options
each widget has a different data source, and on a static site u cant do anything server-side. the viable options were:
| Widget | Best option | How it works | Difficulty |
|---|---|---|---|
| now listening | Last.fm + Web Scrobbler | scrobbler extension to Last.fm, site fetches their API | lazy u_u |
| now playing | Lanyard | discord detects the game via rich presence, lanyard exposes it as public API | low |
| now studying | manual status.json | JSON file in the repo, site fetches it, i update via git push | very low |
steam API would be another option for gaming, but it has CORS blocked and would need a proxy. Lanyard has open CORS, its free, open source, and discord already detects games automatically. obvious choice.
discord rich presence only works with the desktop app, not the web one. and on linux game detection is irregular, native steam games usually work, but games via proton/wine are hit or miss, discord sometimes sees the wine process instead of the actual game. on my nobara linux its been working reasonably well.
the implementation
the code part was chill. three changes:
1. created status.json at the site root:
{
"studying": "Lua & C & C++"
}
2. added IDs to the HTML widget elements and separated gaming from studying (before everything was mixed in one widget with the wrong label):
<!-- widget gaming -->
<div class="track" id="game-track">—</div>
<div class="music-bars" id="game-bars" style="display:none">...</div>
<!-- widget studying (new) -->
<div class="track" id="study-track">—</div>
3. ~40 lines of inline JS at the end of the HTML. lanyard with 60s polling, status.json fetched once on load:
// Lanyard: polling every 60s
(function () {
var LANYARD = 'https://api.lanyard.rest/v1/users/YOUR_ID';
function fetchGame() {
fetch(LANYARD)
.then(function (r) { return r.json(); })
.then(function (d) {
var track = document.getElementById('game-track');
var bars = document.getElementById('game-bars');
if (!track) return;
var acts = d.data && d.data.activities;
var game = acts && acts.find(function (a) { return a.type === 0; });
if (game) {
track.textContent = game.name;
if (bars) bars.style.display = ''; // show the bars
} else {
track.textContent = 'nothing right now';
if (bars) bars.style.display = 'none';
}
})
.catch(function () {});
}
fetchGame();
setInterval(fetchGame, 60000);
})();
// status.json: single fetch on load
(function () {
fetch('/status.json')
.then(function (r) { return r.json(); })
.then(function (d) {
var el = document.getElementById('study-track');
if (el && d.studying) el.textContent = d.studying;
})
.catch(function () {});
})();
studying worked on the first try. gaming... nope. (ꐦ°᷄д°᷅)
the widget wasnt updating
opened localhost:8080 and the gaming widget kept showing "—". studying worked fine (showed "Lua & C & C++"), but gaming nothing.
opened browser console and ran the fetch manually:
fetch('https://api.lanyard.rest/v1/users/1368778707334070343')
.then(r=>r.json()).then(d=>console.log(d))
the result was:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at https://api.lanyard.rest/v1/users/...
(Reason: CORS request did not succeed). Status code: (null).
status (null). this isnt a CORS header error, its a network failure. the browser didnt even get a response. went to the terminal:
curl -s https://api.lanyard.rest/v1/users/1368778707334070343
output: absolutely nothing. no JSON, no visible error. ¯\_(ツ)_/¯
then ran with verbose:
curl -v https://api.lanyard.rest/v1/users/1368778707334070343 2>&1 | head -30
* Could not resolve host: api.lanyard.rest
* Closing connection
curl: (6) Could not resolve host: api.lanyard.rest
DNS couldnt resolve the domain. the server exists, the site exists, but my local DNS simply didnt know who api.lanyard.rest was.
diagnosis: ISP DNS doesnt have the record
tested with google DNS to confirm:
nslookup api.lanyard.rest 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
api.lanyard.rest canonical name = lanyard.gateway.railway.app.
Name: lanyard.gateway.railway.app
Address: 151.101.2.15
with 8.8.8.8 it resolved instantly. the problem is clear, the default DNS i was using didnt have the record for the .rest domain. lanyard is hosted on railway (lanyard.gateway.railway.app) with IP 151.101.2.15.
quick fix: /etc/hosts
with the IP in hand, i put it straight in /etc/hosts so i wouldnt depend on DNS resolution:
echo "151.101.2.15 api.lanyard.rest" | sudo tee -a /etc/hosts
tested:
curl -s https://api.lanyard.rest/v1/users/1368778707334070343
{"data":{"activities":[{"name":"Sixtar Gate STARTRAIL","type":0,...}],
"discord_status":"online",...},"success":true}
beautiful JSON on screen. reloaded localhost:8080 and the widget showed up, Sixtar Gate STARTRAIL. (✿◡‿◡)
the railway IP can change. the permanent fix is changing ur system DNS to 1.1.1.1 (cloudflare) or 8.8.8.8 (google) in network settings. on nobara/fedora: settings → network → ur connection → IPv4 tab → DNS.
final result
| Widget | Before | After |
|---|---|---|
| now playing | hardcoded, wrong label ("now studying:") | discord rich presence via lanyard, 60s polling |
| now studying | hardcoded together with gaming widget | separate widget, reads /status.json from repo |
| now listening | hardcoded "world.execute(me); Mili" | still hardcoded (Last.fm stays for later) |
to update studying just edit status.json and git push. simple. no reason to complicate what is manual by nature.
lesson
if u use ur ISPs default DNS in brazil, theres a real chance domains with less common TLDs (.rest, .dev, etc.) simply wont resolve. the ISP isnt required to have all records updated.
switch to 1.1.1.1 or 8.8.8.8 and forget the problem. its a 30 second change that saves future headaches. (⌐■_■)
and if u get a CORS error with status (null) in the browser, its not CORS, its network. the browser reports any cross-origin fetch failure as "CORS error". go straight to the terminal and test with curl -v.
update: migrating to cloudflare pages
i moved from github pages to cloudflare pages to get edge functions. with that, the "now playing" widget got an extra layer of privacy and performance.
before, the client-side fetched directly to api.lanyard.rest, exposing my discord ID (1368778707334070343) in the HTML for anyone to inspect. now theres an edge function at /api/now-playing that does the fetch on the server, caches for 60s, and the client only sees the local proxy. less exposure, faster, and the CSP got cleaner too.
// before (client-side, ID exposed)
fetch('https://api.lanyard.rest/v1/users/1368778707334070343')
// after (edge function, ID hidden)
fetch('/api/now-playing')
the function is literally ~10 lines. cloudflare does the fetch, applies edge cache, and returns the JSON. the browser never touches the external domain.
discord user IDs arent state secrets, but exposing any identifier without need is bad practice. if someone wants to scrape my presence 24/7, now they have to hit my domain. i control the rate limit and the cache.
besides that, cloudflare pages still delivers the site closer to visitors (global CDN) and has native function support. github pages is great for 100% static sites, but for anything dynamic, edge is the way.