diff --git a/apps/docs/content/docs/api/page.mdx b/apps/docs/content/docs/api/page.mdx index c888169..4ffa6f5 100644 --- a/apps/docs/content/docs/api/page.mdx +++ b/apps/docs/content/docs/api/page.mdx @@ -3,7 +3,7 @@ title: Page description: These are APIs that can be used to fetch page data --- -### Get the latest post +### Get latest post This endpoint will return a JSON response of the latest post in the page. @@ -70,3 +70,25 @@ GET https://yourpage.changes.page/changes.json } ] ``` + +### Get a post by id + +This endpoint will return a JSON response of a post by id. + +```text title="Request" +GET https://yourpage.changes.page/changes.json/:id +``` + +```json title="Response: 200 OK" +{ + "id": "cf8c52a9-27b3-47df-9f22-0c0af15cfaca", + "title": "Unlock Teamwork with Our Latest Feature Update", + "content": "We're kicking off 2025 by introducing one of our most highly requested features!", + "tags": ["announcement", "new"], + "publication_date": "2025-02-14T12:22:16.571+00:00", + "updated_at": "2025-02-14T12:22:17.887817+00:00", + "created_at": "2025-02-14T09:42:52.934045+00:00", + "url": "https://hey.changes.page/post/cf8c52a9-27b3-47df-9f22-0c0af15cfaca/unlock-teamwork-with-our-latest-feature-update", + "plain_text_content": "We're kicking off 2025 by introducing one of our most highly requested features!" +} +``` diff --git a/apps/page/next.config.js b/apps/page/next.config.js index 62bd3ad..d631183 100644 --- a/apps/page/next.config.js +++ b/apps/page/next.config.js @@ -87,6 +87,10 @@ const moduleExports = { source: "/changes.json", destination: "/api/json", }, + { + source: "/changes.json/:id", + destination: "/api/post/:id", + }, { source: "/latest.json", destination: "/api/latest", diff --git a/apps/page/pages/api/json.ts b/apps/page/pages/api/json.ts index 2798912..c318ff1 100644 --- a/apps/page/pages/api/json.ts +++ b/apps/page/pages/api/json.ts @@ -1,12 +1,12 @@ -import type { NextApiRequest, NextApiResponse } from "next"; import { IPost } from "@changes-page/supabase/types/page"; +import { convertMarkdownToPlainText } from "@changes-page/utils"; +import type { NextApiRequest, NextApiResponse } from "next"; import { allowCors } from "../../lib/cors"; import { fetchPosts, fetchRenderData, translateHostToPageIdentifier, } from "../../lib/data"; -import { convertMarkdownToPlainText } from "@changes-page/utils"; import { getPageUrl, getPostUrl } from "../../lib/url"; async function handler( @@ -43,7 +43,7 @@ async function handler( res.status(200).json(postsWithUrl); } catch (e: Error | any) { - console.log("changes.md [Error]", e); + console.log("Failed to fetch posts [Error]", e); res.status(200).json([]); } } diff --git a/apps/page/pages/api/latest.ts b/apps/page/pages/api/latest.ts index 2a79d12..2fc2111 100644 --- a/apps/page/pages/api/latest.ts +++ b/apps/page/pages/api/latest.ts @@ -59,7 +59,7 @@ async function handler( res.status(200).json(postsWithUrl[0] ?? null); } catch (e: Error | any) { - console.log("changes.md [Error]", e); + console.log("Failed to fetch latest post [Error]", e); res.status(404).json(null); } } diff --git a/apps/page/pages/api/markdown.ts b/apps/page/pages/api/markdown.ts index 992332b..9a75b7a 100644 --- a/apps/page/pages/api/markdown.ts +++ b/apps/page/pages/api/markdown.ts @@ -40,7 +40,7 @@ ${post.content} res.status(200).send(markdown); } catch (e: Error | any) { - console.log("changes.md [Error]", e); + console.log("Failed to fetch posts [changes.md] [Error]", e); res.status(200).send("## No posts Found"); } } diff --git a/apps/page/pages/api/pinned.ts b/apps/page/pages/api/pinned.ts index 120efbc..08aca95 100644 --- a/apps/page/pages/api/pinned.ts +++ b/apps/page/pages/api/pinned.ts @@ -60,7 +60,7 @@ async function handler( res.status(200).json(postsWithUrl[0]); } catch (e: Error | any) { - console.log("changes.md [Error]", e); + console.log("Failed to fetch pinned post [Error]", e); res.status(404).json(null); } } diff --git a/apps/page/pages/api/post/[id].ts b/apps/page/pages/api/post/[id].ts new file mode 100644 index 0000000..de7681b --- /dev/null +++ b/apps/page/pages/api/post/[id].ts @@ -0,0 +1,76 @@ +import { supabaseAdmin } from "@changes-page/supabase/admin"; +import { IPost } from "@changes-page/supabase/types/page"; +import { convertMarkdownToPlainText } from "@changes-page/utils"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { allowCors } from "../../../lib/cors"; +import { + fetchRenderData, + translateHostToPageIdentifier, +} from "../../../lib/data"; +import { getPageUrl, getPostUrl } from "../../../lib/url"; + +type IPostWithUrl = Pick< + IPost, + "id" | "title" | "content" | "tags" | "created_at" +> & { url: string; plain_text_content: string }; + +async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { id } = req.query; + if (!id) { + res.status(404).json(null); + return; + } + + await allowCors(req, res); + + const hostname = String(req?.headers?.host); + const { domain, page: url_slug } = translateHostToPageIdentifier(hostname); + + try { + const { page, settings } = await fetchRenderData( + String(domain || url_slug) + ); + + if (!page) throw new Error("Page not found"); + if (!settings) throw new Error("Settings not found"); + + const pageUrl = getPageUrl(page, settings, hostname); + + const { data, error: postsError } = await supabaseAdmin + .from("posts") + .select("id,title,content,tags,publication_date,updated_at,created_at") + .eq("id", String(id)) + .eq("page_id", String(page?.id)) + .eq("status", "published") + .order("publication_date", { ascending: false }) + .limit(1); + + if (postsError) { + console.error("Fetch post error", postsError); + throw new Error("Failed to fetch posts"); + } + + const posts = data as Array; + + if (!posts?.length) { + res.status(404).json(null); + return; + } + + const post = posts[0]; + + res.status(200).json({ + ...post, + url: getPostUrl(pageUrl, post), + plain_text_content: convertMarkdownToPlainText(post.content), + }); + } catch (e: Error | any) { + console.log("Failed to fetch post [Error]", e); + res.status(404).json(null); + } +} + +export default handler;