10 Commits

Author SHA1 Message Date
dependabot[bot] aa86366b7f Bump astro from 6.1.9 to 6.4.6
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 6.1.9 to 6.4.6.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.4.6/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.4.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 05:32:57 +00:00
Hadi 198112501a new notes
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-06-03 23:39:40 +02:00
Hadi 15f2b6c184 init web: ffuf, dir enum & subdomains enum
Signed-off-by: Hadi <hadi@example.com>
2026-06-01 15:31:31 +02:00
Hadi 25fb5a4bf0 Fix TOC links
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 21:03:40 +02:00
Hadi d257a0f26e update repos
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 20:52:09 +02:00
Hadi 3dfbdcf970 New separators
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 20:51:46 +02:00
Hadi 930c3bf3bb Clean homepage
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 20:40:22 +02:00
Hadi a055640fa8 Edit hero & add Infosec Notes section
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 20:21:17 +02:00
Hadi 9c0bbc4b77 edit sidebars
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-07 20:16:01 +02:00
Hadi 7968c662f6 Edit sidebar
Signed-off-by: Hadi <hadi@example.com>
2026-05-04 17:03:05 +02:00
32 changed files with 945 additions and 296 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
"@lucide/astro": "^0.552.0",
"@tailwindcss/vite": "^4.2.4",
"@types/bun": "^1.3.13",
"astro": "6.1.9",
"astro": "6.4.6",
"daisyui": "^5.5.19",
"node-html-parser": "^7.1.0",
"svelte": "^5.55.5",
+10 -5
View File
@@ -18,14 +18,19 @@ const latestPosts = sortedPosts.slice(0, 3);
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Latest Blog Posts</h2>
<p class="text-lg text-base-content/70">
Thoughts, insights, and tutorials on cybersecurity, OSINT, and
technology.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{latestPosts.map((post) => <BlogCard post={post} />)}
{
latestPosts.map((post) => (
<BlogCard
displayBanner={false}
displayTags={false}
displayDate={false}
post={post}
/>
))
}
</div>
<div class="text-center mt-12">
+12 -5
View File
@@ -5,6 +5,9 @@ import { Image } from "astro:assets";
import { formatDate } from "../utils/notes";
interface Props {
displayBanner: boolean;
displayDate: boolean;
displayTags: boolean;
post: CollectionEntry<"blog">;
}
@@ -14,6 +17,8 @@ const { post } = Astro.props;
<article
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
>
{ Astro.props.displayBanner && (
<figure class="aspect-video">
<Image
src={post.data.image}
@@ -23,25 +28,27 @@ const { post } = Astro.props;
height={400}
/>
</figure>
)}
<div class="card-body">
{ Astro.props.displayDate && (
<time class="text-sm text-base-content/60">
{formatDate(post.data.publishDate)}
</time>
)}
<h2 class="card-title hover:text-primary transition-colors">
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</h2>
<p class="text-base-content/80">{post.data.description}</p>
{
post.data.tags && post.data.tags.length > 0 && (
{ Astro.props.displayTags && post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-2">
{post.data.tags.map((tag) => (
<TagBadge tag={tag} />
))}
</div>
)
}
)}
<div class="card-actions justify-end mt-4">
<a href={`/blog/${post.id}`} class="btn btn-primary btn-sm">
<a href={`/blog/${post.id}`} class="btn btn-soft btn-primary btn-sm">
Read More
</a>
</div>
+5 -2
View File
@@ -2,6 +2,7 @@
import { ExternalLink, ChevronDown } from "@lucide/astro";
interface Props {
displayBanner: boolean;
repo: {
name: string;
description: string;
@@ -34,6 +35,7 @@ const hasMultiplePlatforms = platforms.length > 1;
<article
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
>
{ Astro.props.displayBanner && repo.banner_url && (
<figure class="aspect-video bg-base-200 overflow-hidden">
<img
src={repo.banner_url}
@@ -42,6 +44,7 @@ const hasMultiplePlatforms = platforms.length > 1;
onerror="this.parentElement.style.display='none'"
/>
</figure>
)}
<div class="card-body">
<h2 class="card-title hover:text-primary transition-colors">
@@ -83,7 +86,7 @@ const hasMultiplePlatforms = platforms.length > 1;
<div
tabindex="0"
role="button"
class="btn btn-primary btn-sm gap-1"
class="btn btn-soft btn-primary btn-sm gap-1"
>
<ExternalLink class="size-4" />
View Source
@@ -107,7 +110,7 @@ const hasMultiplePlatforms = platforms.length > 1;
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
class="btn btn-primary btn-sm gap-1"
class="btn btn-soft btn-primary btn-sm gap-1"
>
<ExternalLink class="size-4" />
View on Gitea
+5 -27
View File
@@ -1,5 +1,5 @@
---
import { ArrowRight, FolderCode, Key, Rss } from "@lucide/astro";
import { FolderCode, Key, Rss } from "@lucide/astro";
import { Image } from "astro:assets";
import type { SocialLinks } from "../config";
@@ -8,7 +8,6 @@ interface Props {
title: string;
description: string;
avatar: any;
location?: string;
socialLinks?: SocialLinks;
gpgKey?: string;
rssFeed?: string;
@@ -19,18 +18,17 @@ const {
title,
description,
avatar,
location,
socialLinks,
gpgKey,
rssFeed,
} = Astro.props;
---
<section class="hero min-h-[65vh]">
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-8">
<section class="hero py-20">
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-10">
<div class="avatar">
<div
class="w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
class="w-32 md:w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
>
<Image src={avatar} alt={name} />
</div>
@@ -40,8 +38,7 @@ const {
Hi, I'm {name}
</h1>
<p class="text-xl text-base-content/80 mb-2">{title}</p>
{location && <p class="text-base-content/60 mb-4">{location}</p>}
<p class="text-lg leading-relaxed mb-6">
<p class="text-lg max-w-lg leading-relaxed mb-6">
{description}
</p>
{
@@ -335,25 +332,6 @@ const {
</div>
)
}
<div class="mt-12 flex flex-wrap gap-5">
<a href="/blog" class="btn btn-ghost gap-2">
Blog Posts
<ArrowRight class="size-4" />
</a>
<a href="/projects" class="btn btn-ghost gap-2">
Projects
<ArrowRight class="size-4" />
</a>
<a href="/notes" class="btn btn-ghost gap-2">
Infosec Notes
<ArrowRight class="size-4" />
</a>
<a href="/#contact" class="btn btn-ghost gap-2">
Contact Me
<ArrowRight class="size-4" />
</a>
</div>
</div>
</div>
</section>
+28 -2
View File
@@ -10,9 +10,10 @@ interface Props {
graphEdges: { from: string; to: string }[];
forwardLinks: CollectionEntry<"notes">[];
backlinks: CollectionEntry<"notes">[];
externalLinks: { url: string; label: string }[];
}
const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
const { entry, graphNodes, graphEdges, forwardLinks, backlinks, externalLinks } = Astro.props;
---
<aside
@@ -26,7 +27,7 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
>
graph
</p>
<NoteGraph client:visible nodes={graphNodes} edges={graphEdges} />
<NoteGraph client:load nodes={graphNodes} edges={graphEdges} />
{
graphNodes.length < 2 && (
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
@@ -90,6 +91,31 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
)
}
{
externalLinks.length > 0 && (
<div class="p-3 border-t border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
external
</p>
<ul class="space-y-1">
{externalLinks.map(({ url, label }) => (
<li>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
title={url}
>
↗ {label}
</a>
</li>
))}
</ul>
</div>
)
}
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
<time
datetime={entry.data.publishDate.toISOString()}
@@ -65,25 +65,7 @@
}
</script>
<aside
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-[calc(100vh-3rem)]"
style="background: oklch(4% 0 0);"
>
<!-- Mobile close bar -->
<div class="lg:hidden flex items-center justify-between px-3 py-2 border-b border-base-300/40 shrink-0">
<span class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest">nav</span>
<label
for="nav-drawer"
class="cursor-pointer text-base-content/30 hover:text-base-content/70 transition-colors p-1"
aria-label="close sidebar"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</label>
</div>
<!-- Search -->
<div class="flex flex-col flex-1 min-h-0">
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
<div
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
@@ -98,7 +80,6 @@
</div>
</div>
<!-- Nav -->
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
{#each categories as cat}
{@const catNotes = notes.filter(
@@ -107,7 +88,6 @@
{#if catNotes.length > 0 || !search}
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
<div>
<!-- Category header -->
<div class="flex items-center w-full">
<button
onclick={() => toggle(cat)}
@@ -142,7 +122,6 @@
{/if}
</div>
<!-- Notes list -->
{#if openCategories.includes(cat)}
<ul
class="ml-4 mt-0.5 pb-1 space-y-px"
@@ -170,4 +149,4 @@
{/if}
{/each}
</nav>
</aside>
</div>
+26
View File
@@ -0,0 +1,26 @@
---
import NoteNavContent from "./NoteNavContent.svelte";
import type { CollectionEntry } from "astro:content";
interface Props {
notes: CollectionEntry<"notes">[];
currentEntry?: CollectionEntry<"notes">;
currentCategory?: string;
categories: string[];
}
const { notes, currentEntry, currentCategory, categories } = Astro.props;
---
<aside
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full"
style="background: oklch(4% 0 0);"
>
<NoteNavContent
client:load
notes={notes}
currentEntry={currentEntry}
currentCategory={currentCategory}
categories={categories}
/>
</aside>
+25
View File
@@ -0,0 +1,25 @@
---
import { ArrowRight } from "@lucide/astro";
---
<section id="notes" class="py-10 px-4">
<div class="max-w-6xl mx-auto text-center">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Infosec Notes</h2>
</div>
<div class="mt-8">
<p class="text-base text-base-content/70 mb-6">
Cheatsheets and references on tools and techniques I use for CTFs and
pentesting.
</p>
<div class="text-center mt-12">
<a href="/notes" class="btn btn-ghost gap-2">
Browse Notes
<ArrowRight class="size-4" />
</a>
</div>
</div>
</div>
</section>
+6 -6
View File
@@ -10,14 +10,14 @@ const latestRepos = repos.slice(0, 3);
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Check out my latest work</h2>
<p class="text-lg text-base-content/70">
I enjoy the challenge of reimagining existing programs & scripts in my
own unique way.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{latestRepos.map((repo) => <GiteaProjectCard repo={repo} />)}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{
latestRepos.map((repo) => (
<GiteaProjectCard displayBanner={false} repo={repo} />
))
}
</div>
<div class="text-center mt-12">
-2
View File
@@ -23,7 +23,6 @@ export interface SiteConfig {
title: string;
description: string;
avatar: string;
location: string;
socialLinks: SocialLinks;
gpgKey?: string;
rssFeed?: string;
@@ -39,7 +38,6 @@ export const siteConfig: SiteConfig = {
description:
"Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
avatar: "/avatar.png",
location: "🇫🇷 France",
socialLinks: {
github: "https://github.com/anotherhadi",
gitlab: "https://gitlab.com/anotherhadi_mirror",
+67
View File
@@ -0,0 +1,67 @@
---
title: "GRUB Boot Bypass"
description: "Physical access techniques to get a root shell by editing GRUB boot parameters."
tags: ["linux", "grub", "physical-access", "privesc"]
publishDate: 2026-05-18
---
When GRUB is not password-protected, anyone with physical access can edit boot parameters and bypass authentication entirely.
At the GRUB menu, press **`e`** to edit the selected entry. Modify the line starting with `linux`, then press **`F10`** to boot.
## Techniques
### init=/bin/sh
Replaces the init process with a shell; drops directly into a root shell before any login prompt.
```
linux ... init=/bin/sh
```
Filesystem is mounted read-only by default. Remount to make changes:
```bash
mount -o remount,rw /
```
### init=/bin/bash
Same as above but uses bash. Add `rw` on the `linux` line to mount read-write from the start:
```
linux ... rw init=/bin/bash
```
### rd.break (systemd)
Interrupts the boot process in the initramfs, before the real root filesystem is mounted. Useful for resetting the root password.
```
linux ... rd.break
```
From the initramfs shell:
```bash
mount -o remount,rw /sysroot
chroot /sysroot
passwd root
exit
```
### single (single-user mode)
Boots into maintenance mode. On some distros this drops to a root shell without a password prompt (not Debian/Ubuntu).
```
linux ... single
```
### systemd.unit=rescue.target
systemd equivalent of single-user mode: minimal services, root shell.
```
linux ... systemd.unit=rescue.target
```
+80
View File
@@ -0,0 +1,80 @@
---
title: "Linux Privilege Escalation"
description: "Common misconfigurations and weaknesses to check when escalating privileges on Linux."
tags: ["linux", "privesc", "post-exploitation"]
publishDate: 2026-05-18
---
## Sudo
```bash
sudo -l
```
Check [GTFOBins](https://gtfobins.github.io) for any listed binary.
If `env_keep+=LD_PRELOAD` is set:
```bash
# compile a shared lib that spawns a shell
gcc -fPIC -shared -o /tmp/shell.so shell.c -nostartfiles
sudo LD_PRELOAD=/tmp/shell.so <allowed_binary>
```
## SUID / SGID
```bash
find / -user root -perm -4000 -ls 2>/dev/null # SUID
find / -group root -perm -2000 -ls 2>/dev/null # SGID
```
Check any non-standard binary on GTFOBins.
## Misconfiguration
```bash
# World-writable directories
find / -type d -perm -2 -ls 2>/dev/null
# World-writable files owned by root
find / -user root -perm -2 ! -type l -ls 2>/dev/null
```
## Cron Jobs
```bash
cat /etc/crontab
ls -la /etc/cron.*
crontab -l
```
If a cron runs a script you can write to, replace its content:
```bash
echo 'chmod +s /bin/bash' >> /path/to/script.sh
```
If the cron uses a relative PATH and a directory is writable, drop a malicious binary earlier in `$PATH`.
## Capabilities
```bash
getcap -r / 2>/dev/null
```
Dangerous capabilities: `cap_setuid`, `cap_net_raw`, `cap_dac_override`.
Check [GTFOBins](https://gtfobins.github.io) for exploitation.
## Kernel Exploits
```bash
uname -r
searchsploit linux kernel $(uname -r)
```
## LinPEAS / WinPEAS
Automated enumeration scripts to surface privesc vectors quickly.
- [LinPEAS (linux)](https://github.com/peass-ng/PEASS-ng/tree/master/linPEAS)
- [WinPEAS (windows)](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS)
-2
View File
@@ -5,8 +5,6 @@ tags: ["ftp", "network", "service"]
publishDate: 2026-04-29
---
## Overview
FTP runs on **port 21** (control) and uses a secondary data channel (port 20 for active, ephemeral port for passive).
Common implementations: vsftpd, ProFTPD, Pure-FTPd, FileZilla Server, IIS FTP.
+79
View File
@@ -0,0 +1,79 @@
---
title: "NFS"
description: "Enumeration, mounting and privilege escalation techniques for NFS shares."
tags: ["nfs", "network", "service"]
publishDate: 2026-05-18
---
NFS (Network File System) runs on **port 2049** and allows remote filesystem mounting over the network.
Common on Linux/Unix environments. Access control is defined in `/etc/exports` on the server.
## Enumeration
### Nmap
```bash
nmap -sV -p 111,2049 $IP
nmap -p 111,2049 --script nfs-* $IP
```
Key scripts:
- `nfs-showmount`: lists exported shares
- `nfs-ls`: lists files in shares
- `nfs-statfs`: retrieves disk stats
### List shares
```bash
showmount -e $IP
rpcinfo -p $IP
```
## Mount
```bash
mkdir /mnt/nfs
mount -t nfs $IP:/share /mnt/nfs
mount -t nfs -o vers=2 $IP:/share /mnt/nfs # force NFSv2
umount /mnt/nfs
```
## Privilege Escalation
### no_root_squash
If the share is exported with `no_root_squash`, the remote root user keeps root privileges on the share.
Check `/etc/exports` on the server (if readable):
```bash
cat /etc/exports
```
Look for:
```
/share *(rw,no_root_squash)
```
If present, copy a SUID binary onto the share as root from your attacker machine:
```bash
cp /bin/bash /mnt/nfs/bash
chmod +s /mnt/nfs/bash
```
Then execute it on the target with `-p` to keep the SUID effective UID:
```bash
/tmp/nfs/bash -p
```
### UID spoofing
NFS authenticates by UID. If you know a file is owned by UID 1001 on the server, impersonate it directly:
```bash
python3 -c "import os; os.setuid(1001); os.system('/bin/bash')"
```
+108
View File
@@ -0,0 +1,108 @@
---
title: "Nmap"
description: "Host discovery, port scanning, service detection and NSE scripting"
tags: ["nmap", "network", "enumeration"]
publishDate: 2026-05-18
---
Nmap is a network scanner used for host discovery, port scanning, service/version detection, OS fingerprinting, and vulnerability scripting via NSE.
## Host Discovery
```bash
nmap -sn 192.168.1.0/24 # ping sweep, no port scan
nmap -sn -PR 192.168.1.0/24 # ARP ping (local network)
nmap -Pn $IP # skip host discovery, treat as up
```
## Port Scanning
```bash
nmap $IP # top 1000 ports (SYN scan if root)
nmap -p 80,443,8080 $IP # specific ports
nmap -p 1-65535 $IP # all ports
nmap -p- $IP # shorthand for all ports
nmap -sU $IP # UDP scan
nmap -sU -sS $IP # UDP + SYN together
```
Scan types:
- `-sS`: SYN scan (stealth, requires root)
- `-sT`: TCP connect scan (no root needed)
- `-sU`: UDP scan
- `-sA`: ACK scan (firewall rule mapping)
- `-sN/sF/sX`: Null, FIN, Xmas (evasion, unreliable on Windows)
## Service & Version Detection
```bash
nmap -sV $IP
nmap -sV --version-intensity 9 $IP # more aggressive probing
```
## OS Detection
```bash
nmap -O $IP
nmap -O --osscan-guess $IP # guess if not confident
```
## Aggressive Scan
```bash
nmap -A $IP # -sV -O --script=default --traceroute
```
## Timing Templates
```bash
nmap -T0 $IP # paranoid (IDS evasion, very slow)
nmap -T1 $IP # sneaky
nmap -T3 $IP # normal (default)
nmap -T4 $IP # aggressive (faster, good for CTFs)
nmap -T5 $IP # insane (may miss results)
```
## NSE Scripts
```bash
nmap --script default $IP
nmap --script vuln $IP
nmap --script "ftp-*" $IP
nmap --script safe $IP
nmap --script $script --script-args user=$user,pass=$password $IP
```
Common script categories: `auth`, `brute`, `default`, `discovery`, `dos`, `exploit`, `intrusive`, `safe`, `version`, `vuln`.
Scripts are located in `/usr/share/nmap/scripts/`.
## Output Formats
```bash
nmap -oN output.txt $IP # normal
nmap -oX output.xml $IP # XML
nmap -oG output.gnmap $IP # grepable
nmap -oA output $IP # all three at once
```
## Common Profiles
Quick full scan:
```bash
nmap -p- -T4 --min-rate 5000 -sV -sC -oA full $IP
```
CTF/lab initial recon:
```bash
nmap -sV -sC -p- --open $IP
```
UDP top ports:
```bash
nmap -sU --top-ports 100 $IP
```
-2
View File
@@ -5,8 +5,6 @@ tags: ["rdp", "network", "service"]
publishDate: 2026-05-04
---
## Overview
RDP (Remote Desktop Protocol) runs on **port 3389** and provides a graphical remote session.
Common on Windows servers and workstations.
-2
View File
@@ -5,8 +5,6 @@ tags: ["ssh", "network", "service"]
publishDate: 2026-05-04
---
## Overview
SSH runs on **port 22** and provides an encrypted remote shell.
Common implementations: OpenSSH, Dropbear, Bitvise.
-2
View File
@@ -5,8 +5,6 @@ tags: ["telnet", "network", "service"]
publishDate: 2026-05-04
---
## Overview
Telnet runs on **port 23** and transmits all data (including credentials) in **cleartext**.
Common on embedded devices, legacy systems, routers, and IoT equipment.
+1 -1
View File
@@ -32,7 +32,7 @@ If the page has been taken down or modified, the cached version may still show t
## Domain History
[VirusTotal](https://www.virustotal.com) shows the historical DNS records, subdomains, and associated IPs for any domain useful when a site has moved or been taken down.
[VirusTotal](https://www.virustotal.com) shows the historical DNS records, subdomains, and associated IPs for any domaint useful when a site has moved or been taken down.
[ViewDNS.info](https://viewdns.info) covers WHOIS history, reverse IP, reverse MX, and port scans from a single interface.
+2 -2
View File
@@ -44,9 +44,9 @@ tweet_id = 1234567890123456789
timestamp_ms = (tweet_id >> 22) + 1288834974657
```
`1288834974657` is Twitter's custom epoch (Nov 4, 2010). Works on both tweet IDs and user IDs useful to confirm account creation date without needing profile metadata.
`1288834974657` is Twitter's custom epoch (Nov 4, 2010). Works on both tweet IDs and user IDs: useful to confirm account creation date without needing profile metadata.
Several online converters exist if you don't want to do it manually search "snowflake id decoder".
Several online converters exist if you don't want to do it manually: search "snowflake id decoder".
### Direct profile URL by ID
@@ -0,0 +1,38 @@
---
title: "Directory Discovery"
description: "Techniques and tools for discovering hidden directories and files on web servers."
tags: ["web", "enumeration", "discovery", "directory"]
publishDate: 2026-06-01
---
## FFUF
See also [FFUF](/notes/web/ffuf) for fuzzing-based directory discovery.
## Robots.txt
```bash
curl -s $url/robots.txt
```
## Sitemap.xml
```bash
curl -s $url/sitemap.xml
```
## Dirb
```bash
dirb $url
```
## Spider - Katana
A spider is a tool that crawls a website and collects information about its
structure and content. It can be used to find hidden directories, files, and
parameters.
```bash
katana -c 15 -p 15 -u $url > output
```
+33
View File
@@ -0,0 +1,33 @@
---
title: "FFUF"
description: "Reference and usage examples for ffuf, a fast web fuzzer for directories, endpoints and subdomains."
tags:
["web", "enumeration", "discovery", "subdomain", "directory", "bruteforce"]
publishDate: 2026-06-01
---
**Fuff (or ffuf)** is a fast web fuzzer written in Go, mainly used in
cybersecurity to discover hidden directories, files, API endpoints, subdomains,
vhosts and more. Its speed and flexibility make it a must-have for pentesters
and bug bounty hunters.
```bash
# Flags:
# -rate 50 -t 50 # Limit requests to 50 per second with 50 concurrent threads
# -X POST|GET|PUT # Set method
# -e .php,.asp,.bak,.db # Set the extension
# -recursion -recursion-depth 3 # Recursive fuzzing up to 3 levels deep
# -fc 404,500 # Exclude responses with status codes 404 and 500
# Examples:
ffuf -w wordlist.txt -u $url/FUZZ # Basic directory/file fuzzing using a wordlist
ffuf -w subdomains.txt -u https://FUZZ.$url # Subdomain fuzzing
ffuf -w vhosts.txt -u $url -H "Host: https://FUZZ.$url" # Virtual host fuzzing by modifying the Host header
ffuf -w wordlist.txt -u $url/page.php?FUZZ=value # GET parameter fuzzing in the query string
ffuf -w wordlist.txt -u $url/api -X POST -d 'FUZZ=value' # POST body parameter fuzzing
ffuf -w wordlist.txt -u $url/FUZZ -b 'session=abcdef' # Use a session cookie during fuzzing
ffuf -w headers.txt -u $url -H "X-Custom-Header: FUZZ" # HTTP header fuzzing
ffuf -w passwords.txt -X POST -u $url/login -d "username=admin&password=FUZZ" # Password brute-forcing for user "admin"
ffuf -w users.txt:USER -w passwords.txt:PASS -u "$url/login?username=USER&password=PASS" -mode pitchfork # Pitchfork mode: matches each line from both wordlists (USER[i], PASS[i])
ffuf -w users.txt:USER -w passwords.txt:PASS -u "$url/login?username=USER&password=PASS" -mode clusterbomb # Clusterbomb mode: tests every user with every password combination
```
@@ -0,0 +1,85 @@
---
title: "Subdomains Discovery"
description: "Methods and tools for enumerating subdomains of a target domain."
tags: ["web", "enumeration", "discovery", "subdomain"]
publishDate: 2026-06-01
---
## FFUF
See also [FFUF](/notes/web/ffuf) for fuzzing-based subdomain discovery.
## Google Dorking
Google dorks can surface subdomains indexed by Google without any active scanning.
```
site:*.$domain
site:*.$domain -www
site:*.$domain inurl:admin
site:*.$domain ext:php | ext:json | ext:xml
```
## Certificate Transparency
CT logs record every TLS certificate ever issued for a domain. Querying them is
passive and reliable.
```bash
curl -s "https://crt.sh/?q=%25.$domain&output=json" | jq '.[].name_value' | sort -u
```
Tools that aggregate CT logs:
- [crt.sh](https://crt.sh)
- [censys.io](https://search.censys.io)
## Passive DNS
Passive DNS databases store historical DNS resolutions collected from resolvers
worldwide; useful for finding subdomains that no longer resolve but once did.
```bash
# Amass (passive mode, no active scanning)
amass enum -passive -d $domain
# subfinder (uses many passive sources)
subfinder -d $domain -silent
```
## DMARC
DMARC can reveal more domains associated with a target.
Go to `dmarc.live/info/$domain`, it allows you to find domains using the
same DMARC record.
## ASN & IP Ranges
Finding the ASN of a target exposes its entire IP range, which may contain
undiscovered subdomains or related infrastructure.
```bash
# Get ASN from an IP
whois $ip | grep -i "asn\|orgname\|origin"
# Get IP ranges from ASN
whois -h whois.radb.net -- '-i origin AS12345' | grep route
```
## Favicon Hash
A unique favicon can be fingerprinted to find other domains hosted by the same
organisation, including subdomains on non-standard ports.
```bash
# Compute the MMH3 hash of the favicon
python3 -c "
import requests, mmh3, base64
r = requests.get('https://$domain/favicon.ico')
h = mmh3.hash(base64.encodebytes(r.content))
print(h)
"
```
Then search the hash on [Shodan](https://shodan.io): `http.favicon.hash:<hash>`
+229 -117
View File
@@ -7,7 +7,7 @@
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "anotherhadi@noreply.git.hadi.icu",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
@@ -34,7 +34,7 @@
"fork": false,
"template": false,
"mirror": true,
"size": 429945,
"size": 430163,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/nixy/languages",
"html_url": "https://git.hadi.icu/anotherhadi/nixy",
@@ -47,13 +47,14 @@
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:31:04+02:00",
"updated_at": "2026-04-23T09:52:11+02:00",
"updated_at": "2026-05-07T09:47:53+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
@@ -90,7 +91,7 @@
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-04-23T11:17:25+02:00",
"mirror_updated": "2026-05-07T16:22:32+02:00",
"topics": [
"dotfiles",
"hyprland",
@@ -106,6 +107,220 @@
},
"banner_url": "https://git.hadi.icu/anotherhadi/nixy/raw/branch/main/.github/assets/banner.png"
},
{
"id": 9,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "usbguard-tui",
"full_name": "anotherhadi/usbguard-tui",
"description": "A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 425,
"language": "Go",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui/languages",
"html_url": "https://git.hadi.icu/anotherhadi/usbguard-tui",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui",
"link": "",
"ssh_url": "gitea@git.hadi.icu:anotherhadi/usbguard-tui.git",
"clone_url": "https://git.hadi.icu/anotherhadi/usbguard-tui.git",
"original_url": "https://github.com/anotherhadi/usbguard-tui",
"website": "",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-04-30T17:40:42+02:00",
"updated_at": "2026-05-06T14:42:02+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T12:42:33+02:00",
"topics": [
"bubbletea",
"tui",
"usbguard"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/usbguard-tui",
"gitlab": "https://gitlab.com/anotherhadi_mirror/usbguard-tui"
},
"banner_url": "https://git.hadi.icu/anotherhadi/usbguard-tui/raw/branch/main/.github/assets/banner.png"
},
{
"id": 3,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "blog",
"full_name": "anotherhadi/blog",
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 3863,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
"html_url": "https://git.hadi.icu/anotherhadi/blog",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
"link": "",
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
"original_url": "https://github.com/anotherhadi/blog",
"website": "https://hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:39:47+02:00",
"updated_at": "2026-05-04T17:03:05+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T17:22:32+02:00",
"topics": [
"blog",
"cybersecurity",
"portfolio"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/blog",
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
},
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
},
{
"id": 6,
"owner": {
@@ -114,7 +329,7 @@
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "anotherhadi@noreply.git.hadi.icu",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
@@ -141,7 +356,7 @@
"fork": false,
"template": false,
"mirror": true,
"size": 676,
"size": 681,
"language": "Svelte",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/iknowyou/languages",
"html_url": "https://git.hadi.icu/anotherhadi/iknowyou",
@@ -154,6 +369,7 @@
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 2,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
@@ -197,7 +413,7 @@
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-04-23T14:17:25+02:00",
"mirror_updated": "2026-05-07T20:02:34+02:00",
"topics": [
"osint",
"osint-tool"
@@ -211,112 +427,6 @@
},
"banner_url": "https://git.hadi.icu/anotherhadi/iknowyou/raw/branch/main/.github/assets/banner.png"
},
{
"id": 3,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "blog",
"full_name": "anotherhadi/blog",
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 3009,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
"html_url": "https://git.hadi.icu/anotherhadi/blog",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
"link": "",
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
"original_url": "https://github.com/anotherhadi/blog",
"website": "https://hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:39:47+02:00",
"updated_at": "2026-04-11T17:35:02+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-04-23T11:17:26+02:00",
"topics": [
"blog",
"cybersecurity",
"portfolio"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/blog",
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
},
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
},
{
"id": 2,
"owner": {
@@ -325,7 +435,7 @@
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "anotherhadi@noreply.git.hadi.icu",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
@@ -352,7 +462,7 @@
"fork": false,
"template": false,
"mirror": true,
"size": 530,
"size": 535,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/default-creds/languages",
"html_url": "https://git.hadi.icu/anotherhadi/default-creds",
@@ -365,6 +475,7 @@
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
@@ -408,7 +519,7 @@
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-04-23T11:17:25+02:00",
"mirror_updated": "2026-05-07T17:12:33+02:00",
"topics": [
"cybersecurity",
"cybersecurity-tools",
@@ -432,7 +543,7 @@
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "anotherhadi@noreply.git.hadi.icu",
"email": "1+anotherhadi@noreply.git.hadi.icu",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
@@ -472,6 +583,7 @@
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
+6 -1
View File
@@ -45,7 +45,12 @@ const sortedPosts = blogPosts.sort(
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{sortedPosts.map((post) => (
<BlogCard post={post} />
<BlogCard
displayBanner={true}
displayTags={true}
displayDate={true}
post={post}
/>
))}
</div>
)
+7 -2
View File
@@ -3,26 +3,31 @@ import Layout from "../layouts/Layout.astro";
import Hero from "../components/Hero.astro";
import Projects from "../components/Projects.astro";
import Blog from "../components/Blog.astro";
import Notes from "../components/Notes.astro";
import Contact from "../components/Contact.astro";
import { siteConfig } from "../config";
import avatar from "../../public/avatar.jpg";
---
<Layout title={`Another Hadi`} description={siteConfig.description}>
<main>
<main class="px-10">
<Hero
name={siteConfig.name}
title={siteConfig.title}
description={siteConfig.description}
avatar={avatar}
location={siteConfig.location}
socialLinks={siteConfig.socialLinks}
gpgKey={siteConfig.gpgKey}
rssFeed={siteConfig.rssFeed}
/>
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Blog />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Notes />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Projects />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Contact />
</main>
</Layout>
+38 -77
View File
@@ -3,10 +3,10 @@ import { getCollection, render } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import { List, PanelRight } from "@lucide/astro";
import NoteTOC from "../../components/NoteTOC.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
import NoteVars from "../../components/NoteVars.svelte";
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
import { getCategory, extractLinks, extractExternalLinks } from "../../utils/notes";
export async function getStaticPaths() {
const notes = await getCollection("notes");
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const { Content, headings: astroHeadings } = await render(entry);
const allNotes = await getCollection("notes");
const sortedNotes = allNotes.sort((a, b) =>
@@ -60,14 +60,22 @@ const noteVars = [
),
];
const headings = extractHeadings(entry.body ?? "");
const headings = astroHeadings.map((h) => ({ depth: h.depth, text: h.text, id: h.slug }));
const externalLinks = extractExternalLinks(entry.body ?? "");
---
<style>
.drawer.lg\:drawer-open > .drawer-side,
.drawer.xl\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
@media (min-width: 768px) {
.drawer.md\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
@media (min-width: 1280px) {
.drawer.xl\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
</style>
@@ -76,12 +84,12 @@ const headings = extractHeadings(entry.body ?? "");
description={entry.data.description}
>
<main class="max-w-screen-2xl mx-auto">
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
<div class="drawer lg:drawer-open w-full">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex min-h-[calc(100vh-3rem)] min-w-0">
<div class="drawer drawer-end xl:drawer-open w-full" id="right-drawer">
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-w-0">
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
@@ -115,7 +123,7 @@ const headings = extractHeadings(entry.body ?? "");
<div class="flex items-center gap-2">
<label
for="nav-drawer"
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<List size={11} />
nav
@@ -124,7 +132,7 @@ const headings = extractHeadings(entry.body ?? "");
<label
for="graph-drawer"
id="graph-toggle"
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
class="btn btn-ghost btn-xs xl:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
title="Toggle graph"
>
<PanelRight size={11} />
@@ -204,32 +212,29 @@ const headings = extractHeadings(entry.body ?? "");
</main>
</div>
<div class="drawer-side z-50">
<div class="drawer-side z-[60]">
<label
for="nav-drawer"
for="graph-drawer"
aria-label="close sidebar"
class="drawer-overlay"></label>
<NoteNavSidebar
client:load
notes={sortedNotes}
currentEntry={entry}
categories={categories}
<NoteGraphSidebar
entry={entry}
graphNodes={graphNodes}
graphEdges={graphEdges}
forwardLinks={forwardLinks}
backlinks={backlinks}
externalLinks={externalLinks}
/>
</div>
</div>
</div>
<div class="drawer-side z-40">
<label
for="graph-drawer"
aria-label="close sidebar"
class="drawer-overlay xl:hidden"></label>
<NoteGraphSidebar
entry={entry}
graphNodes={graphNodes}
graphEdges={graphEdges}
forwardLinks={forwardLinks}
backlinks={backlinks}
<div class="drawer-side z-[70]">
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<NoteNavSidebar
notes={sortedNotes}
currentEntry={entry}
categories={categories}
/>
</div>
</div>
@@ -290,51 +295,7 @@ const headings = extractHeadings(entry.body ?? "");
});
}
function initXlGraphToggle() {
const graphDrawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
if (!graphDrawer) return;
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
const xlQuery = window.matchMedia("(min-width: 1280px)");
const STORAGE_KEY = "notes-graph-sidebar";
function setXlSidebar(open: boolean) {
if (!outerDrawer) return;
if (open) {
outerDrawer.classList.add("xl:drawer-open");
} else {
outerDrawer.classList.remove("xl:drawer-open");
}
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
}
const graphToggle = document.getElementById("graph-toggle");
graphToggle?.addEventListener("click", (e) => {
if (!xlQuery.matches) return;
e.preventDefault();
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
});
if (xlQuery.matches) {
const saved = localStorage.getItem(STORAGE_KEY);
// Open by default unless user explicitly closed it
setXlSidebar(saved !== "0");
}
xlQuery.addEventListener("change", (e) => {
if (!e.matches) {
outerDrawer?.classList.remove("xl:drawer-open");
} else {
const saved = localStorage.getItem(STORAGE_KEY);
setXlSidebar(saved !== "0");
}
});
}
document.addEventListener("astro:page-load", () => {
injectHeadingAnchors();
initXlGraphToggle();
});
</script>
+10 -12
View File
@@ -1,7 +1,7 @@
---
import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
import { getCategory } from "../../utils/notes";
import { List } from "@lucide/astro";
@@ -41,9 +41,11 @@ if (!categoryNotes) {
---
<style>
.drawer.lg\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
@media (min-width: 768px) {
.drawer.md\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
</style>
@@ -52,7 +54,7 @@ if (!categoryNotes) {
description={`Notes on ${category}.`}
>
<main class="max-w-screen-2xl mx-auto">
<div class="drawer lg:drawer-open min-h-[calc(100vh-3rem)]">
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-w-0">
@@ -72,7 +74,7 @@ if (!categoryNotes) {
</div>
<label
for="nav-drawer"
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<List size={11} />
nav
@@ -161,13 +163,9 @@ if (!categoryNotes) {
</main>
</div>
<div class="drawer-side z-50">
<label
for="nav-drawer"
aria-label="close sidebar"
class="drawer-overlay"></label>
<div class="drawer-side z-[70]">
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<NoteNavSidebar
client:load
notes={allNotes}
currentCategory={category}
categories={categories}
+1 -1
View File
@@ -36,7 +36,7 @@ import repos from "../../data/repos.json";
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{repos.map((repo) => (
<GiteaProjectCard repo={repo} />
<GiteaProjectCard displayBanner={true} repo={repo} />
))}
</div>
)
+13 -2
View File
@@ -36,6 +36,13 @@
--noise: 0;
}
.drawer-side > aside > astro-island {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.btn:not(.btn-circle):not(.btn-square) {
@apply rounded-lg;
}
@@ -50,7 +57,9 @@
background: transparent;
color: var(--color-base-content);
text-decoration: none !important;
transition: background 0.15s ease, border-color 0.15s ease;
transition:
background 0.15s ease,
border-color 0.15s ease;
margin-block: 0.25rem;
}
.link-card::after {
@@ -60,7 +69,9 @@
opacity: 0;
color: var(--color-primary);
font-size: 0.75rem;
transition: opacity 0.15s ease, transform 0.15s ease;
transition:
opacity 0.15s ease,
transform 0.15s ease;
transform: translate(-4px, 4px);
flex-shrink: 0;
}
+28
View File
@@ -15,6 +15,34 @@ function slugify(text: string): string {
.replace(/ +/g, "-");
}
export function extractExternalLinks(body: string): { url: string; label: string }[] {
const seen = new Set<string>();
const links: { url: string; label: string }[] = [];
// Markdown: [label](https://...)
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
let m;
while ((m = mdRe.exec(body)) !== null) {
if (!seen.has(m[2])) {
seen.add(m[2]);
links.push({ url: m[2], label: m[1] });
}
}
// HTML: <a href="https://...">...</a> — use h4 content as label if present, else href host
const htmlRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
while ((m = htmlRe.exec(body)) !== null) {
const url = m[1];
if (seen.has(url)) continue;
seen.add(url);
const h4 = m[2].match(/<h4[^>]*>([\s\S]*?)<\/h4>/);
const label = h4 ? h4[1].trim() : new URL(url).hostname;
links.push({ url, label });
}
return links;
}
export function extractLinks(body: string): string[] {
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
const ids: string[] = [];