diff --git a/dotcom-rendering/src/devServer/docs/home.tsx b/dotcom-rendering/src/devServer/docs/home.tsx
new file mode 100644
index 00000000000..d81f6aa4303
--- /dev/null
+++ b/dotcom-rendering/src/devServer/docs/home.tsx
@@ -0,0 +1,194 @@
+import {
+ articleBold17Object,
+ headlineBold20Object,
+ palette,
+ space,
+ until,
+} from '@guardian/source/foundations';
+import {
+ LinkButton,
+ SvgArrowRightStraight,
+ TextInput,
+} from '@guardian/source/react-components';
+import { formatToString } from '../../lib/articleFormat';
+import type { Article as ArticleModel } from '../../types/article';
+import type { Block } from '../../types/blocks';
+import type { FEElement } from '../../types/content';
+import type { RenderingTarget } from '../../types/renderingTarget';
+
+export const Home = ({ articles }: { articles: ArticleModel[] }) => (
+ <>
+
+ {articles.map((article) => (
+
+ ))}
+ >
+);
+
+const Form = () => (
+
+);
+
+const ArticleInfo = ({ article }: { article: ArticleModel }) => (
+
+
+ {article.frontendData.webTitle}
+
+
+
+ - Format
+ - {formatToString(article)}
+ - Main Media
+ - {mainMediaElement(article.frontendData.mainMediaElements)}
+ - Body
+ - {bodyElements(article.frontendData.blocks)}
+
+
+);
+
+const ArticleLinks = ({ url }: { url: URL }) => (
+
+
+
+
+
+);
+
+const mainMediaElement = (elements: FEElement[]): string => {
+ const element = elements[0];
+
+ return element !== undefined ? elementToString(element) : 'none';
+};
+
+const bodyElements = (blocks: Block[]): string =>
+ blocks
+ .flatMap((block) => block.elements)
+ .map(elementToString)
+ .reduce(mergeText, [])
+ .join(', ');
+
+const elementToString = (element: FEElement): string => {
+ const role = 'role' in element ? ` (${element.role})` : '';
+ const elem = element._type.split('.').at(-1)?.replace('BlockElement', '');
+
+ return `${elem}${role}`;
+};
+
+const mergeText = (elems: string[], elem: string) => {
+ if (elem === 'Text' && elems.at(-1) === 'Text') {
+ return elems;
+ }
+
+ return [...elems, elem];
+};
+
+const StageLinks = ({ stage, url }: { stage: Stage; url: URL }) => (
+
+ }
+ iconSide="right"
+ theme={{ backgroundPrimary: stageColour(stage) }}
+ size="small"
+ >
+ 📱 Apps {stage}
+
+ }
+ iconSide="right"
+ theme={{ backgroundPrimary: stageColour(stage) }}
+ size="small"
+ >
+ 🌍 Dotcom {stage}
+
+
+);
+
+type Stage = 'DEV' | 'CODE' | 'PROD';
+
+const buildUrl = (target: RenderingTarget, stage: Stage, url: URL) => {
+ switch (stage) {
+ case 'PROD': {
+ const builtUrl = new URL(url.pathname, prodOrigin);
+ builtUrl.search = params(target).toString();
+
+ return builtUrl;
+ }
+ case 'CODE': {
+ const builtUrl = new URL(url.pathname, codeOrigin);
+ builtUrl.search = params(target).toString();
+
+ return builtUrl;
+ }
+ case 'DEV':
+ return new URL(`${devPath(target)}/${url.toString()}`, devOrigin);
+ }
+};
+
+const stageTextColour = (stage: Stage) => {
+ switch (stage) {
+ case 'DEV':
+ return palette.neutral[7];
+ case 'CODE':
+ case 'PROD':
+ return palette.neutral[100];
+ }
+};
+
+const stageColour = (stage: Stage) => {
+ switch (stage) {
+ case 'DEV':
+ return palette.brandAlt[400];
+ case 'CODE':
+ return palette.success[400];
+ case 'PROD':
+ return palette.brand[400];
+ }
+};
+
+const devOrigin = 'http://localhost:3030';
+const codeOrigin = 'https://m.code.dev-theguardian.com';
+const prodOrigin = 'https://www.theguardian.com';
+
+const params = (target: RenderingTarget): URLSearchParams =>
+ new URLSearchParams(target === 'Apps' ? { dcr: 'apps' } : undefined);
+
+const devPath = (target: RenderingTarget): string => {
+ switch (target) {
+ case 'Web':
+ return '/Article';
+ case 'Apps':
+ return '/AppsArticle';
+ }
+};
diff --git a/dotcom-rendering/src/devServer/routers/home.tsx b/dotcom-rendering/src/devServer/routers/home.tsx
new file mode 100644
index 00000000000..72ae2273cc1
--- /dev/null
+++ b/dotcom-rendering/src/devServer/routers/home.tsx
@@ -0,0 +1,78 @@
+import { type Request, Router } from 'express';
+import { error, ok, type Result } from '../../lib/result';
+import { validateAsFEArticle } from '../../model/validate';
+import { type Article, enhanceArticleType } from '../../types/article';
+import { Home } from '../docs/home';
+import { sendReact } from '../send';
+
+export const home = Router();
+
+home.get('/', async (req, res, next) => {
+ const dotcomURL = parseUrl(req);
+
+ if (dotcomURL.kind === 'ok') {
+ const article = await extractArticle(dotcomURL.value);
+
+ return sendReact('Home', )(req, res, next);
+ } else if (dotcomURL.error.kind === 'noParameter') {
+ return sendReact('Home', )(req, res, next);
+ } else {
+ console.error(dotcomURL.error.message);
+ res.sendStatus(400);
+ }
+});
+
+const parseUrl = (req: Request): Result => {
+ const param = req.query.dotcomURL;
+
+ if (param === undefined) {
+ return error({ kind: 'noParameter' });
+ }
+
+ if (typeof param !== 'string') {
+ return error({
+ kind: 'parseError',
+ message:
+ "'dotcomURL' query parameter must contain a single string.",
+ });
+ }
+
+ try {
+ const url = new URL(param);
+
+ if (url.host !== 'www.theguardian.com' || url.protocol !== 'https:') {
+ return error({
+ kind: 'parseError',
+ message:
+ "'dotcomURL' must start with 'https://www.theguardian.com'",
+ });
+ }
+
+ return ok(url);
+ } catch (_e) {
+ return error({
+ kind: 'parseError',
+ message: `Could not parse the 'dotcomURL' query parameter. Received: ${param}, expected: a valid URL string.`,
+ });
+ }
+};
+
+type UrlError =
+ | {
+ kind: 'noParameter';
+ }
+ | {
+ kind: 'parseError';
+ message: string;
+ };
+
+const extractArticle = async (dotcomURL: URL): Promise => {
+ const url = new URL(dotcomURL);
+ url.pathname = `${url.pathname}.json`;
+ url.searchParams.append('dcr', 'true');
+
+ const json = await fetch(url).then((res) => res.json());
+ const frontendData = validateAsFEArticle(json);
+
+ return enhanceArticleType(frontendData, 'Web');
+};
diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts
index 134f284341e..d28f5d12569 100644
--- a/dotcom-rendering/src/server/server.dev.ts
+++ b/dotcom-rendering/src/server/server.dev.ts
@@ -1,4 +1,5 @@
import { type Handler, Router } from 'express';
+import { home } from '../devServer/routers/home';
import { pages } from '../devServer/routers/pages';
import { targets } from '../devServer/routers/targets';
import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web';
@@ -125,6 +126,7 @@ renderer.post('/CricketMatchPage', handleCricketMatchPage);
renderer.post('/FootballMatchSummaryPage', handleFootballMatchPage);
const router = Router();
+router.use('/home', home);
router.use('/pages', pages);
router.use('/targets', targets);
router.use(renderer);