initial: Astro port of Hype404 with favicon + SEO redirects

This commit is contained in:
2026-04-20 04:52:49 +00:00
commit 8dc5b450a8
542 changed files with 8168 additions and 0 deletions

151
src/lib/markdown.js Normal file
View File

@@ -0,0 +1,151 @@
import fs from 'node:fs';
import path from 'node:path';
import matter from 'gray-matter';
import { marked } from 'marked';
const CONTENT_DIR = path.resolve(process.cwd(), 'src/content');
marked.setOptions({ breaks: true, gfm: true });
function embedFor(match) {
let m;
if ((m = match.match(/^\{\{youtube:([a-zA-Z0-9_-]+)(\?[^}]*)?\}\}$/))) {
const src = `https://www.youtube.com/embed/${m[1]}${m[2] || ''}`;
return `<div class="video-container"><iframe width="560" height="315" src="${src}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
}
if ((m = match.match(/^\{\{tiktok:([0-9]+)\}\}$/))) {
return `<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@user/video/${m[1]}" data-video-id="${m[1]}" style="max-width:605px;min-width:325px;"><section></section></blockquote><script async src="https://www.tiktok.com/embed.js"></script>`;
}
if ((m = match.match(/^\{\{twitter:([0-9]+)\}\}$/))) {
return `<blockquote class="twitter-tweet"><a href="https://twitter.com/x/status/${m[1]}"></a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`;
}
if (match === '{{facebook}}') {
return `<div class="fb-post" data-href=""></div><div id="fb-root"></div><script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v18.0"></script>`;
}
if ((m = match.match(/^\{\{iframe:([^}]+)\}\}$/))) {
return `<iframe src="${m[1]}" frameborder="0" allowfullscreen style="max-width:100%;height:auto;"></iframe>`;
}
return match;
}
const EMBED_RE = /\{\{(?:youtube:[a-zA-Z0-9_-]+(?:\?[^}]*)?|tiktok:[0-9]+|twitter:[0-9]+|facebook|iframe:[^}]+)\}\}/g;
function protectEmbeds(md) {
const tokens = [];
const transformed = md.replace(EMBED_RE, (match) => {
const placeholder = `\n\n<!--EMBED_${tokens.length}-->\n\n`;
tokens.push(embedFor(match));
return placeholder;
});
return { transformed, tokens };
}
function restoreEmbeds(html, tokens) {
return html.replace(/<!--EMBED_(\d+)-->/g, (_m, i) => tokens[Number(i)] || '');
}
function decodeTitle(data) {
if (data.titleBase64) {
return Buffer.from(data.titleBase64, 'base64').toString('utf-8');
}
return data.title || 'Untitled';
}
export function getExcerpt(content, maxLength = 160) {
let text = content
.replace(/\{\{(youtube|tiktok|twitter|facebook|iframe):[^}]*\}\}/g, '')
.replace(/!\[.*?\]\(.*?\)/g, '')
.replace(/\[.*?\]\(.*?\)/g, '')
.replace(/#{1,6}\s/g, '')
.replace(/\*\*/g, '')
.replace(/__/g, '')
.replace(/`/g, '')
.replace(/\n+/g, ' ')
.trim();
text = text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
if (text.length > maxLength) text = text.substring(0, maxLength - 3) + '...';
return text || '';
}
function loadMarkdownDir(dir) {
const fullPath = path.join(CONTENT_DIR, dir);
if (!fs.existsSync(fullPath)) return [];
return fs
.readdirSync(fullPath)
.filter((f) => f.endsWith('.md'))
.map((file) => {
const raw = fs.readFileSync(path.join(fullPath, file), 'utf-8');
const { data, content } = matter(raw);
return {
slug: file.replace(/\.md$/, ''),
title: decodeTitle(data),
date: data.date ? new Date(data.date).toISOString() : new Date().toISOString(),
tags: data.tags || [],
published: data.published !== false,
body: content,
rawExcerpt: data.excerpt || getExcerpt(content, 200),
frontmatter: data,
};
});
}
let _posts = null;
export function getAllPosts() {
if (_posts) return _posts;
_posts = loadMarkdownDir('posts')
.filter((p) => p.published)
.sort((a, b) => new Date(b.date) - new Date(a.date));
return _posts;
}
export function renderMarkdown(body) {
const { transformed, tokens } = protectEmbeds(body);
const html = marked.parse(transformed);
return restoreEmbeds(html, tokens);
}
export function getRelatedPosts(currentPost, allPosts, limit = 4) {
if (!currentPost.tags || currentPost.tags.length === 0) {
return allPosts.filter((p) => p.slug !== currentPost.slug).slice(0, limit);
}
const scored = allPosts
.filter((p) => p.slug !== currentPost.slug)
.map((post) => ({
post,
score: post.tags.filter((t) => currentPost.tags.includes(t)).length,
}))
.filter((i) => i.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((i) => i.post);
if (scored.length < limit) {
const remaining = allPosts
.filter((p) => p.slug !== currentPost.slug && !scored.find((s) => s.slug === p.slug))
.slice(0, limit - scored.length);
scored.push(...remaining);
}
return scored.slice(0, limit);
}
export function extractFirstImage(html) {
const m = html.match(/<img[^>]+src="(\/images\/[^"]+)"/);
return m ? m[1] : null;
}
export function getPage(slug) {
const filePath = path.join(CONTENT_DIR, 'pages', `${slug}.md`);
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, 'utf-8');
const { data, content } = matter(raw);
return {
slug,
title: decodeTitle(data),
body: content,
};
}

6
src/lib/site.js Normal file
View File

@@ -0,0 +1,6 @@
export const site = {
name: 'Hype404',
description: 'Street Culture Blog',
url: 'https://hype404.com',
postsPerPage: 10,
};