Every son, collected. A gallery dedicated to the “Son 😭😭😭😭😭” meme and its endless variants: pictures, GIFs and clips, in one place.
soncollection.com · clips · upload · leaderboard
Sonion. Capri-Son. Dy-Son. Sonflower. Sontato. If it has a son in it, it belongs here.
Uploads are free, publish instantly, and are displayed without public attribution unless you sign in.
A community gallery, and an attempt at the most complete collection of son memes there is. Find a son in the wild, upload it, and it gets a page of its own with a link that unfurls properly in Discord, Slack, iMessage and X, an embed code that works on any site, and a spot in the grid.
Every son is one of three things:
- an image (PNG, JPG, WEBP, up to 12 MB), re-encoded on the way in;
- a GIF (up to 12 MB), stored exactly as uploaded so it keeps animating;
- a clip (MP4 or WEBM, up to 60 MB), stored as uploaded and played inline.
Once the collection is large enough, the plan is to publish a cleaned and deduplicated dataset on Hugging Face so researchers, artists, and AI agents can study the format -- and perhaps generate even more sons.
- No NSFW, illegal, hateful, violent, harassing or otherwise harmful content.
- Sons only. The original, its edits and reactions, clips, and the wordplay that grows out of them -- anything with a good-faith son in it.
- Upload originals. Crop out screenshots, browser chrome, social-media interfaces, notifications and unrelated borders first.
- Check for duplicates. Exact duplicates are refused automatically; near-duplicates are on you. We do not need a billion copies of the same son floating around.
- Only upload what you have the right to share. See /tos and /dmca.
- No personal information, and nothing that violates someone's privacy.
Uploads may be removed at any time if they break these rules or create legal, safety, privacy, or moderation concerns.
The upload page takes a link as readily as a file: paste one into the box (or anywhere on the page) and the server fetches the media at original quality and runs it through the same pipeline as an upload. What works, and how:
- X / Twitter posts, through the same public endpoint that powers embedded tweets. The highest-bitrate MP4, or the photo at full resolution. Pure Rust.
- Any page with Open Graph or Twitter Card tags, and any bare image or video URL. Pure Rust.
- YouTube, TikTok, Reddit, Vimeo, Twitch clips, Threads, Bluesky, and
Instagram and Facebook when they cooperate:
yt-dlp, run as a subprocess and pointed only at that allowlist of hosts, withffmpegmerging the streams. Instagram reels and many Facebook videos are shown only to a signed-in account;IMPORT_COOKIES_FILElets the importer be one (see.env.example).
Every fetch the server makes itself goes through one guarded client that resolves the host, refuses anything not publicly routable and pins the connection to the checked addresses. The source page is recorded on the son and shown as a "via" link, which is also where a takedown request looks first.
Nothing is screened before it appears and nothing waits for approval: an upload
is live the moment it is stored. Anyone signed in can report a son; three
reports hide it automatically, and every report lands in /admin for a human,
who can hide or delete anything. That is the mechanism, not a fallback for one.
The best way to contribute is simple:
- Find sons in the wild.
- Check whether they are already in the collection.
- Crop and clean the image, or trim the clip.
- Upload it.
Code contributions are welcome too. The repository is public, and anyone brave enough to improve the infrastructure is appreciated.
Have time and a questionable idea? Generate a son with AI and submit it. Tasteful AI slop is welcome -- label it clearly in the title and follow the same rules.
Uploads are shown without public attribution (first name only if you sign in), but this does not mean complete technical anonymity: the service and its infrastructure process IP addresses, timestamps and request logs for security and abuse prevention. Images are re-encoded, which strips camera metadata; GIFs and videos are stored as uploaded, so strip your own if it matters.
Do not upload anything that could identify you or another person.
The license covering this repository's source code does not apply to user-uploaded media. By uploading, you confirm that:
- you created it, have permission to share it, or reasonably believe its use is lawful;
- it does not violate another person's copyright, trademark, privacy, or other rights; and
- you grant the project permission to store, display, process, moderate and distribute it as part of the site, its API and its embeds, and to include it in downloadable research datasets.
This project and its dataset are intended for personal, cultural, archival, and research use.
Commercial use of the dataset or hosted collection is not permitted without prior written permission.
See the repository license and dataset terms for the exact conditions.
This is an experimental community archive. Availability is not guaranteed, uploads may be removed without notice, and the collection may contain material submitted by third parties.
The maintainers do not endorse every upload.
Technical notes — running it, developing it, deploying it
Pure Rust, including the UI: Leptos 0.8 (SSR + hydration)
on Axum, Tailwind for styling, Cloudflare D1 for data and R2 for media. There
is no sidecar, no Python, no browser automation and no model: the app is one
binary, and the production image is that binary plus cloudflared and the
link importer's three pinned helpers (yt-dlp, deno, ffmpeg).
This site and kirkaversary share one codebase with different identities; a fix in one belongs in the other.
No local database. Dev talks to the same D1 and R2 as production — there is no sqlite fallback to drift from. Clean up anything you upload while testing.
Video is never decoded on the server for an upload. The browser draws a
frame of a picked clip onto a canvas and uploads it as the poster; the server
sniffs the container, hashes the bytes, makes the thumbnail from the poster,
and stores the file as it arrived. ffmpeg is only ever run for a link import,
where no browser is in the loop.
cp .env.example .env # fill in the Cloudflare values; the rest degrade
# gracefully when unset
npm install # Tailwind only; the image builds without Node
cargo install cargo-leptos --locked
cargo leptos watch # http://127.0.0.1:3100watch, not serve — serve doesn't watch anything. Kill the old process
first, or you'll review the previous build.
CI takes a while and the image build is last, so a mistake there costs many minutes to learn something these say in three:
cargo fmt --all -- --check
cargo clippy --no-default-features --features ssr --all-targets -- -D warnings
cargo clippy --no-default-features --features hydrate --target wasm32-unknown-unknown -- -D warnings
cargo test --no-default-features --features ssr
scripts/audit-secrets.sh # no credential in history, files or build outputBoth feature sets, always: ssr and hydrate compile disjoint code from the
same files, so one passing tells you nothing about the other. git config core.hooksPath .githooks runs all of it on every push.
Three scripts, three jobs, each idempotent and each re-runnable on its own:
scripts/cloudflare-provision.sh > secrets.env # D1, R2 + media domain, scoped
# tokens, tunnel + DNS, www
# redirect, Web Analytics
scripts/d1-migrate.sh # every migration, in order
scripts/cloudflare-setup.sh --apply # zone settings that matter here
scripts/google-search-console.py # verify the domain, submit the sitemapsecrets.env is one NAME=value line per GitHub Actions secret the deploy
job reads: set each as a repository secret of the same name. It is gitignored.
Never commit it.
One image, one container: the site and cloudflared under supervisor
(Dockerfile, deploy/supervisord.conf, docker-compose.yml). Push to
main and GitHub Actions builds it, pushes it to GHCR by digest, joins the
tailnet as an ephemeral tag:ci node and runs docker compose up on the host
over Tailscale SSH. There is nothing to click.
Every credential is a GitHub Actions secret, injected as runtime environment —
none is a build argument, none is in an image layer, and there is no .env on
the server. scripts/audit-secrets.sh keeps that a fact rather than an
intention: it scans every object in git history, every tracked file, the
compiled binary, the wasm every visitor downloads, and (with --image) every
layer of the built image.
The container publishes no port. cloudflared reaches the app on loopback
inside it, so the host needs no inbound rule for web traffic.
Everything here is built to be found:
sitemap.xmlwith the Google Images and Google Video extensions,lastmodon every entryImageObject/VideoObject,BreadcrumbListandWebSite+SearchActionJSON-LD- Open Graph (with
og:videofor clips), Twitter cards, oEmbed discovery on every son page /?view=clipsas its own canonical, indexable pagellms.txtfor answer engines;robots.txtthat blocks nothing a crawler should read- IndexNow pings (Bing, Yandex, Seznam, Naver) the moment a son publishes, when
INDEXNOW_KEYis set - A Cloudflare Turnstile captcha in front of every upload and link import, when
TURNSTILE_SITE_KEYandTURNSTILE_SECRETare set; the server verifies each token with Cloudflare before it reads the file - a 301 from
www.to the apex at the edge
CLAUDE.md is the list of things that have already cost real time in this
codebase and the sister site that shares it. It is not documentation of how
the code works; it is a list of traps.
Made for the Sons.
