Merge branch 'f0bm' into eins-f0bm

This commit is contained in:
x
2026-01-23 17:03:13 +01:00
8 changed files with 214 additions and 138 deletions

View File

@@ -1,15 +0,0 @@
import db from "../src/inc/sql.mjs";
(async () => {
try {
const item = (await db`select * from items order by id desc limit 1`)?.[0];
console.log("Last Item:", item);
if (item) {
const tags = await db`select * from tags_assign where item_id = ${item.id}`;
console.log("Tags:", tags);
}
} catch (err) {
console.error(err);
}
process.exit(0);
})();

View File

@@ -6,14 +6,13 @@ import { promises as fs } from "fs";
network: "console",
message: _args.join(" "),
args: _args.slice(1),
channel: "#w0bm",
channel: "console",
user: {
prefix: "console!console@console",
nick: "console",
username: "console",
account: "console"
},
raw: {},
reply: (...args) => console.log(args),
replyAction: (...args) => console.log(args),
replyNotice: (...args) => console.log(args)
@@ -25,14 +24,11 @@ import { promises as fs } from "fs";
)).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f }));
try {
if (trigger.length === 0)
if(trigger.length === 0)
return console.error("no matches");
for (const t of trigger) {
console.log(`triggered > ${t.name} (${_e.message})`);
await t.f(_e);
}
} catch (err) {
console.log(`triggered > ${trigger[0].name} (${_e.message})`);
await trigger[0].f(_e);
} catch(err) {
console.error(err);
}
process.exit(0);
})();

View File

@@ -1,3 +0,0 @@
#!/bin/bash
npm i
npm start

View File

@@ -3058,4 +3058,69 @@ input#s_avatar {
50% {
opacity: 1;
}
/* Modern Tags Layout */
.tags-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
padding: 20px 0;
}
.tag-card {
display: flex;
flex-direction: column;
background: var(--badge-bg, #171717);
border-radius: 12px;
overflow: hidden;
text-decoration: none !important;
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid var(--nav-border-color, rgba(255,255,255,0.1));
position: relative;
}
.tag-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.4);
background: var(--dropdown-bg, #232323);
border-color: var(--accent, #9f0);
}
.tag-card-image {
width: 100%;
height: 100px;
overflow: hidden;
position: relative;
background: #000;
}
.tag-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s;
opacity: 0.8;
}
.tag-card:hover .tag-card-image img {
transform: scale(1.1);
opacity: 1;
}
.tag-card-content {
padding: 15px;
display: flex;
flex-direction: column;
gap: 5px;
}
.tag-name {
color: var(--white, #fff);
font-weight: bold;
font-size: 1.1em;
font-family: var(--font, monospace);
}
.tag-count {
color: #888;
font-size: 0.9em;
}

View File

@@ -0,0 +1,41 @@
import crypto from 'crypto';
export default (router, tpl) => {
router.get(/^\/tag_image\/(?<tag>.+)$/, async (req, res) => {
const tag = req.params.tag;
// Create a deterministic hash from the tag
const hash = crypto.createHash('md5').update(tag).digest('hex');
// Generate colors from hash
const c1 = '#' + hash.substring(0, 6);
const c2 = '#' + hash.substring(6, 12);
const c3 = '#' + hash.substring(12, 18);
// Generate some deterministic numbers for shapes
const n1 = parseInt(hash.substring(18, 20), 16);
const n2 = parseInt(hash.substring(20, 22), 16);
const svg = `
<svg width="300" height="150" viewBox="0 0 300 150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:${c1};stop-opacity:1" />
<stop offset="100%" style="stop-color:${c2};stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="150" fill="url(#grad)" />
<circle cx="${n1}%" cy="${n2}%" r="${(n1 + n2) / 4}" fill="${c3}" fill-opacity="0.3" />
<circle cx="${100 - n1}%" cy="${100 - n2}%" r="${(n1 + n2) / 3}" fill="${c3}" fill-opacity="0.2" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="24" fill="#fff" fill-opacity="0.9" font-weight="bold">${tag}</text>
</svg>
`.trim();
res.writeHead(200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=86400'
});
res.end(svg);
});
return router;
};

View File

@@ -9,8 +9,8 @@ import fs from "fs";
import path from "path";
const regex = {
all: /https?:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gi,
yt: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/gi,
all: /https?:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gi,
yt: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/gi,
imgur: /(?:https?:)?\/\/(\w+\.)?imgur\.com\/(\S*)(\..{3,4})/i,
fourchan: /https?:\/\/i\.4cdn\.org\/(\w+)\/(\d+)\.(\w{3,4})/i,
instagram: /(?:https?:\/\/www\.)?instagram\.com\S*?\/(?:p|reel)\/(\w{11})\/?/im
@@ -23,35 +23,35 @@ export default async bot => {
name: "parser",
call: regex.all,
active: true,
f: async e => {
f: e => {
const links = e.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain)) || [];
let repost;
if (e.media)
if(e.media)
links.push(e.media);
if (links.length === 0)
if(links.length === 0)
return false;
if (e.message.match(/\!i(gnore)?\b/))
if(e.message.match(/\!i(gnore)?\b/))
return false;
if (!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined')))
if(!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined')))
return false;
if (e.type === 'tg' && // proto: tg
if(e.type === 'tg' && // proto: tg
!e.message.match(/\!w(0bm)?\b/i) && // !w / !w0bm
!e.raw.forward_date && // is forwarded?
!mediagroupids.has(e.raw.media_group_id) // prepared mediagroup?
) {
return false;
}
else if (e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) {
else if(e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) {
mediagroupids.add(e.raw.media_group_id);
}
console.log(`parsing ${links.length} link${links.length > 1 ? "s" : ""}...`);
for (const link of links) {
links.forEach(async link => {
//if(regex.imgur.test(link))
// return await e.reply(`fuck imgur... seriously`);
@@ -63,7 +63,7 @@ export default async bot => {
// check repost (link)
repost = await queue.checkrepostlink(link);
if (repost)
if(repost)
return await e.reply(`repost motherf0cker (link): ${cfg.main.url.full}/${repost}`);
// generate uuid
@@ -73,41 +73,39 @@ export default async bot => {
// read metadata
let ext;
const proxyArgs = cfg.main.socks ? `--proxy ${cfg.main.socks}` : '';
if (link.match(regex.instagram)) {
if(link.match(regex.instagram)) {
// is instagram
try {
// @flummi -> is there a variable for the actual work directory so it doesn't have to be hardcoded?
const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext;
} catch (err) {
} catch(err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead];
}
}
else if (link.match(regex.imgur)) {
else if(link.match(regex.imgur)) {
// imghure
ext = link.split('.').pop();
}
else if (link.match(regex.yt)) {
else if(link.match(regex.yt)) {
//yt - fuck anti bot protection
try {
const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout);
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext;
} catch (err) {
console.error("YT-DLP Error:", err);
} catch(err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead];
}
}
else if (link.match(regex.fourchan)) {
else if(link.match(regex.fourchan)) {
//4chan - fuck cloudflare :)
try {
const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext;
} catch (err) {
} catch(err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead];
@@ -119,15 +117,14 @@ export default async bot => {
try {
const meta = JSON.parse((await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout);
ext = meta.ext;
} catch (err) {
console.error('err:', err);
if (e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol / check maxfilesize?');
} catch(err) {
const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"];
// this can be undefined for unsupported mime types, but will be caught in the general mime check below
ext = cfg.mimes[tmphead];
}
}
if (!Object.values(cfg.mimes).includes(ext?.toLowerCase())) {
if(!Object.values(cfg.mimes).includes(ext?.toLowerCase())) {
return console.log('mime schmime ' + ext);
}
@@ -139,44 +136,44 @@ export default async bot => {
const start = new Date();
let source;
if (link.match(regex.instagram)) {
if(link.match(regex.instagram)) {
try {
// add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network
source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch (err) {
if (e.type == 'tg')
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) {
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "instagram dl error");
return await e.reply("instagram dl error", err);
}
}
else if (link.match(regex.imgur)) {
else if(link.match(regex.imgur)) {
// imghure via torsocks // needs torsocks setup so this is optional
try {
await queue.exec(`torsocks wget ${link} -O ./tmp/${uuid}.${ext}`);
source = `./tmp/${uuid}.${ext}`;
} catch (err) {
} catch(err) {
console.error('err:', err);
if (e.type == 'tg')
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol');
}
}
else if (link.match(regex.yt)) {
else if(link.match(regex.yt)) {
try {
// add --cookies <path-to-cookies-file> on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network
source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch (err) {
if (e.type == 'tg')
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) {
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error");
return await e.reply("yt dl error", err);
}
}
else if (link.match(regex.fourchan)) {
else if(link.match(regex.fourchan)) {
// 4chan via proxy - fuck cloudflare
try {
source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch (err) {
if (e.type == 'tg')
source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch(err) {
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error");
return await e.reply("yt dl error", err);
}
@@ -185,23 +182,23 @@ export default async bot => {
// everything except the exceptions
try {
source = (await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim();
} catch (err) {
} catch(err) {
console.error('err:', err);
if (e.type == 'tg')
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err);
return await e.reply('something went wrong lol / check maxfilesize?');
}
}
// </download data>
if (!source) {
if (e.type == 'tg')
if(!source) {
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol");
}
if (source.match(/larger than/)) {
if (e.type == 'tg')
if(source.match(/larger than/)) {
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "too large lol");
return await e.reply("too large lol");
}
@@ -209,9 +206,9 @@ export default async bot => {
// filesize check
const size = fs.statSync(source).size;
if (size > maxfilesize) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
if(size > maxfilesize) {
await fs.promises.unlink(source).catch(_=>{});
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`);
return await e.reply(`too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`);
}
@@ -219,28 +216,28 @@ export default async bot => {
// mime check
let mime = (await queue.exec(`file --mime-type -b ${source}`)).stdout.trim();
try {
if (mime == 'video/x-matroska') { // mkv failsafe
if(mime == 'video/x-matroska') { // mkv failsafe
await queue.exec(`ffmpeg -i ./tmp/${uuid}.mkv -codec copy ./tmp/${uuid}.mp4`);
await fs.promises.unlink(source).catch(_ => { });
await fs.promises.unlink(source).catch(_=>{});
source = source.replace(/\.mkv$/, '.mp4');
mime = 'video/mp4';
}
if (source.match(/\.opus$/)) { // opus failsafe
if(source.match(/\.opus$/)) { // opus failsafe
await queue.exec(`ffmpeg -i ./tmp/${uuid}.opus -codec copy ./tmp/${uuid}.ogg`);
await fs.promises.unlink(source);
source = source.replace(/\.opus$/, '.ogg');
mime = 'audio/ogg';
}
} catch (err) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
} catch(err) {
await fs.promises.unlink(source).catch(_=>{});
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol");
return await e.reply("something went wrong lol");
}
if (!Object.keys(cfg.mimes).includes(mime)) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
if(!Object.keys(cfg.mimes).includes(mime)) {
await fs.promises.unlink(source).catch(_=>{});
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `lol, go f0ck yourself (${mime})`);
return await e.reply(`lol, go f0ck yourself (${mime})`);
}
@@ -250,9 +247,9 @@ export default async bot => {
// check repost (checksum)
repost = await queue.checkrepostsum(checksum);
if (repost) {
await fs.promises.unlink(source).catch(_ => { });
if (e.type == 'tg')
if(repost) {
await fs.promises.unlink(source).catch(_=>{});
if(e.type == 'tg')
return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
return await e.reply(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`);
}
@@ -260,7 +257,7 @@ export default async bot => {
const filename = path.basename(source);
await fs.promises.copyFile(source, `./public/b/${filename}`);
await fs.promises.unlink(source).catch(_ => { });
await fs.promises.unlink(source).catch(_=>{});
// user alias
let username = e.user.nick || e.user.username;
@@ -271,42 +268,33 @@ export default async bot => {
where lower(user_alias.alias) ilike ${username}
limit 1
`)?.[0]?.user;
if (alias) {
if(alias) {
username = alias;
}
await db`
insert into items ${db({
src: e.media ? "" : link,
dest: filename,
mime: mime,
size: size,
checksum: checksum,
username: username,
userchannel: e.channel,
usernetwork: e.network,
stamp: ~~(new Date() / 1000),
active: true
}, 'src', 'dest', 'mime', 'size', 'checksum', 'username', 'userchannel', 'usernetwork', 'stamp', 'active')
insert into items ${
db({
src: e.media ? "" : link,
dest: filename,
mime: mime,
size: size,
checksum: checksum,
username: username,
userchannel: e.channel,
usernetwork: e.network,
stamp: ~~(new Date() / 1000),
active: true
}, 'src', 'dest', 'mime', 'size', 'checksum', 'username', 'userchannel', 'usernetwork', 'stamp', 'active')
}
`;
const itemid = await queue.getItemID(filename);
// auto-tag sfw
await db`
insert into tags_assign ${db({
item_id: itemid,
tag_id: 1, // sfw
user_id: 2 // f0ck
}, 'item_id', 'tag_id', 'user_id')
}
`;
// generate thumbnail
try {
await queue.genThumbnail(filename, mime, itemid, link);
} catch (err) {
} catch(err) {
await queue.exec(`magick ./mugge.png ./public/t/${itemid}.webp`);
}
@@ -327,13 +315,13 @@ export default async bot => {
let outputmsgirc = `[f0cked] link: ${cfg.main.url.full}/${itemid} | size: ${lib.formatSize(size)} | speed: ${speed}`;
let outputmsgtg = `[f0cked] size: ${lib.formatSize(size)} | speed: ${speed}`;
if (tags?.length > 0) {
if(tags?.length > 0) {
const tagstr = tags.join(', ');
outputmsgirc += ` | tags: ${tagstr}`;
outputmsgtg += ` | tags: ${tagstr}`;
}
if (e.type == 'tg') {
if(e.type == 'tg') {
await e.deleteMessage(msg.result.chat.id, msg.result.message_id);
await e.reply(outputmsgtg, {
reply_markup: JSON.stringify({
@@ -351,7 +339,7 @@ export default async bot => {
else {
await e.reply(outputmsgirc);
}
}
});
}
}];
};

View File

@@ -2,7 +2,6 @@
<!-- logged in -->
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
<div class="navigation-links-guest">
<ol>
<a href="/tags">tags</a>
@@ -13,7 +12,7 @@
</ol>
</div>
<!-- show pagination only for tags and main page -->
@if(!/^\/\d$/.test(url.pathname))
@if(!/^\/\d+$/.test(url.pathname))
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid">
<div class="pagination-wrapper">
@@ -27,7 +26,6 @@
<!-- not logged in -->
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
<div class="navigation-links-guest">
<ol>
<a href="/tags">tags</a>
@@ -38,7 +36,7 @@
</ol>
</div>
<!-- show pagination only for tags and main page -->
@if(!/^\/\d$/.test(url.pathname))
@if(!/^\/\d+$/.test(url.pathname))
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid">
<div class="pagination-wrapper">

View File

@@ -2,24 +2,30 @@
<div id="main">
<div class="container">
<h3 style="text-align: center;"></h3>
<div class="tags">
<div class="tags-grid">
@if(session)
@each(toptags_regged as toptag)
<div class="tag badge badge-light mr-2">
<div class="tagbox-body">
<span class="toptag_id">{!! toptag.tag !!}</span>
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
<div class="tag-card-image">
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
</div>
</div>
<div class="tag-card-content">
<span class="tag-name">#{!! toptag.tag !!}</span>
<span class="tag-count">{{ toptag.total_items }} posts</span>
</div>
</a>
@endeach
@else
@each(toptags as toptag)
<div class="tag badge badge-light mr-2">
<div class="tagbox-body">
<span class="toptag_id">{!! toptag.tag !!}</span>
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
<div class="tag-card-image">
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
</div>
</div>
<div class="tag-card-content">
<span class="tag-name">#{!! toptag.tag !!}</span>
<span class="tag-count">{{ toptag.total_items }} posts</span>
</div>
</a>
@endeach
@endif
</div>