all the website mainly done

This commit is contained in:
0xa0
2026-07-21 20:45:47 +02:00
parent 20f9aca26b
commit 30609c3bb3
15 changed files with 2127 additions and 25 deletions
+11
View File
@@ -0,0 +1,11 @@
<?php
require __DIR__ . '/inc/posts.php';
$page_title = 'blog · ' . SITE_TITLE;
require __DIR__ . '/inc/header.php';
?>
<h1>blog</h1>
<p>things I've written, newest first.</p>
<?php render_post_list(all_posts()); ?>
<?php require __DIR__ . '/inc/footer.php'; ?>
+1 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
const DISCORD_ID = '298221448642953217'; const DISCORD_ID = '298221448642953217';
const SITE_TITLE = '0xa0'; const SITE_TITLE = '0xA0.dev';
define('DATA_DIR', getenv('DATA_DIR') ?: '/var/www/data'); define('DATA_DIR', getenv('DATA_DIR') ?: '/var/www/data');
+105 -3
View File
@@ -25,6 +25,7 @@ header nav {
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
h1 { font-size: 1.6rem; } h1 { font-size: 1.6rem; }
h1 .phonetic { color: darkgrey; font-size: 1rem; font-weight: normal; }
h2 { font-size: 1.1rem; color: var(--muted); font-weight: normal; } h2 { font-size: 1.1rem; color: var(--muted); font-weight: normal; }
footer { footer {
margin-top: 3rem; margin-top: 3rem;
@@ -32,6 +33,19 @@ footer {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
color: var(--muted); color: var(--muted);
} }
.buttons88x31 {
display: flex;
flex-wrap: wrap;
gap: .4rem;
margin-bottom: .8rem;
}
.buttons88x31 a { border-bottom: none; }
.buttons88x31 img {
width: 88px;
height: 31px;
image-rendering: pixelated;
display: block;
}
.dot { .dot {
display: inline-block; display: inline-block;
@@ -45,21 +59,58 @@ footer {
.dot-dnd { background: #ed4245; } .dot-dnd { background: #ed4245; }
.dot-offline { background: #747f8d; } .dot-offline { background: #747f8d; }
.status-row {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: .6rem;
}
.status-card { .status-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .8rem; gap: .8rem;
padding: .8rem; padding: .8rem;
flex: 1 1 220px;
min-width: 0;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 4px;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
} }
.status-card .avatar { border-radius: 8px; flex: none; } .status-card .avatar { border-radius: 4px; flex: none; }
.status-card .who { display: flex; flex-direction: column; gap: .15rem; } .status-card .who { display: flex; flex-direction: column; gap: .15rem; }
.status-card .name { font-weight: bold; } .status-card .name { font-weight: bold; }
.status-card .state { color: var(--muted); font-size: .9rem; } .status-card .state { color: var(--muted); font-size: .9rem; }
.now-playing { color: var(--muted); font-size: .9rem; }
.spotify-card {
display: flex;
align-items: center;
gap: .8rem;
padding: .7rem;
flex: 1 1 220px;
min-width: 0;
border: 1px solid var(--line);
border-radius: 4px;
color: var(--fg);
}
a.spotify-card:hover { border-color: #1db954; }
.spotify-card .album-art { border-radius: 4px; flex: none; }
.spotify-card .album-art--empty {
width: 64px; height: 64px;
display: flex; align-items: center; justify-content: center;
background: var(--field);
color: var(--muted);
font-size: 1.5rem;
}
.np-label { font-size: .72rem; letter-spacing: .02em; }
.spotify-card .np-label { color: #1db954; }
.spotify-idle .np-label { color: var(--muted); }
.dc-label { color: #5865f2; }
.spotify-card .track { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.spotify-card .np-label { color: #1db954; font-size: .72rem; letter-spacing: .02em; }
.spotify-card .song { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.spotify-card .artist { color: var(--muted); font-size: .85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
form { display: flex; flex-direction: column; gap: .5rem; margin: 1.5rem 0; } form { display: flex; flex-direction: column; gap: .5rem; margin: 1.5rem 0; }
input, textarea { input, textarea {
@@ -84,3 +135,54 @@ button {
.entry { border-top: 1px solid var(--line); padding: .8rem 0; } .entry { border-top: 1px solid var(--line); padding: .8rem 0; }
.entry .date { color: var(--muted); font-size: .8rem; margin-left: .5rem; } .entry .date { color: var(--muted); font-size: .8rem; margin-left: .5rem; }
.entry p { margin: .3rem 0 0; } .entry p { margin: .3rem 0 0; }
.muted { color: var(--muted); }
/* post list (home widget + /blog) */
.posts-widget { margin: 2rem 0; }
.post-list { list-style: none; padding: 0; margin: 1rem 0; }
.post-list li {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: .6rem 0;
border-top: 1px solid var(--line);
}
.post-list .date { color: var(--muted); font-size: .85rem; white-space: nowrap; }
/* single post */
.back-link { margin: .5rem 0; }
.back-link a { border-bottom: none; color: var(--muted); }
.back-link a:hover { color: var(--fg); }
.post-title { margin-bottom: .2rem; }
.post-meta { color: var(--muted); font-size: .85rem; margin-bottom: 1.5rem; }
.post-body img {
max-width: 100%;
height: auto;
display: block;
margin: 1.5rem 0;
border-radius: 4px;
}
.post-body h1, .post-body h2, .post-body h3 { margin: 1.8rem 0 .6rem; }
.post-body h2 { color: var(--fg); font-size: 1.2rem; }
.post-body pre {
background: var(--field);
border: 1px solid var(--line);
border-radius: 4px;
padding: .8rem;
overflow-x: auto;
}
.post-body code {
background: var(--field);
padding: .1rem .3rem;
border-radius: 3px;
font-size: .9em;
}
.post-body pre code { background: none; padding: 0; }
.post-body blockquote {
border-left: 3px solid var(--line);
margin: 1rem 0;
padding-left: 1rem;
color: var(--muted);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+1712
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/../config.php';
/**
* 88x31 buttons shown in the footer.
* To add one, just append a line: ['href' => 'https://...', 'src' => 'https://.../banner.png', 'alt' => 'name'].
* The image is fetched + cached server-side, so nothing hotlinks the remote host on every visit.
*/
const BUTTONS_88x31 = [
['href' => 'https://patate.dev', 'src' => 'https://patate.dev/images/patate_dev_banner.jpeg', 'alt' => 'patate.dev'],
['href' => 'https://blog.counter-strike.net/', 'src' => 'https://matdoes.dev/buttons/i/8678a5dfa58ce412f7d8f978b14e2f64.gif', 'alt' => 'https://blog.counter-strike.net/'],
['href' => 'caddyserver.com', 'src' => 'https://caddyserver.com/resources/images/nostalgia/caddy.png', 'alt' => 'caddyserver.com'],
['href' => '', 'src' => 'https://matdoes.dev/buttons/i/9025eae1851e8f32ff70f43f2c3393f0.png', 'alt' => 'ownpaws'],
['href' => 'https://ssi.fyi/', 'src'=>'https://ssi.fyi/88x31.gif', 'alt'=>'https://ssi.fyi'],
['href' => 'https://vmfunc.re/', 'src' => 'https://vmfunc.re/button.png', 'alt' => 'vmfunc.re'],
['href' => 'https://0xa0.dev/', 'src' => '/img/0xa0.png', 'alt' => '0xA0.dev']
];
const BUTTON_CACHE_TTL = 7 * 86400; // re-fetch each button at most once a week
/**
* Fetch a remote image, cache it locally, and return it as a base64 data: URI.
* Returns null on failure so the caller can fall back to hotlinking.
*/
function button_data_uri(string $url): ?string
{
$dir = DATA_DIR . '/buttons';
@mkdir($dir, 0775, true);
$file = $dir . '/' . md5($url); // stores "content-type\n<raw bytes>"
if (!is_file($file) || (time() - filemtime($file)) > BUTTON_CACHE_TTL) {
$ctx = stream_context_create(['http' => [
'timeout' => 5,
'user_agent' => 'Mozilla/5.0 (0xa0.dev button fetcher)',
]]);
$bytes = @file_get_contents($url, false, $ctx);
if ($bytes !== false && $bytes !== '') {
$type = 'image/png';
foreach ($http_response_header ?? [] as $h) {
if (stripos($h, 'content-type:') === 0) {
$type = trim(substr($h, strlen('content-type:')));
break;
}
}
file_put_contents($file, $type . "\n" . $bytes);
}
}
if (!is_file($file)) {
return null;
}
$raw = (string) file_get_contents($file);
$nl = strpos($raw, "\n");
if ($nl === false) {
return null;
}
$type = substr($raw, 0, $nl);
$data = substr($raw, $nl + 1);
return 'data:' . $type . ';base64,' . base64_encode($data);
}
/** Render the whole 88x31 button row as HTML. */
function render_buttons_88x31(): string
{
$html = '';
foreach (BUTTONS_88x31 as $b) {
$uri = button_data_uri($b['src']) ?? $b['src']; // fall back to hotlink if fetch failed
$img = '<img src="' . htmlspecialchars($uri) . '" alt="' . htmlspecialchars($b['alt']) . '" width="88" height="31">';
$href = $b['href'] ?? '';
if ($href !== '') {
$html .= '<a href="' . htmlspecialchars($href) . '" target="_blank" rel="noopener">' . $img . '</a>' . "\n";
} else {
$html .= $img . "\n"; // no href → just the image, not clickable
}
}
return $html;
}
+4
View File
@@ -1,6 +1,10 @@
<?php $build = trim((string) @file_get_contents(__DIR__ . '/../build.txt')) ?: 'dev'; ?> <?php $build = trim((string) @file_get_contents(__DIR__ . '/../build.txt')) ?: 'dev'; ?>
<?php require_once __DIR__ . '/buttons.php'; ?>
</main> </main>
<footer> <footer>
<div class="buttons88x31">
<?= render_buttons_88x31() ?>
</div>
<small>&copy; <?= date('Y') ?> · <a href="https://git.0xa0.dev">source</a> · build: <?= htmlspecialchars($build) ?></small> <small>&copy; <?= date('Y') ?> · <a href="https://git.0xa0.dev">source</a> · build: <?= htmlspecialchars($build) ?></small>
</footer> </footer>
</body> </body>
+3 -1
View File
@@ -4,14 +4,16 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(SITE_TITLE) ?></title> <title><?= htmlspecialchars($page_title ?? SITE_TITLE) ?></title>
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<script src="/js/htmx.min.js" defer></script> <script src="/js/htmx.min.js" defer></script>
<script src="/js/idiomorph-ext.min.js" defer></script>
</head> </head>
<body> <body>
<header> <header>
<nav> <nav>
<a href="/">home</a> <a href="/">home</a>
<a href="/blog.php">blog</a>
<a href="/guestbook.php">guestbook</a> <a href="/guestbook.php">guestbook</a>
</nav> </nav>
</header> </header>
+87
View File
@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/../config.php';
$__er = error_reporting();
error_reporting($__er & ~E_DEPRECATED);
require_once __DIR__ . '/Parsedown.php';
error_reporting($__er);
const POSTS_DIR = __DIR__ . '/../posts';
function post_slug_valid(string $slug): bool {
return (bool) preg_match('/^[a-z0-9][a-z0-9-]*$/', $slug);
}
function parse_front_matter(string $raw): array {
$meta = [];
$body = $raw;
if (preg_match('/^(?:\xEF\xBB\xBF)?---\r?\n(.*?)\r?\n---\r?\n?(.*)$/s', $raw, $m)) {
foreach (preg_split('/\r?\n/', $m[1]) as $line) {
if (preg_match('/^(\w+):\s*(.*)$/', $line, $kv)) {
$meta[strtolower($kv[1])] = trim($kv[2]);
}
}
$body = $m[2];
}
return [$meta, $body];
}
function load_post(string $slug): ?array {
if (!post_slug_valid($slug)) {
return null;
}
$file = POSTS_DIR . '/' . $slug . '.md';
if (!is_file($file)) {
return null;
}
$raw = (string) file_get_contents($file);
[$meta, $body] = parse_front_matter($raw);
return [
'slug' => $slug,
'title' => $meta['title'] ?? $slug,
'date' => $meta['date'] ?? date('Y-m-d', filemtime($file)),
'body' => $body,
];
}
function all_posts(): array {
$posts = [];
foreach (glob(POSTS_DIR . '/*.md') ?: [] as $file) {
if ($p = load_post(basename($file, '.md'))) {
$posts[] = $p;
}
}
usort($posts, fn($a, $b) => strcmp($b['date'], $a['date']));
return $posts;
}
function render_markdown(string $md): string {
static $pd = null;
if ($pd === null) {
$pd = new Parsedown();
$pd->setSafeMode(true);
}
return $pd->text($md);
}
function post_date_display(string $date): string {
$ts = strtotime($date);
return $ts ? date('m/d/Y', $ts) : htmlspecialchars($date);
}
function render_post_list(array $posts): void {
if (!$posts) {
echo '<p class="muted">no posts yet.</p>';
return;
}
echo '<ul class="post-list">';
foreach ($posts as $p) {
printf(
'<li><a href="/post.php?slug=%s">%s</a><span class="date">%s</span></li>',
urlencode($p['slug']),
htmlspecialchars($p['title']),
post_date_display($p['date'])
);
}
echo '</ul>';
}
+19 -3
View File
@@ -1,13 +1,29 @@
<?php require __DIR__ . '/inc/header.php'; ?> <?php require __DIR__ . '/inc/header.php'; ?>
<h1>hey, I'm 0x</h1> <h1>hey, I'm 0x <span class="phonetic">/ˈzɪəroʊ ɛks/</span></h1>
<p>welcome to my corner of the web. no framework, no build step — just PHP printing HTML.</p> <p>welcome to my corner of the web. just PHP printing HTML as God intended.</p>
<section class="status-widget"> <section class="status-widget">
<h2>status</h2> <h2>status</h2>
<div id="status" hx-get="/status.php" hx-trigger="every 5s" hx-swap="innerHTML"> <div id="status" hx-get="/status.php" hx-trigger="every 10s" hx-ext="morph" hx-swap="morph:innerHTML">
<?php include __DIR__ . '/status.php'; ?> <?php include __DIR__ . '/status.php'; ?>
</div> </div>
</section> </section>
<?php $age = (new DateTime('2002-11-07'))->diff(new DateTime('today'))->y; ?>
<p>I'm a <?= $age ?>-year-old developer (I know I'm unc), I hold a bachelor's degree in computer science (BUT Informatique) and I'm also a 42 school student.</p>
<p>if you wanna see what I'm building, feel free to check out my <a href="https://github.com/0xa00">GitHub</a> !</p>
<section class="posts-widget">
<h2>recent posts</h2>
<?php require_once __DIR__ . '/inc/posts.php'; ?>
<?php render_post_list(array_slice(all_posts(), 0, 3)); ?>
<p><a href="/blog.php">all posts &rarr;</a></p>
</section>
<p>If you want to leave a message, feel free to write something on my <a href="/guestbook.php">guestbook</a> !</p>
<?php require __DIR__ . '/inc/footer.php'; ?> <?php require __DIR__ . '/inc/footer.php'; ?>
+1
View File
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
<?php
require __DIR__ . '/inc/posts.php';
$post = load_post($_GET['slug'] ?? '');
if (!$post) {
http_response_code(404);
require __DIR__ . '/inc/header.php';
?>
<p class="back-link"><a href="/blog.php">&larr; go back</a></p>
<h1>404</h1>
<p>no such post.</p>
<?php
require __DIR__ . '/inc/footer.php';
exit;
}
$page_title = $post['title'] . ' · ' . SITE_TITLE;
require __DIR__ . '/inc/header.php';
?>
<p class="back-link"><a href="/blog.php">&larr; go back</a></p>
<article class="post">
<h1 class="post-title">[ <?= htmlspecialchars($post['title']) ?> ]</h1>
<div class="post-meta"><?= post_date_display($post['date']) ?></div>
<div class="post-body"><?= render_markdown($post['body']) ?></div>
</article>
<p class="back-link"><a href="/blog.php">&larr; go back</a></p>
<?php require __DIR__ . '/inc/footer.php'; ?>
+9
View File
@@ -0,0 +1,9 @@
---
title: 42 Piscine
date: 2025-08-30
---
During the month of July 2025, I had the opportunity to participate in the 42 Piscine in Perpignan, France. This intensive coding bootcamp was a unique experience that challenged my skills and pushed me to my limits. After an entire month of rigorous work, I managed to be selected among the candidates to be part of the 42 school.
it was an unique experience and I learned a lot of things in the short amount of time, I look forward to continue to learn during my time at 42 and apply that knowledge in real-world projects.
![42 Perpignan Occitanie](/img/42-perpignan.png)
+54 -6
View File
@@ -2,23 +2,43 @@
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
$cacheFile = DATA_DIR . '/lanyard.json'; $cacheFile = DATA_DIR . '/lanyard.json';
$ttl = 4; $ttl = 15;
if (!is_file($cacheFile) || time() - filemtime($cacheFile) > $ttl) { if (!function_exists('lanyard_refresh')) {
function lanyard_refresh(string $cacheFile): void {
$ch = curl_init('https://api.lanyard.rest/v1/users/' . DISCORD_ID); $ch = curl_init('https://api.lanyard.rest/v1/users/' . DISCORD_ID);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 4]); curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 3,
]);
$res = curl_exec($ch); $res = curl_exec($ch);
curl_close($ch); curl_close($ch);
if ($res) { if ($res) {
@mkdir(DATA_DIR, 0755, true); @mkdir(DATA_DIR, 0755, true);
file_put_contents($cacheFile, $res); file_put_contents($cacheFile, $res, LOCK_EX);
} }
} }
}
$haveCache = is_file($cacheFile);
$stale = !$haveCache || (time() - filemtime($cacheFile) > $ttl);
if ($stale && !$haveCache) {
lanyard_refresh($cacheFile);
$stale = false;
}
$data = json_decode(@file_get_contents($cacheFile), true)['data'] ?? null; $data = json_decode(@file_get_contents($cacheFile), true)['data'] ?? null;
$status = $data['discord_status'] ?? 'offline'; $status = $data['discord_status'] ?? 'offline';
$spotify = ($data['listening_to_spotify'] ?? false) ? $data['spotify'] : null; $spotify = ($data['listening_to_spotify'] ?? false) ? $data['spotify'] : null;
// Discord can lag clearing the Spotify activity after playback stops, leaving a
// stale "ghost" track. If the song's end time has already passed, treat it as stopped.
if ($spotify && !empty($spotify['timestamps']['end']) && $spotify['timestamps']['end'] < time() * 1000) {
$spotify = null;
}
$user = $data['discord_user'] ?? []; $user = $data['discord_user'] ?? [];
$did = $user['id'] ?? DISCORD_ID; $did = $user['id'] ?? DISCORD_ID;
$name = $user['global_name'] ?? $user['username'] ?? ''; $name = $user['global_name'] ?? $user['username'] ?? '';
@@ -33,13 +53,41 @@ $banner = $bannerHash
? "https://cdn.discordapp.com/banners/$did/$bannerHash." . (str_starts_with($bannerHash, 'a_') ? 'gif' : 'png') . '?size=480' ? "https://cdn.discordapp.com/banners/$did/$bannerHash." . (str_starts_with($bannerHash, 'a_') ? 'gif' : 'png') . '?size=480'
: null; : null;
?> ?>
<div class="status-row">
<div class="status-card"<?= $banner ? ' style="background-image:url(\'' . htmlspecialchars($banner) . '\')"' : '' ?>> <div class="status-card"<?= $banner ? ' style="background-image:url(\'' . htmlspecialchars($banner) . '\')"' : '' ?>>
<img class="avatar" src="<?= htmlspecialchars($avatar) ?>" alt="" width="56" height="56" loading="lazy"> <img class="avatar" src="<?= htmlspecialchars($avatar) ?>" alt="" width="56" height="56" loading="lazy">
<div class="who"> <div class="who">
<span class="np-label dc-label">discord</span>
<span class="name"><?= htmlspecialchars($name) ?></span> <span class="name"><?= htmlspecialchars($name) ?></span>
<span class="state"><span class="dot dot-<?= htmlspecialchars($status) ?>"></span> <?= htmlspecialchars($status) ?></span> <span class="state"><span class="dot dot-<?= htmlspecialchars($status) ?>"></span> <?= htmlspecialchars($status) ?></span>
</div>
</div>
<?php if ($spotify): ?> <?php if ($spotify): ?>
<span class="now-playing">&#9834; <?= htmlspecialchars($spotify['song']) ?> — <?= htmlspecialchars($spotify['artist']) ?></span> <a class="spotify-card" href="https://open.spotify.com/track/<?= htmlspecialchars($spotify['track_id']) ?>" target="_blank" rel="noopener">
<img class="album-art" src="<?= htmlspecialchars($spotify['album_art_url']) ?>" alt="" width="64" height="64" loading="lazy">
<div class="track">
<span class="np-label">&#9834; listening on spotify</span>
<span class="song"><?= htmlspecialchars($spotify['song']) ?></span>
<span class="artist"><?= htmlspecialchars(str_replace('; ', ', ', $spotify['artist'])) ?></span>
</div>
</a>
<?php else: ?>
<div class="spotify-card spotify-idle">
<div class="album-art album-art--empty">&#9834;</div>
<div class="track">
<span class="np-label">spotify</span>
<span class="song">nothing playing</span>
<span class="artist">not listening right now</span>
</div>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> <?php
if ($stale && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
lanyard_refresh($cacheFile);
}
?>