mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
fix(web): CI E2E — resilient blog/RSS without Supabase + update stale test
The dynamic /blog/rss.xml returned 500 in CI (no Supabase env) and the E2E still asserted the old static feed's hand-written items. - /blog, /blog/[slug], and /blog/rss.xml now degrade gracefully (empty feed/ list, HTTP 200) when Supabase is unavailable, instead of throwing. - E2E: assert the always-present channel <title>LogicSRC Blog</title> and a looser xml content-type, dropping the removed static post titles. Verified with `next dev` and no Supabase env (CI conditions): rss/blog/sitemap all return 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
74abf8985f
commit
e47616bf9d
4 changed files with 42 additions and 26 deletions
|
|
@ -61,8 +61,9 @@ test.describe("LogicSRC PWA", () => {
|
||||||
await expect(sitemap.text()).resolves.toContain("https://logicsrc.com/hire-us");
|
await expect(sitemap.text()).resolves.toContain("https://logicsrc.com/hire-us");
|
||||||
|
|
||||||
expect(rss.status()).toBe(200);
|
expect(rss.status()).toBe(200);
|
||||||
expect(rss.headers()["content-type"]).toMatch(/(?:application|text)\/xml/);
|
expect(rss.headers()["content-type"]).toMatch(/xml/);
|
||||||
await expect(rss.text()).resolves.toContain("LogicSRC OpenSpec Compatibility");
|
// Feed is generated from the blog_posts table; the channel header is always
|
||||||
await expect(rss.text()).resolves.toContain("LogicSRC Credential Sharing OpenSpec");
|
// present even when there are no posts (e.g. CI without Supabase).
|
||||||
|
await expect(rss.text()).resolves.toContain("<title>LogicSRC Blog</title>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,18 @@ type PostRow = {
|
||||||
const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
||||||
|
|
||||||
async function loadPost(slug: string): Promise<PostRow | null> {
|
async function loadPost(slug: string): Promise<PostRow | null> {
|
||||||
const supabase = publicClient();
|
try {
|
||||||
const { data } = await supabase
|
const supabase = publicClient();
|
||||||
.from("blog_posts")
|
const { data } = await supabase
|
||||||
.select("slug, title, excerpt, html, featured_image, published_at, updated_at")
|
.from("blog_posts")
|
||||||
.eq("slug", slug)
|
.select("slug, title, excerpt, html, featured_image, published_at, updated_at")
|
||||||
.eq("status", "published")
|
.eq("slug", slug)
|
||||||
.maybeSingle();
|
.eq("status", "published")
|
||||||
return (data as PostRow | null) ?? null;
|
.maybeSingle();
|
||||||
|
return (data as PostRow | null) ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,18 @@ function formatDate(value: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function BlogIndex(): Promise<ReactNode> {
|
export default async function BlogIndex(): Promise<ReactNode> {
|
||||||
const supabase = publicClient();
|
let posts: PostRow[] = [];
|
||||||
const { data } = await supabase
|
try {
|
||||||
.from("blog_posts")
|
const supabase = publicClient();
|
||||||
.select("slug, title, excerpt, featured_image, published_at")
|
const { data } = await supabase
|
||||||
.eq("status", "published")
|
.from("blog_posts")
|
||||||
.order("published_at", { ascending: false });
|
.select("slug, title, excerpt, featured_image, published_at")
|
||||||
const posts = (data ?? []) as PostRow[];
|
.eq("status", "published")
|
||||||
|
.order("published_at", { ascending: false });
|
||||||
|
posts = (data ?? []) as PostRow[];
|
||||||
|
} catch {
|
||||||
|
posts = [];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteShell active="Blog">
|
<SiteShell active="Blog">
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,22 @@ function escapeXml(value: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /blog/rss.xml — RSS 2.0 feed generated from published blog_posts.
|
// GET /blog/rss.xml — RSS 2.0 feed generated from published blog_posts.
|
||||||
|
// Degrades to an empty (but valid) feed when Supabase is unavailable.
|
||||||
export async function GET(): Promise<Response> {
|
export async function GET(): Promise<Response> {
|
||||||
const base = baseUrl();
|
const base = baseUrl();
|
||||||
const supabase = publicClient();
|
let posts: PostRow[] = [];
|
||||||
const { data } = await supabase
|
try {
|
||||||
.from("blog_posts")
|
const supabase = publicClient();
|
||||||
.select("slug, title, excerpt, published_at")
|
const { data } = await supabase
|
||||||
.eq("status", "published")
|
.from("blog_posts")
|
||||||
.order("published_at", { ascending: false })
|
.select("slug, title, excerpt, published_at")
|
||||||
.limit(50);
|
.eq("status", "published")
|
||||||
const posts = (data ?? []) as PostRow[];
|
.order("published_at", { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
posts = (data ?? []) as PostRow[];
|
||||||
|
} catch {
|
||||||
|
posts = [];
|
||||||
|
}
|
||||||
|
|
||||||
const items = posts
|
const items = posts
|
||||||
.map((post) => {
|
.map((post) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue