refactor: migrate route from post/... to blog/...
This commit is contained in:
parent
03ce3caefd
commit
414cd7d3d7
12 changed files with 18 additions and 9 deletions
68
src/pages/blog/[...slug].astro
Normal file
68
src/pages/blog/[...slug].astro
Normal file
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
|
||||
import { getCollection, getEntry } from 'astro:content';
|
||||
import Comments from "../../components/Comments.astro";
|
||||
import {getImage} from "astro:assets";
|
||||
import {siteConfig} from "../../config";
|
||||
import ReplyViaEmail from "../../components/ReplyViaEmail.astro";
|
||||
import { ExtractFirstImage } from '../../plugins/extract-images';
|
||||
import AuthorInfo from "../../components/helper/authors/Info.astro";
|
||||
import TableOfContents from "../../components/TableOfContents.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.slug }, props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
const headings = await entry.render().then(rendered => rendered.headings);
|
||||
|
||||
const noscript = siteConfig.noClientJavaScript
|
||||
const slug = Astro.params.slug;
|
||||
const author = entry.data.author || {collection: 'authors', id: siteConfig.defaultAuthor.id};
|
||||
|
||||
// Get author data
|
||||
const authorData = await getEntry(author);
|
||||
const authorInfo = authorData ? authorData.data : siteConfig.defaultAuthor;
|
||||
|
||||
// get featured image and use it as og:image
|
||||
// use the custom cover image if it exists, otherwise use the featured image file in the same directory
|
||||
const featuredImages = import.meta.glob(`/src/content/posts/*/featured.{avif,png,jpg,jpeg,webp}`,{import:'default',eager:true});
|
||||
const customFeaturedImage = entry.data.cover?.src
|
||||
const matchedImage = Object.keys(featuredImages).find(path => path.includes(slug));
|
||||
let matchedImage_src;
|
||||
if (matchedImage && !customFeaturedImage) {
|
||||
matchedImage_src = await getImage({src: featuredImages[matchedImage], format: 'webp'}) || null;
|
||||
}
|
||||
const firstImageURL = await ExtractFirstImage(Content)
|
||||
|
||||
const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `/blog/${slug}/featured.png` || '';
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={entry.data.title}
|
||||
description={entry.data.description}
|
||||
ogImage={cover}
|
||||
author={authorInfo.name}
|
||||
>
|
||||
<h1 class="title">{entry.data.title}</h1>
|
||||
<AuthorInfo data={authorData} />
|
||||
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
||||
{headings.length !== 0 && <TableOfContents headings={headings} />}
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<div class="extra-post" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<ReplyViaEmail title={entry.data.title} email={authorInfo.email} />
|
||||
<br>
|
||||
<a href="/blog">← Back to posts</a>
|
||||
{noscript && <h2>Comments</h2> <Comments path={`blog/${slug}`} />}
|
||||
</div>
|
||||
|
||||
</Layout>
|
||||
|
25
src/pages/blog/[...slug].txt.js
Normal file
25
src/pages/blog/[...slug].txt.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
|
||||
export const prerender = true;
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.slug }, props: { entry },
|
||||
}));
|
||||
}
|
||||
export async function GET({ props }) {
|
||||
const { entry } = props;
|
||||
// Format the content as plain text
|
||||
const title = entry.data.title;
|
||||
const date = entry.data.pubDate.toISOString().split('T')[0];
|
||||
const content = entry.body;
|
||||
|
||||
// Combine the post info and body into a single text file
|
||||
const textContent = `Title: ${title}\nPublished at: ${date}\n\n${content}`;
|
||||
|
||||
return new Response(textContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
194
src/pages/blog/[slug]/featured.png.js
Normal file
194
src/pages/blog/[slug]/featured.png.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
import sharp from 'sharp';
|
||||
import {getImage} from "astro:assets";
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
// Ensure cache directory exists
|
||||
const CACHE_DIR = 'node_modules/.astro/og-cache';
|
||||
|
||||
// Cache for external images to avoid repeated processing
|
||||
const externalImageCache = new Map();
|
||||
// Cache for generated OG images (memory cache)
|
||||
const ogImageCache = new Map();
|
||||
|
||||
// Initialize cache directory
|
||||
async function ensureCacheDir() {
|
||||
try {
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error('Error creating cache directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create hash for consistent filenames
|
||||
function createHash(str) {
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
// File-based cache operations
|
||||
const fileCache = {
|
||||
async get(key) {
|
||||
try {
|
||||
const filePath = path.join(CACHE_DIR, `${createHash(key)}.webp`);
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
// Check if file exists and is not too old (30 days)
|
||||
const now = new Date();
|
||||
const fileAge = (now - stats.mtime) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (fileAge > 30) {
|
||||
return null; // File is too old
|
||||
}
|
||||
|
||||
return await fs.readFile(filePath);
|
||||
} catch (error) {
|
||||
return null; // File doesn't exist or can't be read
|
||||
}
|
||||
},
|
||||
|
||||
async set(key, data) {
|
||||
try {
|
||||
await ensureCacheDir();
|
||||
const filePath = path.join(CACHE_DIR, `${createHash(key)}.webp`);
|
||||
await fs.writeFile(filePath, data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error writing to file cache:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
return blogEntries.map(post => ({
|
||||
params: { slug: post.slug }, props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
// get the post has a external featured.* image files
|
||||
async function getExternalImage(post) {
|
||||
// Check cache first
|
||||
if (externalImageCache.has(post.slug)) {
|
||||
return externalImageCache.get(post.slug);
|
||||
}
|
||||
|
||||
const featuredImages = import.meta.glob(`/src/content/posts/*/featured.{avif,png,jpg,jpeg,webp}`, {import: 'default', eager: true});
|
||||
const matchedImage = Object.keys(featuredImages).find(path => path.includes(post.slug));
|
||||
let matchedImage_;
|
||||
if (matchedImage) {
|
||||
matchedImage_ = await getImage({src: featuredImages[matchedImage], format: 'webp'}) || null;
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
const result = matchedImage_?.src;
|
||||
externalImageCache.set(post.slug, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to check for images in markdown without rendering
|
||||
function checkForImages(markdownContent) {
|
||||
// Match markdown image syntax  or HTML <img> tags
|
||||
const imageRegex = /!\[.*?]\(.*?\)|<img.*?src=["'].*?["'].*?>/g;
|
||||
return imageRegex.test(markdownContent);
|
||||
}
|
||||
|
||||
// This function dynamically generates og:images for posts that don't have a featured image
|
||||
export async function GET({ props }) {
|
||||
const {post} = props;
|
||||
|
||||
// Generate consistent cache key
|
||||
const cacheKey = `${post.slug}-${post.id}`;
|
||||
|
||||
// Check in-memory cache first (fastest)
|
||||
if (ogImageCache.has(cacheKey)) {
|
||||
return new Response(ogImageCache.get(cacheKey), {
|
||||
headers: {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'ETag': `"${cacheKey}"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Then check file cache (persists between server restarts)
|
||||
const cachedFile = await fileCache.get(cacheKey);
|
||||
if (cachedFile) {
|
||||
// Store in memory cache for faster subsequent access
|
||||
ogImageCache.set(cacheKey, cachedFile);
|
||||
|
||||
return new Response(cachedFile, {
|
||||
headers: {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'ETag': `"${cacheKey}"`,
|
||||
'X-Cache': 'HIT-FILE'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Short-circuit early if we know we won't generate an image
|
||||
if (post.data.cover) {
|
||||
return new Response(null);
|
||||
}
|
||||
|
||||
// Only fetch external image if needed
|
||||
const ExternalImageURL = await getExternalImage(post);
|
||||
if (ExternalImageURL) {
|
||||
return new Response(null);
|
||||
}
|
||||
|
||||
// Only check for images if needed
|
||||
const hasImage = checkForImages(post.body);
|
||||
if (hasImage) {
|
||||
return new Response(null);
|
||||
}
|
||||
|
||||
// Generate an image with post title and description
|
||||
const width = 1280;
|
||||
const height = 720;
|
||||
|
||||
// Sanitize text for SVG
|
||||
const sanitizeText = (text) => {
|
||||
return text
|
||||
? text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
: '';
|
||||
};
|
||||
|
||||
const title = sanitizeText(post.data.title);
|
||||
const description = sanitizeText(post.data.description);
|
||||
|
||||
// Create a simple image with text - optimized SVG
|
||||
const svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"><rect width="${width}" height="${height}" fill="#2e3440"/><text x="50%" y="40%" font-family="JetBrains Mono, monospace" font-size="50" text-anchor="middle" fill="#eceff4">${title}</text><text x="50%" y="60%" font-family="JetBrains Mono, monospace" font-size="30" text-anchor="middle" fill="#81a1c1">${description}</text></svg>`;
|
||||
|
||||
// Convert SVG to WebP with optimized settings
|
||||
const buffer = await sharp(Buffer.from(svg))
|
||||
.webp({ quality: 80, lossless: false })
|
||||
.toBuffer();
|
||||
|
||||
// Store in both memory and file caches
|
||||
ogImageCache.set(cacheKey, buffer);
|
||||
await fileCache.set(cacheKey, buffer);
|
||||
|
||||
// Return the image with proper caching headers
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'ETag': `"${cacheKey}"`,
|
||||
'X-Cache': 'MISS'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating image:', error);
|
||||
return new Response('Error generating image', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue