diff --git a/examples/with-markdoc/.gitignore b/examples/with-markdoc/.gitignore
new file mode 100644
index 000000000000..6240da8b10bf
--- /dev/null
+++ b/examples/with-markdoc/.gitignore
@@ -0,0 +1,21 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/examples/with-markdoc/.vscode/extensions.json b/examples/with-markdoc/.vscode/extensions.json
new file mode 100644
index 000000000000..22a15055d638
--- /dev/null
+++ b/examples/with-markdoc/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+  "recommendations": ["astro-build.astro-vscode"],
+  "unwantedRecommendations": []
+}
diff --git a/examples/with-markdoc/.vscode/launch.json b/examples/with-markdoc/.vscode/launch.json
new file mode 100644
index 000000000000..d6422097621f
--- /dev/null
+++ b/examples/with-markdoc/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "command": "./node_modules/.bin/astro dev",
+      "name": "Development server",
+      "request": "launch",
+      "type": "node-terminal"
+    }
+  ]
+}
diff --git a/examples/with-markdoc/README.md b/examples/with-markdoc/README.md
new file mode 100644
index 000000000000..e14d3255bb80
--- /dev/null
+++ b/examples/with-markdoc/README.md
@@ -0,0 +1,46 @@
+# Astro Starter Kit: Minimal
+
+```
+npm create astro@latest -- --template minimal
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```
+/
+├── public/
+├── src/
+│   └── pages/
+│       └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command                | Action                                           |
+| :--------------------- | :----------------------------------------------- |
+| `npm install`          | Installs dependencies                            |
+| `npm run dev`          | Starts local dev server at `localhost:3000`      |
+| `npm run build`        | Build your production site to `./dist/`          |
+| `npm run preview`      | Preview your build locally, before deploying     |
+| `npm run astro ...`    | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro --help` | Get help using the Astro CLI                     |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/with-markdoc/astro.config.mjs b/examples/with-markdoc/astro.config.mjs
new file mode 100644
index 000000000000..29d846359bb2
--- /dev/null
+++ b/examples/with-markdoc/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import markdoc from '@astrojs/markdoc';
+
+// https://astro.build/config
+export default defineConfig({
+	integrations: [markdoc()],
+});
diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json
new file mode 100644
index 000000000000..e1daefcf61b9
--- /dev/null
+++ b/examples/with-markdoc/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "@example/with-markdoc",
+  "type": "module",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "dev": "astro dev",
+    "start": "astro dev",
+    "build": "astro build",
+    "preview": "astro preview",
+    "astro": "astro"
+  },
+  "dependencies": {
+    "@astrojs/markdoc": "^0.0.1",
+    "astro": "^2.0.6",
+    "html-escaper": "^3.0.3"
+  },
+  "devDependencies": {
+    "@markdoc/markdoc": "^0.2.2"
+  }
+}
diff --git a/examples/with-markdoc/public/favicon.svg b/examples/with-markdoc/public/favicon.svg
new file mode 100644
index 000000000000..0f3906297879
--- /dev/null
+++ b/examples/with-markdoc/public/favicon.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
+  <path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
+  <path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
+  <defs>
+    <linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#000"/>
+      <stop offset="1" stop-color="#000" stop-opacity="0"/>
+    </linearGradient>
+  </defs>
+	<style>
+    @media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
+  </style>
+</svg>
diff --git a/examples/with-markdoc/sandbox.config.json b/examples/with-markdoc/sandbox.config.json
new file mode 100644
index 000000000000..9178af77d7de
--- /dev/null
+++ b/examples/with-markdoc/sandbox.config.json
@@ -0,0 +1,11 @@
+{
+  "infiniteLoopProtection": true,
+  "hardReloadOnChange": false,
+  "view": "browser",
+  "template": "node",
+  "container": {
+    "port": 3000,
+    "startScript": "start",
+    "node": "14"
+  }
+}
diff --git a/examples/with-markdoc/src/components/RedP.astro b/examples/with-markdoc/src/components/RedP.astro
new file mode 100644
index 000000000000..1b4252688da2
--- /dev/null
+++ b/examples/with-markdoc/src/components/RedP.astro
@@ -0,0 +1,7 @@
+<p><slot /></p>
+
+<style>
+	p {
+		color: red;
+	}
+</style>
diff --git a/examples/with-markdoc/src/components/test.mdoc b/examples/with-markdoc/src/components/test.mdoc
new file mode 100644
index 000000000000..33ab897d26d3
--- /dev/null
+++ b/examples/with-markdoc/src/components/test.mdoc
@@ -0,0 +1,31 @@
+# Hey there
+
+This is a test file?
+
+{% table %}
+* Heading 1
+* Heading 2
+---
+* Row 1 Cell 1
+* Row 1 Cell 2
+---
+* Row 2 Cell 1
+* Row 2 cell 2
+{% /table %}
+
+{% if $shouldMarquee %}
+{% mq direction="right" %}
+Testing!
+{% /mq %}
+{% /if %}
+
+{% link href=$href %}Link{% /link %}
+
+Some `inline code` should help
+
+```js
+const testing = true;
+function further() {
+	console.log('still highlighted!')
+}
+```
diff --git a/examples/with-markdoc/src/env.d.ts b/examples/with-markdoc/src/env.d.ts
new file mode 100644
index 000000000000..acef35f175aa
--- /dev/null
+++ b/examples/with-markdoc/src/env.d.ts
@@ -0,0 +1,2 @@
+/// <reference path="../.astro/types.d.ts" />
+/// <reference types="astro/client" />
diff --git a/examples/with-markdoc/src/pages/index.astro b/examples/with-markdoc/src/pages/index.astro
new file mode 100644
index 000000000000..59a5ef7bc463
--- /dev/null
+++ b/examples/with-markdoc/src/pages/index.astro
@@ -0,0 +1,73 @@
+---
+import { body } from '../components/test.mdoc';
+import { Markdoc } from '@astrojs/markdoc';
+import RenderMarkdoc from '../renderer/RenderMarkdoc.astro';
+import RedP from '../components/RedP.astro';
+import { Code } from 'astro/components';
+import { Tag } from '@markdoc/markdoc';
+import { ComponentRenderer } from '../renderer/astroNode';
+
+const parsed = Markdoc.parse(body);
+const content = Markdoc.transform(parsed, {
+	variables: {
+		shouldMarquee: true,
+		href: 'https://astro.build',
+	},
+	tags: {
+		mq: {
+			render: 'marquee',
+			attributes: {
+				direction: {
+					type: String,
+					default: 'left',
+					matches: ['left', 'right', 'up', 'down'],
+					errorLevel: 'critical',
+				},
+			},
+		},
+		link: {
+			render: 'a',
+			attributes: {
+				href: {
+					type: String,
+					required: true,
+				},
+			},
+		},
+	},
+});
+
+const code: ComponentRenderer = {
+	component: Code,
+	props({ attributes, getTreeNode }) {
+		return {
+			...attributes,
+			lang: attributes.lang ?? attributes['data-language'],
+			code: attributes.code ?? Markdoc.renderers.html(getTreeNode().children),
+		};
+	},
+};
+---
+
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+		<meta name="viewport" content="width=device-width" />
+		<meta name="generator" content={Astro.generator} />
+		<title>Astro</title>
+	</head>
+	<body>
+		<h1>Astro</h1>
+		<article>
+			<RenderMarkdoc
+				content={content}
+				components={{
+					p: RedP,
+					code,
+					pre: code,
+				}}
+			/>
+		</article>
+	</body>
+</html>
diff --git a/examples/with-markdoc/src/renderer/RenderMarkdoc.astro b/examples/with-markdoc/src/renderer/RenderMarkdoc.astro
new file mode 100644
index 000000000000..00c602c7df72
--- /dev/null
+++ b/examples/with-markdoc/src/renderer/RenderMarkdoc.astro
@@ -0,0 +1,14 @@
+---
+import type { RenderableTreeNode } from '@markdoc/markdoc';
+import { ComponentRenderer, createAstroNode } from './astroNode';
+import RenderNode from './RenderNode.astro';
+
+type Props = {
+	content: RenderableTreeNode;
+	components: Record<string, ComponentRenderer>;
+};
+
+const { content, components } = Astro.props as Props;
+---
+
+<RenderNode node={createAstroNode(content, components)} />
diff --git a/examples/with-markdoc/src/renderer/RenderNode.astro b/examples/with-markdoc/src/renderer/RenderNode.astro
new file mode 100644
index 000000000000..c5f2eb07b490
--- /dev/null
+++ b/examples/with-markdoc/src/renderer/RenderNode.astro
@@ -0,0 +1,29 @@
+---
+import type { AstroNode } from './astroNode';
+
+type Props = {
+	node: AstroNode;
+};
+
+const Node = (Astro.props as Props).node;
+---
+
+{
+	typeof Node === 'string' ? (
+		<Fragment set:text={Node} />
+	) : 'component' in Node ? (
+		<Node.component {...Node.props}>
+			{Node.children.map((child) => (
+				<Astro.self node={child} />
+			))}
+		</Node.component>
+	) : (
+		<Fragment>
+			<Fragment set:html={`<${Node.tag}>`} />
+			{Node.children.map((child) => (
+				<Astro.self node={child} />
+			))}
+			<Fragment set:html={`</${Node.tag}>`} />
+		</Fragment>
+	)
+}
diff --git a/examples/with-markdoc/src/renderer/astroNode.ts b/examples/with-markdoc/src/renderer/astroNode.ts
new file mode 100644
index 000000000000..4d4acb817941
--- /dev/null
+++ b/examples/with-markdoc/src/renderer/astroNode.ts
@@ -0,0 +1,67 @@
+import { RenderableTreeNode, Tag, renderers, NodeType } from '@markdoc/markdoc';
+import { escape } from 'html-escaper';
+
+// TODO: expose `AstroComponentFactory` type from core
+type AstroComponentFactory = (props: Record<string, any>) => any & {
+	isAstroComponentFactory: true;
+};
+
+export type ComponentRenderer =
+	| AstroComponentFactory
+	| {
+			component: AstroComponentFactory;
+			props?(params: { attributes: Record<string, any>; getTreeNode(): Tag }): Record<string, any>;
+	  };
+
+export type AstroNode =
+	| string
+	| {
+			component: AstroComponentFactory;
+			props: Record<string, any>;
+			children: AstroNode[];
+	  }
+	| {
+			tag: string;
+			attributes: Record<string, any>;
+			children: AstroNode[];
+	  };
+
+export function createAstroNode(
+	node: RenderableTreeNode,
+	components: Record<string, ComponentRenderer> = {}
+): AstroNode {
+	if (typeof node === 'string' || typeof node === 'number') {
+		return escape(String(node));
+	} else if (node === null || typeof node !== 'object' || !Tag.isTag(node)) {
+		return '';
+	}
+
+	if (Object.hasOwn(components, node.name)) {
+		const componentRenderer = components[node.name];
+		const component =
+			'Component' in componentRenderer ? componentRenderer.component : componentRenderer;
+		const props =
+			'props' in componentRenderer
+				? componentRenderer.props({
+						attributes: node.attributes,
+						getTreeNode() {
+							return node;
+						},
+				  })
+				: node.attributes;
+
+		const children = node.children.map((child) => createAstroNode(child, components));
+
+		return {
+			component,
+			props,
+			children,
+		};
+	} else {
+		return {
+			tag: node.name,
+			attributes: node.attributes,
+			children: node.children.map((child) => createAstroNode(child, components)),
+		};
+	}
+}
diff --git a/examples/with-markdoc/tsconfig.json b/examples/with-markdoc/tsconfig.json
new file mode 100644
index 000000000000..d78f81ec4e8e
--- /dev/null
+++ b/examples/with-markdoc/tsconfig.json
@@ -0,0 +1,3 @@
+{
+  "extends": "astro/tsconfigs/base"
+}
diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json
index fafebee3af4d..70c475f5350a 100644
--- a/packages/integrations/markdoc/package.json
+++ b/packages/integrations/markdoc/package.json
@@ -30,6 +30,7 @@
     "test:match": "mocha --timeout 20000 -g"
   },
   "dependencies": {
+    "@markdoc/markdoc": "^0.2.2"
   },
   "devDependencies": {
     "@types/chai": "^4.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cb389bc0d708..a5a1cf845257 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -285,6 +285,19 @@ importers:
       unocss: 0.15.6
       vite-imagetools: 4.0.18
 
+  examples/with-markdoc:
+    specifiers:
+      '@astrojs/markdoc': ^0.0.1
+      '@markdoc/markdoc': ^0.2.2
+      astro: ^2.0.6
+      html-escaper: ^3.0.3
+    dependencies:
+      '@astrojs/markdoc': link:../../packages/integrations/markdoc
+      astro: link:../../packages/astro
+      html-escaper: 3.0.3
+    devDependencies:
+      '@markdoc/markdoc': 0.2.2
+
   examples/with-markdown-plugins:
     specifiers:
       '@astrojs/markdown-remark': ^2.0.1
@@ -2886,6 +2899,7 @@ importers:
 
   packages/integrations/markdoc:
     specifiers:
+      '@markdoc/markdoc': ^0.2.2
       '@types/chai': ^4.3.1
       '@types/mocha': ^9.1.1
       astro: workspace:*
@@ -2894,6 +2908,8 @@ importers:
       linkedom: ^0.14.12
       mocha: ^9.2.2
       vite: ^4.0.3
+    dependencies:
+      '@markdoc/markdoc': 0.2.2
     devDependencies:
       '@types/chai': 4.3.4
       '@types/mocha': 9.1.1
@@ -6432,6 +6448,20 @@ packages:
       - supports-color
     dev: false
 
+  /@markdoc/markdoc/0.2.2:
+    resolution: {integrity: sha512-0TiD9jmA5h5znN4lxo7HECAu3WieU5g5vUsfByeucrdR/x88hEilpt16EydFyJwJddQ/3w5HQgW7Ovy62r4cyw==}
+    engines: {node: '>=14.7.0'}
+    peerDependencies:
+      '@types/react': '*'
+      react: '*'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      react:
+        optional: true
+    optionalDependencies:
+      '@types/markdown-it': 12.2.3
+
   /@mdx-js/mdx/2.3.0:
     resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
     dependencies:
@@ -7236,11 +7266,27 @@ packages:
   /@types/json5/0.0.30:
     resolution: {integrity: sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==}
 
+  /@types/linkify-it/3.0.2:
+    resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
+    optional: true
+
+  /@types/markdown-it/12.2.3:
+    resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
+    requiresBuild: true
+    dependencies:
+      '@types/linkify-it': 3.0.2
+      '@types/mdurl': 1.0.2
+    optional: true
+
   /@types/mdast/3.0.10:
     resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
     dependencies:
       '@types/unist': 2.0.6
 
+  /@types/mdurl/1.0.2:
+    resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
+    optional: true
+
   /@types/mdx/2.0.3:
     resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==}
     dev: false