Add image search metadata: CC0 license data, sitemap, robots, 404 page

Every tile image gets ImageObject license data so search engines can list it
as Creative Commons, the build writes sitemap.xml with all tile images,
robots.txt points at it, unknown paths get a real 404 instead of the gallery,
and the page gets a descriptive title, canonical, large image previews and
an og.png collage for link previews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 21:53:49 +00:00
co-authored by Claude Opus 5.5
parent 25b7109665
commit c910011cd2
5 changed files with 71 additions and 11 deletions
+14 -2
View File
@@ -3,9 +3,21 @@
<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>vibegif</title> <title>vibegif · <!--count--> free public domain gifs and animated webp</title>
<link rel="icon" type="image/webp" href="/favicon.webp"> <link rel="icon" type="image/webp" href="/favicon.webp">
<meta name="description" content="public domain gallery of <!--count--> gifs and animated webp"> <link rel="canonical" href="https://vibegif.direct-img.link/">
<meta name="description" content="public domain gallery of <!--count--> gifs and animated webp, free to use and hot link (CC0)">
<meta name="robots" content="max-image-preview:large">
<meta property="og:type" content="website">
<meta property="og:site_name" content="vibegif">
<meta property="og:url" content="https://vibegif.direct-img.link/">
<meta property="og:title" content="vibegif · <!--count--> free public domain gifs and animated webp">
<meta property="og:description" content="public domain gallery of <!--count--> gifs and animated webp, free to use and hot link (CC0)">
<meta property="og:image" content="https://vibegif.direct-img.link/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<!--ld-->
<script>try{localStorage.bg&&document.documentElement.classList.add("dark")}catch{}</script> <script>try{localStorage.bg&&document.documentElement.classList.add("dark")}catch{}</script>
<style> <style>
:root{--bg:#fff;--fg:#000;--mute:#888} :root{--bg:#fff;--fg:#000;--mute:#888}
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>not found · vibegif</title>
<meta name="robots" content="noindex">
<link rel="icon" type="image/webp" href="/favicon.webp">
<script>try{localStorage.bg&&document.documentElement.classList.add("dark")}catch{}</script>
<style>
:root{--bg:#fff;--fg:#000;--mute:#888}
.dark{--bg:#000;--fg:#fff;--mute:#777}
@font-face{font-family:"Stain";src:url("https://cdn.jsdelivr.net/gh/multipleof4/stain.otf@master/dist/Stain.otf") format("opentype");font-display:swap}
body{background:var(--bg);color:var(--fg);font-family:"Stain",sans-serif;text-align:center;margin:0;padding:15vh 16px}
h1{font-weight:400;font-size:clamp(22px,5vw,44px);margin:16px 0}
a{color:var(--mute);font-size:clamp(16px,3vw,22px)}
a:hover{color:var(--fg)}
</style>
</head>
<body>
<img src="/ghost/float.webp" width="180" height="180" alt="ghost float">
<h1>nothing here</h1>
<a href="/">back to the gallery</a>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://vibegif.direct-img.link/sitemap.xml
+28 -9
View File
@@ -4,13 +4,16 @@
// hand-made files that have no gen file go in the first section // hand-made files that have no gen file go in the first section
// only files inside a subject folder are tiles, so root files like favicon.webp stay out // only files inside a subject folder are tiles, so root files like favicon.webp stay out
// tile images get ?v=<hash> so browsers drop old copies when a file changes, links stay clean for hotlinking // tile images get ?v=<hash> so browsers drop old copies when a file changes, links stay clean for hotlinking
// <!--ld--> gets CC0 license data for every tile image, so image search can list them under creative commons
// and the build writes sitemap.xml with the page and its tile images (google reads up to 1000 images per page)
import{readdirSync as rd,readFileSync as rf}from'fs' import{readdirSync as rd,readFileSync as rf}from'fs'
import{createHash}from'crypto' import{createHash}from'crypto'
const site='https://vibegif.direct-img.link',cc0='https://creativecommons.org/publicdomain/zero/1.0/'
const sections=['bw','color','anime','manga','mini','minicolor'] const sections=['bw','color','anime','manga','mini','minicolor']
const hidden=['cat/tail.gif'] // same drawing as cat/wag.gif, kept so old links still work const hidden=['cat/tail.gif'] // same drawing as cat/wag.gif, kept so old links still work
const walk=d=>rd(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(`${d}/${e.name}`):`${d}/${e.name}`) const walk=d=>rd(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(`${d}/${e.name}`):`${d}/${e.name}`)
const dims=(b,x)=>x=='gif'?[b.readUInt16LE(6),b.readUInt16LE(8)]:[b.readUIntLE(24,3)+1,b.readUIntLE(27,3)+1] const dims=(b,x)=>x=='gif'?[b.readUInt16LE(6),b.readUInt16LE(8)]:[b.readUIntLE(24,3)+1,b.readUIntLE(27,3)+1]
const gallery=()=>{ const tiles=()=>{
const sec={},g=Object.fromEntries(sections.map(s=>[s,{}])) const sec={},g=Object.fromEntries(sections.map(s=>[s,{}]))
for(const f of walk('gen').filter(f=>f.endsWith('.js'))){const[,s,...r]=f.split('/');sec[r.join('/').slice(0,-3)]=s} for(const f of walk('gen').filter(f=>f.endsWith('.js'))){const[,s,...r]=f.split('/');sec[r.join('/').slice(0,-3)]=s}
for(const f of walk('public').filter(f=>/^public\/[^/]+\/.+\.(webp|gif)$/.test(f)).sort()){ for(const f of walk('public').filter(f=>/^public\/[^/]+\/.+\.(webp|gif)$/.test(f)).sort()){
@@ -18,12 +21,28 @@ const gallery=()=>{
if(!hidden.includes(p))(g[sec[id]??sections[0]][id]??={})[x]=f if(!hidden.includes(p))(g[sec[id]??sections[0]][id]??={})[x]=f
} }
return sections.map(s=>Object.entries(g[s]).map(([id,fm])=>{ return sections.map(s=>Object.entries(g[s]).map(([id,fm])=>{
const x=fm.webp?'webp':'gif',b=rf(fm[x]),[w,h]=dims(b,x),v=createHash('md5').update(b).digest('hex').slice(0,8) const x=fm.webp?'webp':'gif',b=rf(fm[x]),[w,h]=dims(b,x)
const links=['webp','gif'].filter(k=>fm[k]).map(k=>`<a href="/${id}.${k}">${k}</a>`).join(' ') return{id,fm,x,w,h,src:`/${id}.${x}?v=${createHash('md5').update(b).digest('hex').slice(0,8)}`}
return`<figure><a href="/${id}.${x}"><img src="/${id}.${x}?v=${v}" alt="${id.replace('/',' ')}" width="${w}" height="${h}" loading="lazy" decoding="async"></a><figcaption>${links}</figcaption></figure>` }))
}).join('\n')).filter(Boolean).join('\n<hr>\n')
} }
export default{plugins:[{name:'gallery',transformIndexHtml:h=>{ const figure=t=>{
const g=gallery() const links=['webp','gif'].filter(k=>t.fm[k]).map(k=>`<a href="/${t.id}.${k}">${k}</a>`).join(' ')
return h.replace('<!--gallery-->',g).replaceAll('<!--count-->',g.split('<figure>').length-1) return`<figure><a href="/${t.id}.${t.x}"><img src="${t.src}" alt="${t.id.replace('/',' ')}" width="${t.w}" height="${t.h}" loading="lazy" decoding="async"></a><figcaption>${links}</figcaption></figure>`
}}]} }
const ld=all=>`<script type="application/ld+json">${JSON.stringify({'@context':'https://schema.org','@graph':
all.map(t=>({'@type':'ImageObject',contentUrl:site+t.src,license:cc0,acquireLicensePage:site+'/'}))})}</script>`
const sitemap=all=>`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
<url><loc>${site}/</loc><lastmod>${new Date().toISOString().slice(0,10)}</lastmod>
${all.map(t=>`<image:image><image:loc>${site+t.src}</image:loc></image:image>`).join('\n')}
</url>
</urlset>
`
export default{plugins:[{name:'gallery',
transformIndexHtml:h=>{
const s=tiles(),all=s.flat()
return h.replace('<!--gallery-->',s.map(ts=>ts.map(figure).join('\n')).filter(Boolean).join('\n<hr>\n'))
.replace('<!--ld-->',ld(all)).replaceAll('<!--count-->',all.length)
},
generateBundle(){this.emitFile({type:'asset',fileName:'sitemap.xml',source:sitemap(tiles().flat())})}
}]}