initial: Astro port of Hype404 with favicon + SEO redirects
This commit is contained in:
151
src/lib/markdown.js
Normal file
151
src/lib/markdown.js
Normal 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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/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
6
src/lib/site.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const site = {
|
||||
name: 'Hype404',
|
||||
description: 'Street Culture Blog',
|
||||
url: 'https://hype404.com',
|
||||
postsPerPage: 10,
|
||||
};
|
||||
Reference in New Issue
Block a user