mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
chore(website): add reusable blog components including IconFeatureGrid, ImageGallery, LinkCards, PostButton, ReleaseMeta, Spoiler, StatusPill, StepRail, TerminalBlock
- Introduced new components for blog posts to enhance UI and functionality. - Added support for featured posts and draft visibility in blog collections. - Created demo blog post to showcase new components and their usage. - Updated existing blog posts to utilize new components and improve content presentation.
This commit is contained in:
1 parent
d19ae82e72
commit
69db46e122
26 files changed
+898
-13
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,44 @@
|
||||
---
|
||||
interface Props {
|
||||
type?: 'info' | 'success' | 'warning';
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { type = 'info', title } = Astro.props;
|
||||
|
||||
const styles = {
|
||||
info: {
|
||||
wrapper: 'border-accent-500/35 bg-accent-500/[0.07]',
|
||||
icon: 'text-accent-300',
|
||||
title: 'text-accent-200',
|
||||
},
|
||||
success: {
|
||||
wrapper: 'border-emerald-500/35 bg-emerald-500/[0.08]',
|
||||
icon: 'text-emerald-300',
|
||||
title: 'text-emerald-200',
|
||||
},
|
||||
warning: {
|
||||
wrapper: 'border-amber-500/35 bg-amber-500/[0.09]',
|
||||
icon: 'text-amber-300',
|
||||
title: 'text-amber-200',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const variant = styles[type];
|
||||
---
|
||||
|
||||
<div class={`not-prose my-6 rounded-xl border border-dashed p-4 sm:p-5 ${variant.wrapper}`}>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class={`mt-0.5 ${variant.icon}`} aria-hidden="true">
|
||||
<svg class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8.25v4.5m0 3h.008M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
{title && <p class={`m-0 text-sm font-semibold ${variant.title}`}>{title}</p>}
|
||||
<div class={`mt-1 text-sm leading-relaxed text-surface-200 [&_p]:my-0 [&_code]:rounded [&_code]:bg-surface-900/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs`}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
interface ChangeRow {
|
||||
area: string;
|
||||
change: string;
|
||||
impact?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
rows: ChangeRow[];
|
||||
}
|
||||
|
||||
const { rows = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 overflow-x-auto rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50">
|
||||
<table class="w-full min-w-[560px] border-collapse text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-dashed border-surface-700/60">
|
||||
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Area</th>
|
||||
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Change</th>
|
||||
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Impact</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr class="border-b border-dashed border-surface-700/40 last:border-b-0">
|
||||
<td class="px-4 py-3 text-sm font-semibold text-surface-200">{row.area}</td>
|
||||
<td class="px-4 py-3 text-sm text-surface-300">{row.change}</td>
|
||||
<td class="px-4 py-3 text-sm text-surface-400">{row.impact ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
interface Props {
|
||||
command: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { command, title = 'Command' } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/55 p-3">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">{title}</span>
|
||||
<button class="copy-command-btn rounded-md border border-dashed border-surface-700/70 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.08em] text-surface-400 transition-colors hover:text-accent-300" data-command={command}>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<code class="block overflow-x-auto rounded-md bg-surface-950/70 px-3 py-2 font-mono text-sm text-accent-200">{command}</code>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const initCopyButtons = () => {
|
||||
document.querySelectorAll('.copy-command-btn').forEach((btn) => {
|
||||
if (btn.getAttribute('data-bound') === 'true') return;
|
||||
btn.setAttribute('data-bound', 'true');
|
||||
btn.addEventListener('click', async () => {
|
||||
const cmd = btn.getAttribute('data-command');
|
||||
if (!cmd) return;
|
||||
await navigator.clipboard.writeText(cmd);
|
||||
const prev = btn.textContent;
|
||||
btn.textContent = 'Copied';
|
||||
setTimeout(() => {
|
||||
btn.textContent = prev || 'Copy';
|
||||
}, 1400);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCopyButtons);
|
||||
} else {
|
||||
initCopyButtons();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
interface FaqItem {
|
||||
q: string;
|
||||
a: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: FaqItem[];
|
||||
}
|
||||
|
||||
const { items = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 space-y-2">
|
||||
{items.map((item) => (
|
||||
<details class="group rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 px-4 py-3">
|
||||
<summary class="cursor-pointer list-none">
|
||||
<span class="flex items-center justify-between gap-3 text-sm font-semibold text-surface-100">
|
||||
<span>{item.q}</span>
|
||||
<span class="text-surface-500 transition-transform duration-200 group-open:rotate-45">+</span>
|
||||
</span>
|
||||
</summary>
|
||||
<p class="mt-2 border-t border-dashed border-surface-700/60 pt-2 text-sm leading-relaxed text-surface-400">{item.a}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
interface FeatureItem {
|
||||
title: string;
|
||||
text: string;
|
||||
icon?: 'spark' | 'shield' | 'speed' | 'playlist' | 'epg';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: FeatureItem[];
|
||||
}
|
||||
|
||||
const { items = [] } = Astro.props;
|
||||
|
||||
const iconPaths = {
|
||||
spark: 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z',
|
||||
shield: 'M12 2l7 4v6c0 5-3.4 9.7-7 10-3.6-.3-7-5-7-10V6l7-4z',
|
||||
speed: 'M12 3v2m6.364.636l-1.414 1.414M21 12h-2m-.636 6.364l-1.414-1.414M12 21v-2m-6.364-.636l1.414-1.414M3 12h2m.636-6.364l1.414 1.414M15 12l-4 4',
|
||||
playlist: 'M4 6h14M4 10h14M4 14h10M4 18h10M18 16l3 2-3 2v-4z',
|
||||
epg: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
} as const;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 grid gap-3 sm:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<div class="rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 p-4">
|
||||
<div class="mb-2.5 flex items-center gap-2.5">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-md border border-accent-500/25 bg-accent-500/[0.1]">
|
||||
<svg class="h-3.5 w-3.5 text-accent-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[item.icon ?? 'spark']} />
|
||||
</svg>
|
||||
</span>
|
||||
<h4 class="m-0 text-sm font-semibold text-surface-100">{item.title}</h4>
|
||||
</div>
|
||||
<p class="m-0 text-sm leading-relaxed text-surface-400">{item.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
interface GalleryImage {
|
||||
src: string;
|
||||
alt: string;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
images: GalleryImage[];
|
||||
columns?: 2 | 3 | 4;
|
||||
}
|
||||
|
||||
const { images = [], columns = 3 } = Astro.props;
|
||||
|
||||
const colClass = {
|
||||
2: 'sm:grid-cols-2',
|
||||
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
||||
} as const;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6">
|
||||
<div class={`grid gap-3 ${colClass[columns]}`}>
|
||||
{images.map((img, idx) => (
|
||||
<figure class="group overflow-hidden rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45">
|
||||
<button
|
||||
type="button"
|
||||
class="gallery-open block w-full"
|
||||
data-src={img.src}
|
||||
data-alt={img.alt}
|
||||
data-caption={img.caption ?? ''}
|
||||
aria-label={`Open image ${idx + 1}`}
|
||||
>
|
||||
<img
|
||||
src={img.src}
|
||||
alt={img.alt}
|
||||
loading="lazy"
|
||||
class="aspect-video w-full object-cover transition-transform duration-300 group-hover:scale-[1.02]"
|
||||
/>
|
||||
</button>
|
||||
{img.caption && <figcaption class="px-3 py-2 text-xs text-surface-500">{img.caption}</figcaption>}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blog-gallery-lightbox" class="fixed inset-0 z-[80] hidden items-center justify-center bg-black/85 p-4" aria-hidden="true">
|
||||
<button type="button" class="gallery-close absolute right-4 top-4 rounded-md border border-surface-600/60 bg-surface-900/70 px-2.5 py-1 text-xs text-surface-300">Close</button>
|
||||
<figure class="max-h-[92vh] max-w-[92vw]">
|
||||
<img id="blog-gallery-lightbox-img" src="" alt="" class="max-h-[82vh] max-w-[92vw] rounded-lg border border-surface-700/70 object-contain" />
|
||||
<figcaption id="blog-gallery-lightbox-cap" class="mt-2 text-center text-sm text-surface-300"></figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const initBlogGallery = () => {
|
||||
const lightbox = document.getElementById('blog-gallery-lightbox');
|
||||
const lightboxImg = document.getElementById('blog-gallery-lightbox-img');
|
||||
const lightboxCap = document.getElementById('blog-gallery-lightbox-cap');
|
||||
if (!lightbox || !lightboxImg || !lightboxCap) return;
|
||||
|
||||
document.querySelectorAll('.gallery-open').forEach((btn) => {
|
||||
if (btn.getAttribute('data-bound') === 'true') return;
|
||||
btn.setAttribute('data-bound', 'true');
|
||||
btn.addEventListener('click', () => {
|
||||
const src = btn.getAttribute('data-src') || '';
|
||||
const alt = btn.getAttribute('data-alt') || '';
|
||||
const caption = btn.getAttribute('data-caption') || '';
|
||||
lightboxImg.setAttribute('src', src);
|
||||
lightboxImg.setAttribute('alt', alt);
|
||||
lightboxCap.textContent = caption;
|
||||
lightbox.classList.remove('hidden');
|
||||
lightbox.classList.add('flex');
|
||||
lightbox.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
lightbox.classList.add('hidden');
|
||||
lightbox.classList.remove('flex');
|
||||
lightbox.setAttribute('aria-hidden', 'true');
|
||||
lightboxImg.setAttribute('src', '');
|
||||
};
|
||||
|
||||
lightbox.querySelector('.gallery-close')?.addEventListener('click', close);
|
||||
lightbox.addEventListener('click', (e) => {
|
||||
if (e.target === lightbox) close();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && lightbox.getAttribute('aria-hidden') === 'false') close();
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBlogGallery);
|
||||
} else {
|
||||
initBlogGallery();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
interface LinkItem {
|
||||
label: string;
|
||||
href: string;
|
||||
hint?: string;
|
||||
icon?: 'github' | 'telegram' | 'sponsor' | 'kofi' | 'download' | 'docs';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
links: LinkItem[];
|
||||
}
|
||||
|
||||
const { links = [] } = Astro.props;
|
||||
|
||||
const iconPaths = {
|
||||
github:
|
||||
'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
|
||||
telegram:
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.27-.02-.12.03-1.99 1.27-5.62 3.72-.53.36-1.01.54-1.44.53-.47-.01-1.38-.27-2.06-.49-.83-.27-1.49-.42-1.43-.88.03-.24.37-.49 1.02-.74 3.99-1.74 6.65-2.89 7.99-3.44 3.81-1.58 4.6-1.86 5.12-1.87.11 0 .37.03.54.17.14.12.18.28.2.45-.01.06.01.24 0 .38z',
|
||||
sponsor:
|
||||
'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
|
||||
kofi: 'M4 5h12a4 4 0 0 1 0 8h-1v2.5A4.5 4.5 0 0 1 10.5 20h-1A4.5 4.5 0 0 1 5 15.5V13H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm11 6h1a2 2 0 0 0 0-4h-1v4Z',
|
||||
download: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4',
|
||||
docs: 'M12 6.253v13m0-13C10.832 5.483 9.246 5 7.5 5 5.205 5 3.182 5.836 2 7.117v11.41C3.182 17.247 5.205 16.41 7.5 16.41c1.746 0 3.332.484 4.5 1.254m0-11.41C13.168 5.483 14.754 5 16.5 5c2.295 0 4.318.836 5.5 2.117v11.41C20.818 17.247 18.795 16.41 16.5 16.41c-1.746 0-3.332.484-4.5 1.254',
|
||||
} as const;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{links.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 p-4 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06]"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-md border border-accent-500/25 bg-accent-500/[0.09]">
|
||||
<svg class="h-3.5 w-3.5 text-accent-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7">
|
||||
{item.icon === 'download' || item.icon === 'docs' ? (
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[item.icon]} />
|
||||
) : (
|
||||
<path fill="currentColor" stroke="none" d={iconPaths[item.icon ?? 'docs']} />
|
||||
)}
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-surface-100">{item.label}</span>
|
||||
</div>
|
||||
{item.hint && <p class="mt-2 text-xs leading-relaxed text-surface-500">{item.hint}</p>}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
interface Props {
|
||||
href: string;
|
||||
label?: string;
|
||||
external?: boolean;
|
||||
}
|
||||
|
||||
const { href, label, external = true } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
class="not-prose group my-6 inline-flex items-center gap-2 rounded-lg border border-dashed border-accent-500/35 bg-accent-500/[0.08] px-4 py-2.5 text-sm font-medium text-accent-200 transition-all duration-200 hover:border-accent-400/50 hover:bg-accent-500/[0.14] hover:text-accent-100"
|
||||
>
|
||||
<span>{label ?? 'Open link'}</span>
|
||||
<svg class="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5H19.5m0 0v6m0-6L10.5 13.5M6 6h3.75M6 10.5V18a1.5 1.5 0 001.5 1.5H15" />
|
||||
</svg>
|
||||
</a>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
interface Props {
|
||||
version: string;
|
||||
releaseDate: string;
|
||||
channels?: string[];
|
||||
}
|
||||
|
||||
const { version, releaseDate, channels = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 p-4 sm:p-5">
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<p class="m-0 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Version</p>
|
||||
<p class="mt-1 text-sm font-semibold text-surface-100">{version}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="m-0 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Release date</p>
|
||||
<p class="mt-1 text-sm font-semibold text-surface-100">{releaseDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="m-0 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Channels</p>
|
||||
<p class="mt-1 text-sm font-semibold text-surface-100">{channels.length ? channels.join(' · ') : 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const { title = 'Show spoiler', open = false } = Astro.props;
|
||||
---
|
||||
|
||||
<details class="not-prose my-5 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 px-4 py-3" open={open}>
|
||||
<summary class="cursor-pointer list-none font-mono text-xs uppercase tracking-[0.1em] text-accent-300">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.968 9.968 0 012.06-3.368m2.25-1.93A9.956 9.956 0 0112 5c4.478 0 8.268 2.943 9.543 7a9.964 9.964 0 01-4.293 5.274M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{title}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="mt-3 border-t border-dashed border-surface-700/60 pt-3 text-sm leading-relaxed text-surface-300 [&_p]:my-2">
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
interface Props {
|
||||
type?: 'new' | 'fixed' | 'improved' | 'breaking' | 'beta';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const { type = 'new', label } = Astro.props;
|
||||
|
||||
const styles = {
|
||||
new: 'border-cyan-500/35 bg-cyan-500/[0.12] text-cyan-200',
|
||||
fixed: 'border-emerald-500/35 bg-emerald-500/[0.12] text-emerald-200',
|
||||
improved: 'border-accent-500/35 bg-accent-500/[0.12] text-accent-200',
|
||||
breaking: 'border-rose-500/35 bg-rose-500/[0.12] text-rose-200',
|
||||
beta: 'border-amber-500/35 bg-amber-500/[0.12] text-amber-200',
|
||||
} as const;
|
||||
---
|
||||
|
||||
<span class={`not-prose inline-flex items-center rounded-full border border-dashed px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.08em] ${styles[type]}`}>
|
||||
{label ?? type}
|
||||
</span>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
steps: string[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { steps = [], title } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 p-4 sm:p-5">
|
||||
{title && <p class="mb-3 font-mono text-[11px] uppercase tracking-[0.12em] text-accent-400">{title}</p>}
|
||||
<ol class="m-0 list-none space-y-3 p-0">
|
||||
{steps.map((step, index) => (
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-dashed border-accent-500/35 bg-accent-500/[0.1] font-mono text-[11px] font-semibold text-accent-200">
|
||||
{index + 1}
|
||||
</span>
|
||||
<p class="m-0 pt-0.5 text-sm leading-relaxed text-surface-200">{step}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { title = 'Terminal' } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="not-prose my-6 overflow-hidden rounded-xl border border-dashed border-surface-700/70 bg-surface-900/60">
|
||||
<div class="flex items-center justify-between border-b border-dashed border-surface-700/60 px-4 py-2.5">
|
||||
<div class="flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-red-500/70"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-yellow-500/70"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-green-500/70"></span>
|
||||
</div>
|
||||
<span class="font-mono text-[11px] uppercase tracking-[0.12em] text-surface-500">{title}</span>
|
||||
</div>
|
||||
<pre class="m-0 overflow-x-auto p-4"><code class="font-mono text-sm text-accent-200"><slot /></code></pre>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@ const blog = defineCollection({
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
author: z.string().default('4gray'),
|
||||
featured: z.boolean().default(false),
|
||||
heroImage: z.string().optional(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
draft: z.boolean().default(false),
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
title: Blog Components Demo (MDX)
|
||||
description: Reusable MDX components for IPTVnator blog posts including alerts, CTA buttons, and terminal command blocks.
|
||||
pubDate: 2026-02-15
|
||||
author: 4gray
|
||||
tags:
|
||||
- blog
|
||||
- mdx
|
||||
- docs
|
||||
draft: true
|
||||
---
|
||||
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import PostButton from '../../components/blog/PostButton.astro';
|
||||
import TerminalBlock from '../../components/blog/TerminalBlock.astro';
|
||||
import StepRail from '../../components/blog/StepRail.astro';
|
||||
import LinkCards from '../../components/blog/LinkCards.astro';
|
||||
import IconFeatureGrid from '../../components/blog/IconFeatureGrid.astro';
|
||||
import ReleaseMeta from '../../components/blog/ReleaseMeta.astro';
|
||||
import StatusPill from '../../components/blog/StatusPill.astro';
|
||||
import ChangeTable from '../../components/blog/ChangeTable.astro';
|
||||
import CopyCommand from '../../components/blog/CopyCommand.astro';
|
||||
import ImageGallery from '../../components/blog/ImageGallery.astro';
|
||||
import Spoiler from '../../components/blog/Spoiler.astro';
|
||||
import FaqAccordion from '../../components/blog/FaqAccordion.astro';
|
||||
|
||||
This post demonstrates reusable UI components you can use in `.mdx` blog posts.
|
||||
|
||||
## Alert panel
|
||||
|
||||
<Alert type="info" title="Heads up">
|
||||
Use alerts to highlight important notes without breaking the reading flow.
|
||||
</Alert>
|
||||
|
||||
<Alert type="warning" title="Before updating">
|
||||
Back up playlists, credentials, and portal URLs before installing major updates.
|
||||
</Alert>
|
||||
|
||||
## Release meta
|
||||
|
||||
<ReleaseMeta version="v0.18.0" releaseDate="January 14, 2026" channels={['Desktop', 'PWA']} />
|
||||
|
||||
## Status pills
|
||||
|
||||
<div class="not-prose my-4 flex flex-wrap gap-2">
|
||||
<StatusPill type="new" />
|
||||
<StatusPill type="fixed" />
|
||||
<StatusPill type="improved" />
|
||||
<StatusPill type="beta" />
|
||||
<StatusPill type="breaking" />
|
||||
</div>
|
||||
|
||||
## Terminal code block
|
||||
|
||||
<TerminalBlock title="Terminal">
|
||||
$ xattr -c /Applications/IPTVnator.app
|
||||
</TerminalBlock>
|
||||
|
||||
## Copy command
|
||||
|
||||
<CopyCommand title="One-line fix" command="xattr -c /Applications/IPTVnator.app" />
|
||||
|
||||
## Button / CTA
|
||||
|
||||
<PostButton
|
||||
href="https://github.com/4gray/iptvnator/releases/latest"
|
||||
label="Download latest release"
|
||||
/>
|
||||
|
||||
<PostButton
|
||||
href="/iptvnator/blog/v0-18-release-notes/"
|
||||
label="Read v0.18 release notes"
|
||||
external={false}
|
||||
/>
|
||||
|
||||
## Step rail
|
||||
|
||||
<StepRail
|
||||
title="Quick setup"
|
||||
steps={[
|
||||
'Import an M3U, Xtream, or Stalker playlist.',
|
||||
'Configure EPG (XMLTV URL) from playlist settings.',
|
||||
'Mark channels as favorites for quick access.',
|
||||
]}
|
||||
/>
|
||||
|
||||
## Link cards
|
||||
|
||||
<LinkCards
|
||||
links={[
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/4gray/iptvnator',
|
||||
hint: 'Source code, issues, and releases.',
|
||||
icon: 'github',
|
||||
},
|
||||
{
|
||||
label: 'Telegram',
|
||||
href: 'https://t.me/iptvnator',
|
||||
hint: 'Announcements and community updates.',
|
||||
icon: 'telegram',
|
||||
},
|
||||
{
|
||||
label: 'GitHub Sponsors',
|
||||
href: 'https://github.com/sponsors/4gray',
|
||||
hint: 'Support long-term development.',
|
||||
icon: 'sponsor',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Icon feature grid
|
||||
|
||||
<IconFeatureGrid
|
||||
items={[
|
||||
{
|
||||
title: 'Playlist Support',
|
||||
text: 'Use M3U/M3U8, Xtream Codes API, and Stalker portals.',
|
||||
icon: 'playlist',
|
||||
},
|
||||
{
|
||||
title: 'Built-in EPG',
|
||||
text: 'Parse large XMLTV guides with improved loading behavior.',
|
||||
icon: 'epg',
|
||||
},
|
||||
{
|
||||
title: 'Performance',
|
||||
text: 'Background workers keep UI responsive during heavy parsing.',
|
||||
icon: 'speed',
|
||||
},
|
||||
{
|
||||
title: 'Privacy-first',
|
||||
text: 'No tracking pipeline and local-first playback workflow.',
|
||||
icon: 'shield',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Change table
|
||||
|
||||
<ChangeTable
|
||||
rows={[
|
||||
{
|
||||
area: 'General',
|
||||
change: 'Playlist switcher moved to a cleaner unified UI.',
|
||||
impact: 'Faster navigation',
|
||||
},
|
||||
{
|
||||
area: 'M3U',
|
||||
change: 'EPG parsing improved for large XMLTV files.',
|
||||
impact: 'More reliable loading',
|
||||
},
|
||||
{
|
||||
area: 'Xtream',
|
||||
change: 'Category visibility controls and item counters.',
|
||||
impact: 'Better content management',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Image gallery
|
||||
|
||||
<ImageGallery
|
||||
columns={2}
|
||||
images={[
|
||||
{
|
||||
src: '/iptvnator/blog/iptvnator-v18.jpeg',
|
||||
alt: 'IPTVnator v0.18 UI screenshot',
|
||||
caption: 'v0.18 interface update',
|
||||
},
|
||||
{
|
||||
src: '/iptvnator/blog/macos-app-damaged.jpg',
|
||||
alt: 'macOS app damaged warning dialog',
|
||||
caption: 'macOS Gatekeeper warning example',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Spoiler block
|
||||
|
||||
<Spoiler title="Show advanced troubleshooting note">
|
||||
If launch issues persist, verify the app path and run `xattr -c` on the exact `.app` bundle location.
|
||||
</Spoiler>
|
||||
|
||||
## FAQ accordion
|
||||
|
||||
<FaqAccordion
|
||||
items={[
|
||||
{
|
||||
q: 'Can I use these components in regular .md files?',
|
||||
a: 'No. Use .mdx when you want to render custom Astro components inside post content.',
|
||||
},
|
||||
{
|
||||
q: 'Will draft posts appear on production blog pages?',
|
||||
a: 'No. Posts with draft: true are excluded from listing and route generation.',
|
||||
},
|
||||
{
|
||||
q: 'Can I mix markdown with components in one post?',
|
||||
a: 'Yes. MDX supports regular markdown syntax and imported components together.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## When to use MDX
|
||||
|
||||
- Use `.md` for plain posts
|
||||
- Use `.mdx` when you want custom components, richer callouts, or advanced layout blocks
|
||||
+13
-3
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Understanding the Electronic Program Guide
|
||||
description: Learn how IPTVnator uses XMLTV data to display current and upcoming programs alongside your live TV channels.
|
||||
pubDate: 2026-02-15
|
||||
pubDate: 2026-02-14
|
||||
author: 4gray
|
||||
heroImage: /iptvnator/blog/epg.png
|
||||
tags:
|
||||
@@ -10,12 +10,19 @@ tags:
|
||||
draft: false
|
||||
---
|
||||
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import PostButton from '../../components/blog/PostButton.astro';
|
||||
|
||||
One of the most useful features in IPTVnator is the built-in Electronic Program Guide (EPG). Here is how it works and how to set it up.
|
||||
|
||||
## What is EPG?
|
||||
|
||||
The Electronic Program Guide displays a schedule of current and upcoming TV programs for your channels. IPTVnator supports EPG data in the **XMLTV format**, which is the standard used by most IPTV providers.
|
||||
|
||||
<Alert type="info" title="Format tip">
|
||||
Most providers expose EPG as `.xml` or compressed `.xml.gz` files.
|
||||
</Alert>
|
||||
|
||||
## Setting up EPG
|
||||
|
||||
To enable EPG in IPTVnator:
|
||||
@@ -41,8 +48,11 @@ IPTVnator matches EPG data to channels using the `tvg-id` attribute from your M3
|
||||
|
||||
## Tips
|
||||
|
||||
- EPG files can be large — IPTVnator caches parsed data in the database (Electron)
|
||||
- EPG files can be large — IPTVnator caches parsed data in the database (Electron)
|
||||
- You can add multiple EPG sources per playlist
|
||||
- The EPG automatically refreshes periodically to keep the schedule up to date
|
||||
|
||||
Check the [GitHub repository](https://github.com/4gray/iptvnator) for more information on supported EPG formats.
|
||||
<PostButton
|
||||
href="https://github.com/4gray/iptvnator"
|
||||
label="See supported formats on GitHub"
|
||||
/>
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: macOS ARM Users - Fix for "App is Damaged" Error
|
||||
description: If IPTVnator shows "app is damaged and can't be opened" on Apple Silicon Macs, run one Terminal command to remove quarantine and launch safely.
|
||||
pubDate: 2026-02-15
|
||||
author: 4gray
|
||||
heroImage: /iptvnator/blog/macos-app-damaged.jpg
|
||||
tags:
|
||||
- macos
|
||||
- troubleshooting
|
||||
- installation
|
||||
draft: false
|
||||
---
|
||||
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import PostButton from '../../components/blog/PostButton.astro';
|
||||
import TerminalBlock from '../../components/blog/TerminalBlock.astro';
|
||||
|
||||
If you are on macOS ARM (Apple Silicon) and IPTVnator shows **"app is damaged and can't be opened"**, use this quick fix.
|
||||
|
||||
## TL;DR
|
||||
|
||||
<TerminalBlock title="Terminal">
|
||||
$ xattr -c /Applications/IPTVnator.app
|
||||
</TerminalBlock>
|
||||
|
||||
## Why this happens
|
||||
|
||||
IPTVnator is currently not code-signed for macOS. After download, Gatekeeper adds a quarantine attribute, and macOS can block launch with a generic "damaged" message.
|
||||
|
||||
To distribute a fully signed and notarized macOS app, Apple requires an active Apple Developer subscription (currently **$99/year**). As an open-source project, IPTVnator does not currently use that signing pipeline.
|
||||
|
||||
## What the command does
|
||||
|
||||
The command above removes the quarantine attribute from the app bundle:
|
||||
|
||||
- It does **not** modify IPTVnator binaries
|
||||
- It tells your Mac to trust the app you intentionally downloaded
|
||||
- It is the standard local workaround for unsigned apps
|
||||
|
||||
<Alert type="warning" title="Safety note">
|
||||
Only run this command for apps you trust and downloaded from a trusted source.
|
||||
</Alert>
|
||||
|
||||
<PostButton
|
||||
href="https://github.com/4gray/iptvnator/releases/latest"
|
||||
label="Download from official GitHub releases"
|
||||
/>
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: v0.18 - Release Notes
|
||||
description: IPTVnator v0.18 brings a new playlist switcher UI, large EPG improvements, Xtream and Stalker updates, and multiple UX fixes across the app.
|
||||
featured: true
|
||||
pubDate: 2026-01-14
|
||||
author: 4gray
|
||||
heroImage: /iptvnator/blog/iptvnator-v18.jpeg
|
||||
tags:
|
||||
- release
|
||||
- release-notes
|
||||
- v0.18
|
||||
draft: false
|
||||
---
|
||||
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import LinkCards from '../../components/blog/LinkCards.astro';
|
||||
import ReleaseMeta from '../../components/blog/ReleaseMeta.astro';
|
||||
import StatusPill from '../../components/blog/StatusPill.astro';
|
||||
import ChangeTable from '../../components/blog/ChangeTable.astro';
|
||||
|
||||
IPTVnator **v0.18** is out.
|
||||
|
||||
<ReleaseMeta version="v0.18.0" releaseDate="January 14, 2026" channels={['Desktop', 'PWA']} />
|
||||
|
||||
## Support the project
|
||||
|
||||
- **GitHub Sponsors**: [https://github.com/sponsors/4gray](https://github.com/sponsors/4gray)
|
||||
- **Buy me a coffee**: [https://ko-fi.com/4gray](https://ko-fi.com/4gray)
|
||||
- **USDT (TRC20)**: `TLz5rTS7QTxYGfRH1Pm6Hs9fewqstDM47c`
|
||||
|
||||
<LinkCards
|
||||
links={[
|
||||
{
|
||||
label: 'GitHub Sponsors',
|
||||
href: 'https://github.com/sponsors/4gray',
|
||||
hint: 'Support continued development.',
|
||||
icon: 'sponsor',
|
||||
},
|
||||
{
|
||||
label: 'Ko-fi',
|
||||
href: 'https://ko-fi.com/4gray',
|
||||
hint: 'One-time support for the project.',
|
||||
icon: 'kofi',
|
||||
},
|
||||
{
|
||||
label: 'Telegram',
|
||||
href: 'https://t.me/iptvnator',
|
||||
hint: 'Follow announcements and updates.',
|
||||
icon: 'telegram',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Summary
|
||||
|
||||
<ChangeTable
|
||||
rows={[
|
||||
{
|
||||
area: 'General',
|
||||
change: 'Unified playlist switcher and cleaner header controls.',
|
||||
impact: 'Faster day-to-day navigation',
|
||||
},
|
||||
{
|
||||
area: 'M3U',
|
||||
change: 'Large EPG handling, progress popup, and restored quality selector.',
|
||||
impact: 'More stable playback setup',
|
||||
},
|
||||
{
|
||||
area: 'Xtream',
|
||||
change: 'Category visibility controls and recently-added improvements.',
|
||||
impact: 'Better catalog browsing',
|
||||
},
|
||||
{
|
||||
area: 'Stalker',
|
||||
change: 'Initial support for playlists requiring device IDs/signatures.',
|
||||
impact: 'Broader provider compatibility',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## General
|
||||
|
||||
<StatusPill type="improved" />
|
||||
|
||||
- New playlist switcher UI: a unified and cleaner way to switch between playlists
|
||||
- Global favorites moved to the header for quicker access
|
||||
- Sort and filter options merged into a single header menu
|
||||
|
||||
## M3U
|
||||
|
||||
<StatusPill type="improved" />
|
||||
|
||||
- Improved EPG download and parsing to better handle large EPG files
|
||||
- Added a popup dialog showing EPG download progress
|
||||
- Restored quality selector for the Video.js player
|
||||
|
||||
## Xtream & Stalker
|
||||
|
||||
<StatusPill type="fixed" />
|
||||
|
||||
- Improved look and feel of the details view for VODs and TV shows
|
||||
- Fixed opening favorite items with embedded players
|
||||
|
||||
## Xtream
|
||||
|
||||
<StatusPill type="new" />
|
||||
|
||||
- Category management: hide/show categories
|
||||
- Display item counts per category
|
||||
- New separate view for recently added items
|
||||
- Recently added items are shown when no category is selected
|
||||
- Fixed empty categories issue
|
||||
- Restored import progress counter
|
||||
|
||||
## Stalker
|
||||
|
||||
<StatusPill type="beta" label="partial support" />
|
||||
|
||||
- Added support for playlists that require device IDs and signatures
|
||||
- Some playlists are still not supported; more testing data is needed
|
||||
|
||||
<Alert type="warning" title="Before updating">
|
||||
Please back up your playlists, credentials, URLs, and other important data.
|
||||
</Alert>
|
||||
|
||||
## Download
|
||||
|
||||
<LinkCards
|
||||
links={[
|
||||
{
|
||||
label: 'Download v0.18.0',
|
||||
href: 'https://github.com/4gray/iptvnator/releases/tag/v0.18.0',
|
||||
hint: 'Official release binaries and checksums.',
|
||||
icon: 'download',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
+14
-2
@@ -9,7 +9,14 @@ tags:
|
||||
draft: false
|
||||
---
|
||||
|
||||
We are excited to launch the official IPTVnator website and developer blog!
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import PostButton from '../../components/blog/PostButton.astro';
|
||||
|
||||
We are excited to launch the official IPTVnator website and developer blog.
|
||||
|
||||
<Alert type="success" title="What this means">
|
||||
You now have one place for releases, tutorials, troubleshooting guides, and project updates.
|
||||
</Alert>
|
||||
|
||||
## What is IPTVnator?
|
||||
|
||||
@@ -26,4 +33,9 @@ This blog will be the home for:
|
||||
|
||||
## Get involved
|
||||
|
||||
IPTVnator is open source and we welcome contributions! Check out our [GitHub repository](https://github.com/4gray/iptvnator) to get started.
|
||||
IPTVnator is open source and we welcome contributions.
|
||||
|
||||
<PostButton
|
||||
href="https://github.com/4gray/iptvnator"
|
||||
label="View project on GitHub"
|
||||
/>
|
||||
@@ -3,9 +3,10 @@ import { getCollection, render } from 'astro:content';
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
const posts = await getCollection('blog');
|
||||
return posts
|
||||
.filter((post) => !post.data.draft)
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
|
||||
@@ -4,11 +4,14 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FeaturedBlogPost from '../../components/FeaturedBlogPost.astro';
|
||||
import BlogPostCard from '../../components/BlogPostCard.astro';
|
||||
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((post) => !post.data.draft)
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const [featuredPost, ...remainingPosts] = posts;
|
||||
const featuredPost = posts.find((post) => post.data.featured) ?? posts[0];
|
||||
const remainingPosts = featuredPost ? posts.filter((post) => post.id !== featuredPost.id) : [];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
|
||||
@@ -9,12 +9,15 @@ import SupportSection from '../components/SupportSection.astro';
|
||||
import FeaturedBlogPost from '../components/FeaturedBlogPost.astro';
|
||||
import BlogPostCard from '../components/BlogPostCard.astro';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
|
||||
.slice(0, 3);
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
|
||||
const [featuredPost, ...otherPosts] = posts;
|
||||
const allPosts = (await getCollection('blog'))
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const featuredPost = allPosts.find((post) => post.data.featured) ?? allPosts[0];
|
||||
const otherPosts = featuredPost ? allPosts.filter((post) => post.id !== featuredPost.id).slice(0, 2) : [];
|
||||
const posts = featuredPost ? [featuredPost, ...otherPosts] : allPosts.slice(0, 3);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
|
||||
Reference in new issue
Block a user