diff --git a/astro.config.ts b/astro.config.ts
index 8b43468653a27ff..f5ad0ed7468b01c 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -90,15 +90,6 @@ export default defineConfig({
src: "./src/assets/logo.svg",
},
favicon: "/favicon.png",
- head: ["image", "og:image", "twitter:image"].map((name) => {
- return {
- tag: "meta",
- attrs: {
- name,
- content: "https://developers.cloudflare.com/cf-twitter-card.png",
- },
- };
- }),
social: {
github: "https://github.com/cloudflare/cloudflare-docs",
"x.com": "https://x.com/cloudflare",
diff --git a/public/__redirects b/public/__redirects
index 898b7819f75eae6..efdfe5eb78fa49b 100644
--- a/public/__redirects
+++ b/public/__redirects
@@ -1737,6 +1737,7 @@
/cloudflare-one/tutorials/zsh-env-var/ /cloudflare-one/tutorials/cli/ 301
### DYNAMIC REDIRECTS ###
+/*/index.html.md /:splat/index.md 301
/api-next/* /api/:splat 301
/changelog-next/* /changelog/:splat 301
/browser-rendering/quick-actions-rest-api/* /browser-rendering/rest-api/:splat 301
diff --git a/public/_headers b/public/_headers
index fbd8ac72f61fcf2..dd41c248045ba35 100644
--- a/public/_headers
+++ b/public/_headers
@@ -3,3 +3,9 @@
/_astro/*
Cache-Control: public, max-age=604800, immutable
+
+/*/llms-full.txt:
+ Content-Type: text/markdown; charset=utf-8
+
+/*/index.md:
+ Content-Type: text/markdown; charset=utf-8
\ No newline at end of file
diff --git a/src/components/overrides/Head.astro b/src/components/overrides/Head.astro
index ce02b16a69df1be..4e86d946e38a8ca 100644
--- a/src/components/overrides/Head.astro
+++ b/src/components/overrides/Head.astro
@@ -150,6 +150,16 @@ const ogImageUrl = new URL(ogImagePath, Astro.url.origin).toString();
});
});
+head.push({
+ tag: "link",
+ attrs: {
+ rel: "alternate",
+ type: "text/markdown",
+ href: Astro.url.pathname + "index.md",
+ },
+ content: "",
+});
+
metaTags.map((attrs) => {
head.push({
tag: "meta",
diff --git a/src/content/docs/style-guide/fixtures/index.mdx b/src/content/docs/style-guide/fixtures/index.mdx
new file mode 100644
index 000000000000000..e21a2330e93f1da
--- /dev/null
+++ b/src/content/docs/style-guide/fixtures/index.mdx
@@ -0,0 +1,8 @@
+---
+title: Fixtures
+noindex: true
+sidebar:
+ hidden: true
+---
+
+This folder stores test fixtures to be used in CI.
\ No newline at end of file
diff --git a/src/content/docs/style-guide/fixtures/markdown.mdx b/src/content/docs/style-guide/fixtures/markdown.mdx
new file mode 100644
index 000000000000000..0befccb7f1e18be
--- /dev/null
+++ b/src/content/docs/style-guide/fixtures/markdown.mdx
@@ -0,0 +1,23 @@
+---
+title: Markdown
+noindex: true
+sidebar:
+ hidden: true
+---
+
+import { Tabs, TabItem } from "~/components";
+
+The HTML generated by this file is used as a test fixture for our Markdown generation.
+
+
+
+ ```mdx
+ test
+ ```
+
+
+ ```md
+ test
+ ```
+
+
\ No newline at end of file
diff --git a/src/pages/[...entry]/index.html.md.ts b/src/pages/[...entry]/index.html.md.ts
deleted file mode 100644
index f89b70f375c3560..000000000000000
--- a/src/pages/[...entry]/index.html.md.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { APIRoute } from "astro";
-import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
-
-import { getCollection } from "astro:content";
-
-export const getStaticPaths = (async () => {
- const entries = await getCollection("docs", (e) => Boolean(e.body));
-
- return entries.map((entry) => {
- return {
- params: {
- // https://llmstxt.org/: (URLs without file names should append index.html.md instead.)
- entry: entry.id,
- },
- props: {
- entry,
- },
- };
- });
-}) satisfies GetStaticPaths;
-
-type Props = InferGetStaticPropsType;
-
-export const GET: APIRoute = (context) => {
- return new Response(context.props.entry.body, {
- headers: {
- "content-type": "text/markdown",
- },
- });
-};
diff --git a/src/pages/cloudflare-one/[...entry]/index.md.ts b/src/pages/cloudflare-one/[...entry]/index.md.ts
deleted file mode 100644
index 5751d9b6342bfbd..000000000000000
--- a/src/pages/cloudflare-one/[...entry]/index.md.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { APIRoute } from "astro";
-import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
-
-import { getCollection } from "astro:content";
-import { entryToString } from "~/util/container";
-
-import { process } from "~/util/rehype";
-import rehypeParse from "rehype-parse";
-import rehypeBaseUrl from "~/plugins/rehype/base-url";
-import rehypeFilterElements from "~/plugins/rehype/filter-elements";
-import remarkGfm from "remark-gfm";
-import rehypeRemark from "rehype-remark";
-import remarkStringify from "remark-stringify";
-
-export const getStaticPaths = (async () => {
- const entries = await getCollection("docs", (e) => {
- return e.id.startsWith("cloudflare-one") && Boolean(e.body);
- });
-
- return entries.map((entry) => {
- return {
- params: {
- entry: entry.id.replace("cloudflare-one/", ""),
- },
- props: {
- entry,
- },
- };
- });
-}) satisfies GetStaticPaths;
-
-type Props = InferGetStaticPropsType;
-
-export const GET: APIRoute = async (context) => {
- const html = await entryToString(context.props.entry, context.locals);
-
- const md = await process(html, [
- rehypeParse,
- rehypeBaseUrl,
- rehypeFilterElements,
- remarkGfm,
- rehypeRemark,
- remarkStringify,
- ]);
-
- return new Response(md, {
- headers: {
- "content-type": "text/markdown",
- },
- });
-};
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
index ad9c1e3df0ea0b6..726b50a6bc52aea 100644
--- a/vitest.workspace.ts
+++ b/vitest.workspace.ts
@@ -8,6 +8,14 @@ const workspace = defineWorkspace([
test: {
name: "Workers",
include: ["**/*.worker.test.ts"],
+ deps: {
+ optimizer: {
+ ssr: {
+ enabled: true,
+ include: ["node-html-parser"],
+ },
+ },
+ },
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
diff --git a/worker/index.ts b/worker/index.ts
index cb39efd7e085bd3..93689f223ede13e 100644
--- a/worker/index.ts
+++ b/worker/index.ts
@@ -2,6 +2,16 @@ import { WorkerEntrypoint } from "cloudflare:workers";
import { generateRedirectsEvaluator } from "redirects-in-workers";
import redirectsFileContents from "../dist/__redirects";
+import { parse } from "node-html-parser";
+import { process } from "../src/util/rehype";
+
+import rehypeParse from "rehype-parse";
+import rehypeBaseUrl from "../src/plugins/rehype/base-url";
+import rehypeFilterElements from "../src/plugins/rehype/filter-elements";
+import remarkGfm from "remark-gfm";
+import rehypeRemark from "rehype-remark";
+import remarkStringify from "remark-stringify";
+
const redirectsEvaluator = generateRedirectsEvaluator(redirectsFileContents, {
maxLineLength: 10_000, // Usually 2_000
maxStaticRules: 10_000, // Usually 2_000
@@ -10,6 +20,45 @@ const redirectsEvaluator = generateRedirectsEvaluator(redirectsFileContents, {
export default class extends WorkerEntrypoint {
override async fetch(request: Request) {
+ if (request.url.endsWith("/index.md")) {
+ const res = await this.env.ASSETS.fetch(
+ request.url.replace("index.md", ""),
+ request,
+ );
+
+ if (res.status === 404) {
+ return res;
+ }
+
+ if (
+ res.status === 200 &&
+ res.headers.get("content-type")?.startsWith("text/html")
+ ) {
+ const html = await res.text();
+
+ const content = parse(html).querySelector(".sl-markdown-content");
+
+ if (!content) {
+ return new Response("Not Found", { status: 404 });
+ }
+
+ const markdown = await process(content.toString(), [
+ rehypeParse,
+ rehypeBaseUrl,
+ rehypeFilterElements,
+ remarkGfm,
+ rehypeRemark,
+ remarkStringify,
+ ]);
+
+ return new Response(markdown, {
+ headers: {
+ "content-type": "text/markdown; charset=utf-8",
+ },
+ });
+ }
+ }
+
try {
try {
const redirect = await redirectsEvaluator(request, this.env.ASSETS);
diff --git a/worker/index.worker.test.ts b/worker/index.worker.test.ts
index f57473337dc1b56..e91868d570a2769 100644
--- a/worker/index.worker.test.ts
+++ b/worker/index.worker.test.ts
@@ -63,6 +63,14 @@ describe("Cloudflare Docs", () => {
expect(response.status).toBe(301);
expect(response.headers.get("Location")).toBe("/changelog/rss.xml");
});
+
+ it("redirects /workers/index.html.md to /workers/index.md", async () => {
+ const request = new Request("http://fakehost/workers/index.html.md");
+ const response = await SELF.fetch(request, { redirect: "manual" });
+
+ expect(response.status).toBe(301);
+ expect(response.headers.get("Location")).toBe("/workers/index.md");
+ });
});
describe("json endpoints", () => {
@@ -247,4 +255,40 @@ describe("Cloudflare Docs", () => {
expect(text).toContain('from "~/components"');
});
});
+
+ describe("index.md handling", () => {
+ it("style-guide fixture", async () => {
+ const request = new Request(
+ "http://fakehost/style-guide/fixtures/markdown/index.md",
+ );
+ const response = await SELF.fetch(request);
+
+ expect(response.status).toBe(200);
+
+ const text = await response.text();
+ expect(text).toMatchInlineSnapshot(`
+ "The HTML generated by this file is used as a test fixture for our Markdown generation.
+
+ * mdx
+
+ \`\`\`mdx
+ test
+ \`\`\`
+
+ * md
+
+ \`\`\`md
+ test
+ \`\`\`
+ "
+ `);
+ });
+
+ it("responds with 404.html at `/non-existent/index.md`", async () => {
+ const request = new Request("http://fakehost/non-existent/index.md");
+ const response = await SELF.fetch(request);
+ expect(response.status).toBe(404);
+ expect(await response.text()).toContain("Page not found.");
+ });
+ });
});
diff --git a/wrangler.toml b/wrangler.toml
index 66f66a0fa85a6da..8f06f88cc4437e8 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -6,7 +6,7 @@ compatibility_flags = ["nodejs_compat"]
main = "./worker/index.ts"
workers_dev = true
-route = { pattern = "developers.cloudflare.com/*", zone_name = "developers.cloudflare.com"}
+route = { pattern = "developers.cloudflare.com/*", zone_name = "developers.cloudflare.com" }
rules = [
{ type = "Text", globs = ["**/__redirects"], fallthrough = true },
@@ -16,4 +16,4 @@ rules = [
directory = "./dist"
binding = "ASSETS"
not_found_handling = "404-page"
-run_worker_first = true
+run_worker_first = true
\ No newline at end of file