Basically my website, hosted at edm115.dev, real-time preview of the latest commit at next.edm115.dev
- Nuxt 4 (Vue 3, Vite 7)
- Typescript
- SCSS
- Markdown-it
- highlight.js
- WASM (assemblyscript)
- Vueuse
- OxLint + EsLint (Stylistic)
- PNPM
- Static Site Generation (SSG) to ship smaller and faster pages
- Automatic light/dark mode + switch animation
- Automatic EN/FR language detection
- Fully responsive design
- Blog posts written in Markdown, with custom theme, syntax highlighting, multiple plugins (headings anchor links, image lazyload, dynamic table of contents, ...). Posts from my Telegram channel have also been ported here
- Homemade blog posts fuzzy search with tags and date filtering + fast WASM-based keyword & full-text search powered by docfind and pagefind
- The projects page actually fetches the repo README at build time and pre-renders it with the above pipeline + some cleanup of GitHub-first Markdown quirks
- The "polychrome" effect on my logo (Balatro inspired) uses multiple layers (glow, color overlay and caustic waves) with usage of Web Workers and WASM for better performance. Check the code to find how to enable the alternative rendering 😉
- Nice "liquid-esque" effect on the navbar buttons + stylish mobile menu
- The odometers have been created using a personal fork of an existing library to make it smaller and faster
- Every single UI component is custom made. No Vuetify, no Tailwind, no shadcn-vue, ...
- Everything has been optimized for performance and will continue to be !
- A job runs every week to update the Open Source contributions list, as well as another one to check the build status on PRs
- Local fonts with Country Flags polyfill for Windows and custom ligature sets
- Image optimization + lazyloading + progressive quality
- Working Open Graph with dynamically generated custom images at build time
- Privacy-respectful analytics (Cloudflare Web Analytics) + any click outside of the banner is considered as reject :)
- 100% Lighthouse score (desktop)
- Automatic redirection of URLs from the v1 website
Start :
git clone https://github.com/EDM115/website.git && cd website
pnpm i --frozen-lockfile
pnpm install-docfind:linux # or windows
pnpm prebuild
pnpm devBefore commits :
pnpm format
pnpm lint:fixTest builds :
pnpm build
pnpm start:ssrTest the actual rendered builds :
pnpm generate
pnpm start:ssgConsidered additions
- Mermaid support in markdown-it
- Better GFM support (=> https://github.com/npm/marky-markdown#dependencies & https://github.github.com/gfm/ + https://mdxjs.com/guides/gfm/)
- Main website status :
- Latest commit preview status :
- Main website (mirror) status :
- Main website (GitHub Pages mirror) status :
Install NGINX outside of the distro packages (to have the latest version). Ubuntu example : https://nginx.org/en/linux_packages.html#Ubuntu
Install the needed dynamic module :
sudo apt install nginx-module-njsPrepare for Brotli compression (optional but recommended) :
sudo apt install brotli build-essential git ca-certificates wget libpcre2-dev zlib1g-dev libssl-dev
# Get NGINX's version
nginx -V # here 1.28.2, note the flags
mkdir -p /tmp/nginx-brotli-build
cd /tmp/nginx-brotli-build
wget http://nginx.org/download/nginx-1.28.2.tar.gz
tar xf nginx-1.28.2.tar.gz
git clone https://github.com/google/ngx_brotli.git
cd ngx_brotli
git submodule update --init --recursive
cd ..
# Run configure with the EXACT flags you had, plus the brotli module on top
# Check for "adding module in /tmp/nginx-brotli-build/ngx_brotli" & "+ ngx_brotli was configured" in the output. Example :
cd /tmp/nginx-brotli-build/nginx-1.28.2
./configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--modules-path=/usr/lib/nginx/modules \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/run/nginx.pid \
--lock-path=/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nginx \
--group=nginx \
--with-compat \
--with-file-aio \
--with-threads \
--with-http_addition_module \
--with-http_auth_request_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_mp4_module \
--with-http_random_index_module \
--with-http_realip_module \
--with-http_secure_link_module \
--with-http_slice_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--with-http_v3_module \
--with-mail \
--with-mail_ssl_module \
--with-stream \
--with-stream_realip_module \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-cc-opt='-g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=/home/builder/debuild/nginx-1.28.2/debian/debuild-base/nginx-1.28.2=. -flto=auto -ffat-lto-objects -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=/home/builder/debuild/nginx-1.28.2/debian/debuild-base/nginx-1.28.2=/usr/src/nginx-1.28.2-1~noble -fPIC' \
--with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \
--add-dynamic-module=/tmp/nginx-brotli-build/ngx_brotli
make modules
# Install the files
sudo cp objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
sudo cp objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/Caution
You will need to redo all that on each NGINX update !
Then start NGINX :
sudo systemctl start nginx
sudo systemctl enable --now nginxuser nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
load_module modules/ngx_http_js_module.so;
# Only if you built Brotli support
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
events {
worker_connections 2048;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
# tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
gzip on;
gzip_vary on;
# Only if Brotli is available
brotli on;
brotli_static on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
# text/html
text/xml
application/xml
application/json
application/javascript
image/svg+xml;
#
server_tokens off;
# pcre_jit on;
include /etc/nginx/sites-available/*;
}ssl_certificate /home/edm115/.secure/cloudflare-origin-server.pem;
ssl_certificate_key /home/edm115/.secure/cloudflare-origin-server.key;proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;root /home/edm115/website/dist;
index index.html;
location ^~ /_nuxt/ {
try_files $uri =404;
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
access_log off;
log_not_found off;
}
location ~* \.(?:js|mjs|css|map|json|txt|xml|webmanifest|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot)$ {
try_files $uri =404;
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
access_log off;
log_not_found off;
}
location = /index.html {
try_files $uri =404;
add_header Cache-Control "no-cache, must-revalidate" always;
}
location ~* \.html$ {
try_files $uri =404;
add_header Cache-Control "no-cache, must-revalidate" always;
}
location / {
try_files $uri $uri/ /404.html;
}server {
listen 80;
listen [::]:80;
server_name edm115.dev *.edm115.dev;
return 301 https://$host$request_uri;
}js_import blog from /etc/nginx/js/blog.js;
js_set $blog_search_redirect blog.blog_redirect;
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name edm115.dev www.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
include /etc/nginx/mime.types;
default_type application/octet-stream;
location ~ '^/blog(?:/telegram)?/\d{4}(?:/\d{2}(?:/\d{2})?)?/?$' {
add_header X-Redirect $blog_search_redirect always;
if ($blog_search_redirect = "") {
return 500;
}
return 301 $scheme://$host$blog_search_redirect;
}
include /etc/nginx/snippets/static-edm115-site.conf;
}# jm-vps.edm115.dev -> :9898
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name jm-vps.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:9898;
}
}
# dicewizard-vps.edm115.dev -> :8686
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name dicewizard-vps.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:8686;
}
}
# edm115-discord-vps.edm115.dev -> :8888
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name edm115-discord-vps.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:8888;
}
}
# logs-vps.edm115.dev -> :10000
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name logs-vps.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:10000;
}
}
# maps.edm115.dev -> :27400
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name maps.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:27400;
}
}
# spendly.edm115.dev -> :60000
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name spendly.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://127.0.0.1:60000;
}
}
# senescalade.edm115.dev -> github redirect
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name senescalade.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
return 301 https://github.com/EDM115-org/Senescalade;
}
# next.edm115.dev -> netlify redirect
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name next.edm115.dev;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
return 301 https://edm115.netlify.app$request_uri;
}
## defunct services
# foudre-vps.edm115.dev -> :8989
# cursedchess-vps.edm115.dev -> :6969
# trackfit.edm115.dev -> :56000
# trackfit-api.edm115.dev -> :56001# Catch-all requests, can be used for unparked subdomains or redirect old URLs
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
http2 on;
server_name _;
include /etc/nginx/snippets/ssl-cloudflare-origin.conf;
return 301 https://edm115.dev;
}function pad2(n) {
return (
n < 10
? "0"
: ""
) + n
}
function blog_redirect(r) {
// strip trailing slash
const uri = (r.uri || "").replace(/\/+$/, "")
const m = uri.match(/^\/blog(\/telegram)?\/(\d{4})(?:\/(\d{2})(?:\/(\d{2}))?)?$/)
if (!m) {
r.warn(`blog_redirect : no match for ${uri}`)
// safe default to avoid empty Location on 3xx
return "/blog"
}
// "" or "/telegram"
const sub = m[1] || ""
const y = parseInt(m[2], 10)
// "MM" or undefined
const mmS = m[3]
// "DD" or undefined
const ddS = m[4]
const base = `/blog${sub}`
if (ddS) {
// /YYYY/MM/DD -> ?search=at:YYYY-MM-DD
return `${base}?search=at:${m[2]}-${mmS}-${ddS}`
}
if (mmS) {
// /YYYY/MM -> ?search=before:YYYY-(MM+1)+after:YYYY-(MM-1) (with year rollovers)
const mm = parseInt(mmS, 10)
const next = mm === 12
? {
y: y + 1,
m: 1,
}
: {
y,
m: mm + 1,
}
const prev = mm === 1
? {
y: y - 1,
m: 12,
}
: {
y,
m: mm - 1,
}
return `${base}?search=before:${next.y}-${pad2(next.m)}+after:${prev.y}-${pad2(prev.m)}`
}
// /YYYY -> ?search=before:(YYYY+1)+after:(YYYY-1)
return `${base}?search=before:${y + 1}+after:${y - 1}`
}
export default { blog_redirect }Finally, set the correct permissions (assuming that the repo is at /home/edm115/website and that it is built) :
sudo find /home/edm115/website/dist -type d -exec chmod 755 {} \;
sudo find /home/edm115/website/dist -type f -exec chmod 644 {} \;
sudo chmod -R 755 /home/edm115/website/dist
sudo chmod -R 755 /home/edm115/websiteTest and restart :
sudo nginx -t
sudo systemctl restart nginx