25
site/src/components/BlogPostCard.astro
Normal file
25
site/src/components/BlogPostCard.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import type { WordpressPost } from "../lib/content/types";
|
||||
|
||||
type Props = {
|
||||
post: WordpressPost;
|
||||
};
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
function truncate(s: string, n: number) {
|
||||
if (!s) return "";
|
||||
const t = s.trim();
|
||||
if (t.length <= n) return t;
|
||||
return `${t.slice(0, Math.max(0, n - 1)).trimEnd()}…`;
|
||||
}
|
||||
---
|
||||
|
||||
<a class="blog-card" href={`/blog/post/${post.slug}`}>
|
||||
{post.featuredImageUrl ? <img src={post.featuredImageUrl} alt="" loading="lazy" /> : null}
|
||||
<div class="blog-card-body">
|
||||
<h3 class="blog-card-title">{post.title}</h3>
|
||||
<p class="blog-card-excerpt">{truncate(post.excerpt || "", 180)}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
25
site/src/components/BlogSecondaryNav.astro
Normal file
25
site/src/components/BlogSecondaryNav.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import type { WordpressCategory } from "../lib/content/types";
|
||||
|
||||
type Props = {
|
||||
categories: WordpressCategory[];
|
||||
activeCategorySlug?: string;
|
||||
};
|
||||
|
||||
const { categories, activeCategorySlug } = Astro.props;
|
||||
---
|
||||
|
||||
<nav class="subnav" aria-label="Blog categories">
|
||||
<a class={!activeCategorySlug ? "active" : ""} href="/blog">
|
||||
All
|
||||
</a>
|
||||
<a class={activeCategorySlug === "__pages" ? "active" : ""} href="/blog/pages">
|
||||
Pages
|
||||
</a>
|
||||
{categories.map((c) => (
|
||||
<a class={activeCategorySlug === c.slug ? "active" : ""} href={`/blog/category/${c.slug}`}>
|
||||
{c.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -79,6 +79,15 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
||||
>
|
||||
Podcast
|
||||
</a>
|
||||
<a
|
||||
href="/blog"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="nav.blog"
|
||||
data-umami-event-placement="nav"
|
||||
data-umami-event-target_url="/blog"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="/about"
|
||||
data-umami-event="click"
|
||||
|
||||
22
site/src/layouts/BlogLayout.astro
Normal file
22
site/src/layouts/BlogLayout.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import BlogSecondaryNav from "../components/BlogSecondaryNav.astro";
|
||||
import type { WordpressCategory } from "../lib/content/types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalPath: string;
|
||||
ogImageUrl?: string;
|
||||
categories: WordpressCategory[];
|
||||
activeCategorySlug?: string;
|
||||
};
|
||||
|
||||
const { categories, activeCategorySlug, ...rest } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout {...rest}>
|
||||
<BlogSecondaryNav categories={categories} activeCategorySlug={activeCategorySlug} />
|
||||
<slot />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,6 +11,9 @@ type IngestConfig = {
|
||||
youtubeApiKey?: string;
|
||||
podcastRssUrl?: string;
|
||||
instagramPostUrlsFile: string;
|
||||
wordpressBaseUrl?: string;
|
||||
wordpressUsername?: string;
|
||||
wordpressAppPassword?: string;
|
||||
};
|
||||
|
||||
export function getPublicConfig(): PublicConfig {
|
||||
@@ -31,5 +34,8 @@ export function getIngestConfigFromEnv(env: NodeJS.ProcessEnv): IngestConfig {
|
||||
youtubeApiKey: env.YOUTUBE_API_KEY,
|
||||
podcastRssUrl: env.PODCAST_RSS_URL,
|
||||
instagramPostUrlsFile: env.INSTAGRAM_POST_URLS_FILE || "content/instagram-posts.json",
|
||||
wordpressBaseUrl: env.WORDPRESS_BASE_URL,
|
||||
wordpressUsername: env.WORDPRESS_USERNAME,
|
||||
wordpressAppPassword: env.WORDPRESS_APP_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import path from "node:path";
|
||||
|
||||
import type { ContentCache } from "./types";
|
||||
|
||||
const DEFAULT_CACHE: ContentCache = { generatedAt: new Date(0).toISOString(), items: [] };
|
||||
const DEFAULT_CACHE: ContentCache = {
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
items: [],
|
||||
wordpress: { posts: [], pages: [], categories: [] },
|
||||
};
|
||||
|
||||
function getCachePath() {
|
||||
// Read from the repo-local content cache (populated by scripts/fetch-content.ts).
|
||||
@@ -18,6 +22,10 @@ export async function readContentCache(): Promise<ContentCache> {
|
||||
if (!parsed || !Array.isArray(parsed.items) || typeof parsed.generatedAt !== "string") {
|
||||
return DEFAULT_CACHE;
|
||||
}
|
||||
// Ensure new optional fields exist (keeps callers simple).
|
||||
if (!parsed.wordpress) {
|
||||
parsed.wordpress = { posts: [], pages: [], categories: [] };
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
// Cache missing is normal for first run.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { ContentCache, ContentItem, ContentSource } from "./types";
|
||||
import type {
|
||||
ContentCache,
|
||||
ContentItem,
|
||||
ContentSource,
|
||||
WordpressCategory,
|
||||
WordpressPage,
|
||||
WordpressPost,
|
||||
} from "./types";
|
||||
|
||||
export function newestItems(
|
||||
cache: ContentCache,
|
||||
@@ -43,3 +50,33 @@ export function highPerformingYoutubeVideos(
|
||||
const ranked = [...videos].sort((a, b) => (b.metrics?.views || 0) - (a.metrics?.views || 0));
|
||||
return ranked.slice(0, Math.max(0, limit));
|
||||
}
|
||||
|
||||
export function wordpressPosts(cache: ContentCache): WordpressPost[] {
|
||||
const posts = cache.wordpress?.posts || [];
|
||||
return [...posts].sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
|
||||
}
|
||||
|
||||
export function wordpressPages(cache: ContentCache): WordpressPage[] {
|
||||
const pages = cache.wordpress?.pages || [];
|
||||
return [...pages].sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
|
||||
}
|
||||
|
||||
export function wordpressCategories(cache: ContentCache): WordpressCategory[] {
|
||||
const cats = cache.wordpress?.categories || [];
|
||||
return [...cats].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export function wordpressPostBySlug(cache: ContentCache, slug: string): WordpressPost | undefined {
|
||||
return (cache.wordpress?.posts || []).find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export function wordpressPageBySlug(cache: ContentCache, slug: string): WordpressPage | undefined {
|
||||
return (cache.wordpress?.pages || []).find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export function wordpressPostsByCategorySlug(cache: ContentCache, categorySlug: string): WordpressPost[] {
|
||||
const cats = cache.wordpress?.categories || [];
|
||||
const category = cats.find((c) => c.slug === categorySlug);
|
||||
if (!category) return [];
|
||||
return wordpressPosts(cache).filter((p) => p.categoryIds.includes(category.id));
|
||||
}
|
||||
|
||||
@@ -17,4 +17,41 @@ export type ContentItem = {
|
||||
export type ContentCache = {
|
||||
generatedAt: string; // ISO-8601
|
||||
items: ContentItem[];
|
||||
wordpress?: WordpressContent;
|
||||
};
|
||||
|
||||
export type WordpressKind = "post" | "page";
|
||||
|
||||
export type WordpressCategory = {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type WordpressBase = {
|
||||
id: string;
|
||||
kind: WordpressKind;
|
||||
slug: string;
|
||||
url: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
contentHtml: string;
|
||||
featuredImageUrl?: string;
|
||||
publishedAt: string; // ISO-8601
|
||||
updatedAt: string; // ISO-8601
|
||||
};
|
||||
|
||||
export type WordpressPost = WordpressBase & {
|
||||
kind: "post";
|
||||
categoryIds: number[];
|
||||
};
|
||||
|
||||
export type WordpressPage = WordpressBase & {
|
||||
kind: "page";
|
||||
};
|
||||
|
||||
export type WordpressContent = {
|
||||
posts: WordpressPost[];
|
||||
pages: WordpressPage[];
|
||||
categories: WordpressCategory[];
|
||||
};
|
||||
|
||||
176
site/src/lib/ingest/wordpress.ts
Normal file
176
site/src/lib/ingest/wordpress.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { WordpressCategory, WordpressContent, WordpressPage, WordpressPost } from "../content/types";
|
||||
|
||||
type WordpressConfig = {
|
||||
baseUrl: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
};
|
||||
|
||||
type FetchResult<T> = {
|
||||
data: T;
|
||||
totalPages?: number;
|
||||
};
|
||||
|
||||
function stripHtml(input: string): string {
|
||||
return input.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function decodeEntities(input: string): string {
|
||||
// Keep it small: enough for common WP fields.
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function sanitizeHtml(input: string): string {
|
||||
// Remove script blocks entirely.
|
||||
return input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
||||
}
|
||||
|
||||
function getAuthHeaders(cfg: WordpressConfig): Record<string, string> {
|
||||
if (!cfg.username || !cfg.appPassword) return {};
|
||||
const token = Buffer.from(`${cfg.username}:${cfg.appPassword}`, "utf8").toString("base64");
|
||||
return { Authorization: `Basic ${token}` };
|
||||
}
|
||||
|
||||
async function wpFetchJson<T>(url: string, headers: Record<string, string>): Promise<FetchResult<T>> {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...headers,
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`WP request failed: ${res.status} ${res.statusText} (${url})`);
|
||||
}
|
||||
|
||||
const totalPagesRaw = res.headers.get("x-wp-totalpages");
|
||||
const totalPages = totalPagesRaw ? Number(totalPagesRaw) : undefined;
|
||||
const data = (await res.json()) as T;
|
||||
return { data, totalPages };
|
||||
}
|
||||
|
||||
async function fetchAllPages<T>(
|
||||
urlForPage: (page: number) => string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<T[]> {
|
||||
const first = await wpFetchJson<T[]>(urlForPage(1), headers);
|
||||
const out = [...first.data];
|
||||
|
||||
const total = first.totalPages && Number.isFinite(first.totalPages) ? first.totalPages : 1;
|
||||
for (let p = 2; p <= total; p++) {
|
||||
const pageRes = await wpFetchJson<T[]>(urlForPage(p), headers);
|
||||
out.push(...pageRes.data);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function wpUrl(baseUrl: string, path: string): string {
|
||||
return `${baseUrl.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
function mapFeaturedImageUrl(wpItem: any): string | undefined {
|
||||
const embedded = wpItem?._embedded;
|
||||
const media = embedded?.["wp:featuredmedia"];
|
||||
const first = Array.isArray(media) ? media[0] : undefined;
|
||||
const src = first?.source_url;
|
||||
return typeof src === "string" && src.length > 0 ? src : undefined;
|
||||
}
|
||||
|
||||
function mapCategoryIds(wpItem: any): number[] {
|
||||
const ids = wpItem?.categories;
|
||||
return Array.isArray(ids) ? ids.filter((x) => typeof x === "number") : [];
|
||||
}
|
||||
|
||||
export function normalizeWordpressPost(raw: any): WordpressPost {
|
||||
const titleHtml = raw?.title?.rendered || "";
|
||||
const excerptHtml = raw?.excerpt?.rendered || "";
|
||||
const contentHtml = raw?.content?.rendered || "";
|
||||
|
||||
const title = decodeEntities(stripHtml(String(titleHtml)));
|
||||
const excerpt = decodeEntities(stripHtml(String(excerptHtml)));
|
||||
|
||||
const date = typeof raw?.date_gmt === "string" && raw.date_gmt ? raw.date_gmt : raw?.date;
|
||||
const modified =
|
||||
typeof raw?.modified_gmt === "string" && raw.modified_gmt ? raw.modified_gmt : raw?.modified;
|
||||
|
||||
return {
|
||||
id: String(raw?.id ?? raw?.slug ?? ""),
|
||||
kind: "post",
|
||||
slug: String(raw?.slug ?? ""),
|
||||
url: String(raw?.link ?? ""),
|
||||
title,
|
||||
excerpt,
|
||||
contentHtml: sanitizeHtml(String(contentHtml)),
|
||||
featuredImageUrl: mapFeaturedImageUrl(raw),
|
||||
publishedAt: new Date(date || 0).toISOString(),
|
||||
updatedAt: new Date(modified || date || 0).toISOString(),
|
||||
categoryIds: mapCategoryIds(raw),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWordpressPage(raw: any): WordpressPage {
|
||||
const titleHtml = raw?.title?.rendered || "";
|
||||
const excerptHtml = raw?.excerpt?.rendered || "";
|
||||
const contentHtml = raw?.content?.rendered || "";
|
||||
|
||||
const title = decodeEntities(stripHtml(String(titleHtml)));
|
||||
const excerpt = decodeEntities(stripHtml(String(excerptHtml)));
|
||||
|
||||
const date = typeof raw?.date_gmt === "string" && raw.date_gmt ? raw.date_gmt : raw?.date;
|
||||
const modified =
|
||||
typeof raw?.modified_gmt === "string" && raw.modified_gmt ? raw.modified_gmt : raw?.modified;
|
||||
|
||||
return {
|
||||
id: String(raw?.id ?? raw?.slug ?? ""),
|
||||
kind: "page",
|
||||
slug: String(raw?.slug ?? ""),
|
||||
url: String(raw?.link ?? ""),
|
||||
title,
|
||||
excerpt,
|
||||
contentHtml: sanitizeHtml(String(contentHtml)),
|
||||
featuredImageUrl: mapFeaturedImageUrl(raw),
|
||||
publishedAt: new Date(date || 0).toISOString(),
|
||||
updatedAt: new Date(modified || date || 0).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWordpressCategory(raw: any): WordpressCategory {
|
||||
return {
|
||||
id: typeof raw?.id === "number" ? raw.id : Number(raw?.id ?? 0),
|
||||
slug: String(raw?.slug ?? ""),
|
||||
name: decodeEntities(stripHtml(String(raw?.name ?? ""))),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchWordpressContent(cfg: WordpressConfig): Promise<WordpressContent> {
|
||||
const headers = getAuthHeaders(cfg);
|
||||
|
||||
// Default per_page max is often 100.
|
||||
const posts = await fetchAllPages<any>(
|
||||
(page) => wpUrl(cfg.baseUrl, `/wp-json/wp/v2/posts?per_page=100&page=${page}&_embed=1`),
|
||||
headers,
|
||||
);
|
||||
const pages = await fetchAllPages<any>(
|
||||
(page) => wpUrl(cfg.baseUrl, `/wp-json/wp/v2/pages?per_page=100&page=${page}&_embed=1`),
|
||||
headers,
|
||||
);
|
||||
const categories = await fetchAllPages<any>(
|
||||
(page) => wpUrl(cfg.baseUrl, `/wp-json/wp/v2/categories?per_page=100&page=${page}`),
|
||||
headers,
|
||||
);
|
||||
|
||||
return {
|
||||
posts: posts.map(normalizeWordpressPost).filter((p) => p.slug && p.title),
|
||||
pages: pages.map(normalizeWordpressPage).filter((p) => p.slug && p.title),
|
||||
categories: categories.map(normalizeWordpressCategory).filter((c) => c.slug && c.name),
|
||||
};
|
||||
}
|
||||
|
||||
54
site/src/pages/blog/category/[slug].astro
Normal file
54
site/src/pages/blog/category/[slug].astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BlogLayout from "../../../layouts/BlogLayout.astro";
|
||||
import BlogPostCard from "../../../components/BlogPostCard.astro";
|
||||
import { readContentCache } from "../../../lib/content/cache";
|
||||
import {
|
||||
wordpressCategories,
|
||||
wordpressPostsByCategorySlug,
|
||||
} from "../../../lib/content/selectors";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cache = await readContentCache();
|
||||
return wordpressCategories(cache).map((c) => ({
|
||||
params: { slug: c.slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const cache = await readContentCache();
|
||||
const categories = wordpressCategories(cache);
|
||||
|
||||
const slug = Astro.params.slug || "";
|
||||
const activeCategory = categories.find((c) => c.slug === slug);
|
||||
const posts = wordpressPostsByCategorySlug(cache, slug);
|
||||
|
||||
if (!activeCategory) {
|
||||
// If the category doesn't exist, there will be no static route generated.
|
||||
// But keep runtime behavior explicit.
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
---
|
||||
|
||||
<BlogLayout
|
||||
title={`${activeCategory.name} | Blog | SanthoshJ`}
|
||||
description={`Posts in category: ${activeCategory.name}`}
|
||||
canonicalPath={`/blog/category/${activeCategory.slug}`}
|
||||
categories={categories}
|
||||
activeCategorySlug={activeCategory.slug}
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>{activeCategory.name}</h2>
|
||||
<span class="muted">{posts.length} post{posts.length === 1 ? "" : "s"}</span>
|
||||
</div>
|
||||
|
||||
{posts.length > 0 ? (
|
||||
<div class="blog-grid">
|
||||
{posts.map((p) => (
|
||||
<BlogPostCard post={p} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">No posts in this category yet.</div>
|
||||
)}
|
||||
</section>
|
||||
</BlogLayout>
|
||||
54
site/src/pages/blog/index.astro
Normal file
54
site/src/pages/blog/index.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BlogLayout from "../../layouts/BlogLayout.astro";
|
||||
import BlogPostCard from "../../components/BlogPostCard.astro";
|
||||
import { readContentCache } from "../../lib/content/cache";
|
||||
import { wordpressCategories, wordpressPages, wordpressPosts } from "../../lib/content/selectors";
|
||||
|
||||
const cache = await readContentCache();
|
||||
const categories = wordpressCategories(cache);
|
||||
const posts = wordpressPosts(cache);
|
||||
const pages = wordpressPages(cache);
|
||||
---
|
||||
|
||||
<BlogLayout
|
||||
title="Blog | SanthoshJ"
|
||||
description="Latest posts from my WordPress blog."
|
||||
canonicalPath="/blog"
|
||||
categories={categories}
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Latest posts</h2>
|
||||
<span class="muted">{posts.length} post{posts.length === 1 ? "" : "s"}</span>
|
||||
</div>
|
||||
|
||||
{posts.length > 0 ? (
|
||||
<div class="blog-grid">
|
||||
{posts.map((p) => (
|
||||
<BlogPostCard post={p} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">No blog posts yet. Configure WordPress and run <code>npm run fetch-content</code>.</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{pages.length > 0 ? (
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Pages</h2>
|
||||
<a class="muted" href="/blog/pages">
|
||||
Browse pages →
|
||||
</a>
|
||||
</div>
|
||||
<div class="empty">
|
||||
{pages.slice(0, 6).map((p) => (
|
||||
<div>
|
||||
<a href={`/blog/page/${p.slug}`}>{p.title}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</BlogLayout>
|
||||
|
||||
49
site/src/pages/blog/page/[slug].astro
Normal file
49
site/src/pages/blog/page/[slug].astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import BlogLayout from "../../../layouts/BlogLayout.astro";
|
||||
import { readContentCache } from "../../../lib/content/cache";
|
||||
import { wordpressCategories, wordpressPageBySlug, wordpressPages } from "../../../lib/content/selectors";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cache = await readContentCache();
|
||||
return wordpressPages(cache).map((page) => ({
|
||||
params: { slug: page.slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const cache = await readContentCache();
|
||||
const categories = wordpressCategories(cache);
|
||||
|
||||
const slug = Astro.params.slug || "";
|
||||
const page = wordpressPageBySlug(cache, slug);
|
||||
|
||||
if (!page) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const metaDescription = (page.excerpt || "").slice(0, 160);
|
||||
---
|
||||
|
||||
<BlogLayout
|
||||
title={`${page.title} | Blog | SanthoshJ`}
|
||||
description={metaDescription || "Blog page"}
|
||||
canonicalPath={`/blog/page/${page.slug}`}
|
||||
categories={categories}
|
||||
ogImageUrl={page.featuredImageUrl}
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 style="margin: 0;">{page.title}</h2>
|
||||
<a class="muted" href="/blog">Back →</a>
|
||||
</div>
|
||||
{page.featuredImageUrl ? (
|
||||
<img
|
||||
src={page.featuredImageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style="width: 100%; max-height: 420px; object-fit: cover; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.12);"
|
||||
/>
|
||||
) : null}
|
||||
<div class="prose" set:html={page.contentHtml} />
|
||||
</section>
|
||||
</BlogLayout>
|
||||
|
||||
37
site/src/pages/blog/pages.astro
Normal file
37
site/src/pages/blog/pages.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import BlogLayout from "../../layouts/BlogLayout.astro";
|
||||
import { readContentCache } from "../../lib/content/cache";
|
||||
import { wordpressCategories, wordpressPages } from "../../lib/content/selectors";
|
||||
|
||||
const cache = await readContentCache();
|
||||
const categories = wordpressCategories(cache);
|
||||
const pages = wordpressPages(cache);
|
||||
---
|
||||
|
||||
<BlogLayout
|
||||
title="Blog Pages | SanthoshJ"
|
||||
description="Pages from my WordPress site."
|
||||
canonicalPath="/blog/pages"
|
||||
categories={categories}
|
||||
activeCategorySlug="__pages"
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Pages</h2>
|
||||
<span class="muted">{pages.length} page{pages.length === 1 ? "" : "s"}</span>
|
||||
</div>
|
||||
|
||||
{pages.length > 0 ? (
|
||||
<div class="empty">
|
||||
{pages.map((p) => (
|
||||
<div style="padding: 6px 0;">
|
||||
<a href={`/blog/page/${p.slug}`}>{p.title}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">No pages yet.</div>
|
||||
)}
|
||||
</section>
|
||||
</BlogLayout>
|
||||
|
||||
52
site/src/pages/blog/post/[slug].astro
Normal file
52
site/src/pages/blog/post/[slug].astro
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
import BlogLayout from "../../../layouts/BlogLayout.astro";
|
||||
import { readContentCache } from "../../../lib/content/cache";
|
||||
import { wordpressCategories, wordpressPostBySlug, wordpressPosts } from "../../../lib/content/selectors";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cache = await readContentCache();
|
||||
return wordpressPosts(cache).map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const cache = await readContentCache();
|
||||
const categories = wordpressCategories(cache);
|
||||
|
||||
const slug = Astro.params.slug || "";
|
||||
const post = wordpressPostBySlug(cache, slug);
|
||||
|
||||
if (!post) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const metaDescription = (post.excerpt || "").slice(0, 160);
|
||||
---
|
||||
|
||||
<BlogLayout
|
||||
title={`${post.title} | Blog | SanthoshJ`}
|
||||
description={metaDescription || "Blog post"}
|
||||
canonicalPath={`/blog/post/${post.slug}`}
|
||||
categories={categories}
|
||||
ogImageUrl={post.featuredImageUrl}
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 style="margin: 0;">{post.title}</h2>
|
||||
<a class="muted" href="/blog">Back →</a>
|
||||
</div>
|
||||
<p class="muted" style="margin-top: 0;">
|
||||
{new Date(post.publishedAt).toLocaleDateString()}
|
||||
</p>
|
||||
{post.featuredImageUrl ? (
|
||||
<img
|
||||
src={post.featuredImageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
style="width: 100%; max-height: 420px; object-fit: cover; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.12);"
|
||||
/>
|
||||
) : null}
|
||||
<div class="prose" set:html={post.contentHtml} />
|
||||
</section>
|
||||
</BlogLayout>
|
||||
|
||||
Reference in New Issue
Block a user