HEX
Server: nginx/1.18.0
System: Linux test-ipsremont 5.4.0-214-generic #234-Ubuntu SMP Fri Mar 14 23:50:27 UTC 2025 x86_64
User: ips (1000)
PHP: 8.0.30
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //var/www/quadcode.com/src/lib/content/blog.ts
/**
 * Local markdown blog loader.
 *
 * Reads .md files from src/content/blog/, parses YAML frontmatter
 * with gray-matter, converts markdown to HTML with marked (GFM),
 * and returns fully-shaped IPost objects compatible with all
 * existing Svelte components.
 */

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
import { getAuthor } from './authors';
import type { IPost } from '$type/post';

const CONTENT_DIR = path.resolve('src/content/blog');

marked.use({ gfm: true, breaks: false });

interface BlogFrontmatter {
  title: string;
  slug: string;
  date: string;
  modified?: string;
  author: string;
  tags?: string[];
  image?: string;
  description?: string;
  readAlso?: { slug: string; title: string; color?: string; type?: string }[];
  faq?: { question: string; answer: string }[];
  views?: number;
  expert?: {
    name: string;
    position: string;
    description: string;
    slug: string;
    linkedin?: string;
    avatar?: string;
  };
}

function estimateReadingTime(text: string): number {
  const words = text.replace(/<[^>]*>/g, '').split(/\s+/).filter(Boolean).length;
  return Math.max(1, Math.round(words / 200));
}

function generateExcerpt(html: string, maxLength = 160): string {
  const text = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength).replace(/\s\S*$/, '') + '...';
}

function frontmatterToIPost(fm: BlogFrontmatter, htmlContent: string): IPost {
  const author = getAuthor(fm.author);
  const readingTime = estimateReadingTime(htmlContent);
  const excerpt = generateExcerpt(htmlContent);
  const imageUrl = fm.image ?? '';

  return {
    id: hashSlug(fm.slug),
    imageData: {
      thumbnail: imageUrl,
      medium: imageUrl,
      mediumLarge: imageUrl,
      large: imageUrl,
    },
    date: fm.date,
    date_gmt: fm.date,
    guid: { rendered: '' },
    modified: fm.modified ?? fm.date,
    modified_gmt: fm.modified ?? fm.date,
    slug: fm.slug,
    status: 'publish',
    type: 'post',
    link: `/blog/${fm.slug}`,
    title: { rendered: fm.title },
    content: { rendered: htmlContent, protected: false },
    excerpt: { rendered: excerpt, protected: false },
    expertData: fm.expert
      ? {
          name: fm.expert.name,
          position: fm.expert.position,
          description: fm.expert.description,
          slug: fm.expert.slug,
          linkedin: fm.expert.linkedin ?? '',
          company: '',
          avatar: {
            thumbnail: fm.expert.avatar ?? '',
            medium: fm.expert.avatar ?? '',
            mediumLarge: fm.expert.avatar ?? '',
            large: fm.expert.avatar ?? '',
          },
        }
      : null,
    author: 0,
    authorData: {
      name: author.name,
      description: author.description,
      position: author.position,
      slug: author.slug,
      totalPosts: author.totalPosts,
      avatar: author.avatar,
      linkedin: author.linkedin,
    },
    featured_media: 0,
    comment_status: 'closed',
    ping_status: 'closed',
    sticky: false,
    template: '',
    format: 'standard',
    meta: { footnotes: '' },
    categories: [],
    tags: [],
    tagsData: fm.tags ?? [],
    views: String(fm.views ?? 0),
    acf: {
      views: String(fm.views ?? 0),
      faq: fm.faq ?? [],
    },
    readAlso: (fm.readAlso ?? []).map((r) => ({
      title: r.title,
      slug: r.slug,
      color: r.color ?? null,
      type: r.type ?? 'post',
    })),
    estReadingTime: String(readingTime),
    yoast_head: '',
    yoast_head_json: {
      title: fm.title,
      robots: {
        index: 'index',
        follow: 'follow',
        'max-snippet': 'max-snippet:-1',
        'max-image-preview': 'max-image-preview:large',
        'max-video-preview': 'max-video-preview:-1',
      },
      og_locale: 'en_US',
      og_type: 'article',
      og_title: fm.title,
      og_description: fm.description ?? excerpt,
      og_url: `/blog/${fm.slug}`,
      og_site_name: 'Quadcode',
      article_published_time: fm.date,
      article_modified_time: fm.modified ?? fm.date,
      og_image: [
        {
          width: 1200,
          height: 630,
          url: imageUrl,
          type: 'image/jpeg',
        },
      ],
      author: author.name,
      twitter_card: 'summary_large_image',
      twitter_misc: {
        'Written by': author.name,
        'Est. reading time': `${readingTime} min`,
      },
    },
  } as IPost;
}

function hashSlug(slug: string): number {
  let hash = 0;
  for (let i = 0; i < slug.length; i++) {
    const char = slug.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash |= 0;
  }
  return Math.abs(hash) + 100000;
}

function parseMarkdownFile(filePath: string): { frontmatter: BlogFrontmatter; html: string } | null {
  try {
    const raw = fs.readFileSync(filePath, 'utf-8');
    const { data, content } = matter(raw);
    const html = marked.parse(content) as string;
    return { frontmatter: data as BlogFrontmatter, html };
  } catch {
    return null;
  }
}

export function getLocalPost(slug: string): IPost | null {
  const filePath = path.join(CONTENT_DIR, `${slug}.md`);
  if (!fs.existsSync(filePath)) return null;

  const parsed = parseMarkdownFile(filePath);
  if (!parsed) return null;

  return frontmatterToIPost(parsed.frontmatter, parsed.html);
}

export function getAllLocalPosts(): IPost[] {
  if (!fs.existsSync(CONTENT_DIR)) return [];

  const files = fs.readdirSync(CONTENT_DIR).filter((f) => f.endsWith('.md'));
  const posts: IPost[] = [];

  for (const file of files) {
    const filePath = path.join(CONTENT_DIR, file);
    const parsed = parseMarkdownFile(filePath);
    if (parsed) {
      posts.push(frontmatterToIPost(parsed.frontmatter, parsed.html));
    }
  }

  return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

export function getAllLocalSlugs(): string[] {
  if (!fs.existsSync(CONTENT_DIR)) return [];
  return fs
    .readdirSync(CONTENT_DIR)
    .filter((f) => f.endsWith('.md'))
    .map((f) => f.replace(/\.md$/, ''));
}