diff --git a/src/markdoc/layouts/Post.svelte b/src/markdoc/layouts/Post.svelte index dcb56d928db..6b203410dce 100644 --- a/src/markdoc/layouts/Post.svelte +++ b/src/markdoc/layouts/Post.svelte @@ -43,6 +43,8 @@ export let callToAction: BlogCallToActionInput; export let lastUpdated: string; export let faqs: { question: string; answer: string }[] | undefined = undefined; + /** Frontmatter `draft: true`: the post stays reachable by its direct URL but is hidden from all listings/feeds and marked noindex. */ + export let draft = false; const posts = getContext('posts')?.filter( (post) => !(post.unlisted ?? false) && !(post.draft ?? false) @@ -133,6 +135,10 @@ + {#if draft} + + + {/if} {resolvedMetaTitle + TITLE_SUFFIX} diff --git a/src/routes/blog/+layout.ts b/src/routes/blog/+layout.ts index 80d57a6b898..ef71bc51742 100644 --- a/src/routes/blog/+layout.ts +++ b/src/routes/blog/+layout.ts @@ -1,8 +1,8 @@ -import { posts, authors, categories } from './content'; +import { publishedPosts, authors, categories } from './content'; export async function load({ data }) { return { - posts, + posts: publishedPosts, authors, categories, rawContent: data.rawContent diff --git a/src/routes/blog/content.ts b/src/routes/blog/content.ts index 1880a2fb944..70a73fb6c57 100644 --- a/src/routes/blog/content.ts +++ b/src/routes/blog/content.ts @@ -81,6 +81,11 @@ export const posts = Object.entries(postsGlob) return b.date.getTime() - a.date.getTime(); }); +// Posts visible to the public. Drafts are excluded everywhere except their own +// direct URL: the /blog/post/ route still renders a draft (and emits a +// noindex meta via Post.svelte), but it never appears in any listing or feed. +export const publishedPosts = posts.filter((post) => !post.draft); + export const authors = Object.values(authorsGlob).map((authorList) => { const { frontmatter } = authorList as { frontmatter: AuthorData; @@ -115,12 +120,14 @@ export const normalizeCategory = (str: string) => str?.replace(/\s+/g, '-').toLo export const getBlogEntries = () => { const filteredCategories = categories.filter((category) => - posts.some((post) => normalizeCategory(post.category) === normalizeCategory(category.name)) + publishedPosts.some( + (post) => normalizeCategory(post.category) === normalizeCategory(category.name) + ) ); return { authors, filteredCategories, - posts: posts.filter((post) => !post.unlisted) + posts: publishedPosts.filter((post) => !post.unlisted) }; }; diff --git a/src/routes/blog/feed.json/+server.ts b/src/routes/blog/feed.json/+server.ts index ea45f6ee037..e3d01942f52 100644 --- a/src/routes/blog/feed.json/+server.ts +++ b/src/routes/blog/feed.json/+server.ts @@ -1,12 +1,12 @@ import type { RequestHandler } from './$types'; -import { posts } from '../content'; +import { publishedPosts } from '../content'; import { json } from '@sveltejs/kit'; export const prerender = true; export const GET: RequestHandler = () => { return json({ - posts, - total: Object.keys(posts).length + posts: publishedPosts, + total: publishedPosts.length }); }; diff --git a/src/routes/blog/rss.xml/+server.ts b/src/routes/blog/rss.xml/+server.ts index cfc2cf1ecf8..f034aafef9c 100644 --- a/src/routes/blog/rss.xml/+server.ts +++ b/src/routes/blog/rss.xml/+server.ts @@ -1,5 +1,5 @@ import type { RequestHandler } from './$types'; -import { posts } from '../content'; +import { publishedPosts } from '../content'; export const prerender = true; @@ -26,7 +26,7 @@ export const GET: RequestHandler = () => { https://appwrite.io Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools. - ${posts + ${publishedPosts .map( (post) => ` ${encodeText(post.title)} diff --git a/src/routes/llms-full.txt/+server.ts b/src/routes/llms-full.txt/+server.ts index 9e7c33134a9..233460f6f9f 100644 --- a/src/routes/llms-full.txt/+server.ts +++ b/src/routes/llms-full.txt/+server.ts @@ -1,5 +1,6 @@ import type { RequestHandler } from '@sveltejs/kit'; import { SPECIAL_PAGES } from '../llms-config'; +import { publishedPosts } from '../blog/content'; export const prerender = true; @@ -9,6 +10,11 @@ const markdocAndMarkdownFiles = import.meta.glob('$routes/**/*.{markdoc,md}', { eager: true }); +// Published (non-draft) blog post slugs — the single source of truth for what is public. +const PUBLISHED_BLOG_SLUGS = new Set( + publishedPosts.map((p) => p.href.split('/blog/post/')[1]).filter(Boolean) +); + function stripRouteGroups(routePath: string): string { return routePath.replace(/\([^/]+\)\//g, ''); } @@ -128,6 +134,12 @@ ${page.fullContent} continue; } + // Skip draft blog posts: only include published ones (single source of truth) + if (href.startsWith('/blog/post')) { + const slug = href.split('/blog/post/')[1]?.replace(/\/+$/, ''); + if (!slug || !PUBLISHED_BLOG_SLUGS.has(slug)) continue; + } + const url = new URL(href, base).toString(); const fmOrH1 = extractTitle(raw); diff --git a/src/routes/llms.txt/+server.ts b/src/routes/llms.txt/+server.ts index fab781cbfe5..60b5b58f261 100644 --- a/src/routes/llms.txt/+server.ts +++ b/src/routes/llms.txt/+server.ts @@ -1,5 +1,6 @@ import type { RequestHandler } from '@sveltejs/kit'; import { SPECIAL_PAGES } from '../llms-config'; +import { publishedPosts } from '../blog/content'; export const prerender = true; @@ -10,6 +11,11 @@ const markdocAndMarkdownFiles = import.meta.glob('$routes/**/*.{markdoc,md}', { eager: true }); +// Published (non-draft) blog post slugs — the single source of truth for what is public. +const PUBLISHED_BLOG_SLUGS = new Set( + publishedPosts.map((p) => p.href.split('/blog/post/')[1]).filter(Boolean) +); + // Strip group directories like (marketing) from a route path function stripRouteGroups(routePath: string): string { return routePath.replace(/\([^/]+\)\//g, ''); @@ -148,6 +154,12 @@ export const GET: RequestHandler = ({ request }) => { continue; } + // Skip draft blog posts: only include published ones (single source of truth) + if (href.startsWith('/blog/post')) { + const slug = href.split('/blog/post/')[1]?.replace(/\/+$/, ''); + if (!slug || !PUBLISHED_BLOG_SLUGS.has(slug)) continue; + } + const url = new URL(href, base).toString(); const title = extractTitle(raw) ?? href.split('/').pop()!.replace(/[-_]/g, ' ');