openvibers/openvibe.codes Commits
The front door names its Markdown twin, and the twin is the front door (SEO-1) (#265)
Changes
@@@ -4,12 +4,13 @@// twin cannot drift from its page. Anonymous only; a run's page is a person's run and has no twin.const { renderPage } = require('./render/layout');--function createAgentPages(ctx, publicViews) {++function createAgentPages(ctx, publicViews, { home = null } = {}) {const views = publicViews;const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);async function renderPublicPage(path) {-- if (path === '/') return twin(views.home(), path);++ // / is the C4 front door a visitor gets (server/http/c4.js visitorHome), not the old marketing home.++ if (path === '/') return twin((home && home()) || views.home(), path);if (path === '/start') return twin(views.start(), path);if (path === '/harnesses') return twin(views.harnesses(), path);if (path === '/improve') return twin(views.improve(), path);@@@ -259,10 +259,11 @@ async function createApp(opts = {}) {// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public// pages. The pages themselves keep their own routes — a signed-in visitor's account bar and forms are theirs// — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.++ let c4Routes = null; // created below, after the chat app's routes (the twin of / reads its front door)const agentLayer = express();require('openvibe-shared/agent-layer').mount(agentLayer, {descriptor: require('./agent-descriptor'),-- render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews).renderPublicPage,++ render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews, { home: () => c4Routes && c4Routes.visitorHome() }).renderPublicPage,});app.use((req, res, next) => {const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');@@@ -273,7 +274,8 @@ async function createApp(opts = {}) {app.use(require('./http/chat').createChatRoutes(ctx));app.use(require('./http/git').createGitRoutes(ctx)); // after the chat app: it renders in ctx.chatShell// C4 workspace pages sit before the legacy marketing pages and share the existing owner-scoped task store.-- app.use(require('./http/c4').createC4Routes(ctx));++ c4Routes = require('./http/c4').createC4Routes(ctx);++ app.use(c4Routes);app.use(pageRoutes);app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));@@@ -379,7 +379,7 @@ ${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>${signedIn && !mine.ok && mine.problem ? note(`Your machines could not be read, so the machine picker has only Auto. ${esc(mine.problem.detail || '')}`, 'bad') : ''}${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h2><div class="c4-grid">${[['Any agent', 'Bring a key or plan. Auto picks a good fit and can hand over when one gets stuck.'], ['Your computer or ours', 'Pair your computer with one command. A hosted sandbox is coming soon.'], ['Proof you can inspect', 'See the changes, tests, screenshots and review before you approve.'], ['A cost you can follow', 'Track task costs and plan limits. Set caps that stop with a wrap-up.']].map(([a, b]) => `<article><h3>${a}</h3><p>${b}</p></article>`).join('')}</div></section>`}`;-- if (!signedIn) return page(req, res, null, wrap(visitorHome()), { index: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });++ if (!signedIn) return page(req, res, null, wrap(visitorHome()), { index: true, agent: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });page(req, res, null, wrap(content), {});});@@@ -795,6 +795,9 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-// module and borrow this one's kit (the page, the frame, the card, the pill, the flash and the same-origin check).r.use(require('./memory').createMemoryRoutes(ctx, { page, wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note }));++ // The front door a visitor gets at /, as a page for the layout: the Markdown twin of / is this page++ // (server/agent-pages.js), so the twin says what a visitor reads (SEO-1; it was the old marketing home).++ r.visitorHome = () => ({ css: ['css/c4.css'], scripts: ['js/c4.js'], body: raw(wrap(visitorHome())), index: true, agent: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });return r;}@@@ -32,6 +32,18 @@ const { boot, check, done } = require('./helpers/boot');assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server/http/c4.js'), 'utf8'), /if \(!signedIn\) return page\(req, res, null, wrap\(visitorHome\(\)\)/, 'the route sends a visitor to the front door');});++ await check('the front door names its Markdown twin, and the twin is the front door (SEO-1)', async () => {++ const home = await t.get('/');++ assert.match(home.text, /<link rel="alternate" type="text\/markdown" href="https:\/\/openvibe\.codes\/index\.md">/, 'the head names the twin');++ assert.match(String(home.headers.get('link') || ''), /<https:\/\/openvibe\.codes\/index\.md>; rel="alternate"; type="text\/markdown"/, 'so does the Link header');++ const twin = await t.get('/index.md');++ assert.strictEqual(twin.status, 200);++ assert.match(String(twin.headers.get('content-type') || ''), /^text\/markdown/);++ assert.ok(twin.text.includes('Every coding agent.') && twin.text.includes('Codex took over: Claude Code hit its plan limit'), 'the twin says what a visitor reads');++ const mine = await t.get('/', { as: ada });++ assert.ok(!/text\/markdown/.test(mine.text), 'a signed-in home names no twin');++ });++await check('task details are owner scoped and management pages honor sign-in', async () => {const settings = await t.get('/settings', { as: ada });assert.strictEqual(settings.status, 200);
@@ -4,12 +4,13 @@
@@ -4,12 +4,13 @@
// twin cannot drift from its page. Anonymous only; a run's page is a person's run and has no twin.
// twin cannot drift from its page. Anonymous only; a run's page is a person's run and has no twin.
const { renderPage } = require('./render/layout');
const { renderPage } = require('./render/layout');
-function createAgentPages(ctx, publicViews) {
+function createAgentPages(ctx, publicViews, { home = null } = {}) {
const views = publicViews;
const views = publicViews;
const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);
const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);
async function renderPublicPage(path) {
async function renderPublicPage(path) {
- if (path === '/') return twin(views.home(), path);
+ // / is the C4 front door a visitor gets (server/http/c4.js visitorHome), not the old marketing home.
+ if (path === '/') return twin((home && home()) || views.home(), path);
if (path === '/start') return twin(views.start(), path);
if (path === '/start') return twin(views.start(), path);
if (path === '/harnesses') return twin(views.harnesses(), path);
if (path === '/harnesses') return twin(views.harnesses(), path);
if (path === '/improve') return twin(views.improve(), path);
if (path === '/improve') return twin(views.improve(), path);
@@ -259,10 +259,11 @@ async function createApp(opts = {}) {
@@ -259,10 +259,11 @@ async function createApp(opts = {}) {
// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public
// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public
// pages. The pages themselves keep their own routes — a signed-in visitor's account bar and forms are theirs
// pages. The pages themselves keep their own routes — a signed-in visitor's account bar and forms are theirs
// — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.
// — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.
+ let c4Routes = null; // created below, after the chat app's routes (the twin of / reads its front door)
const agentLayer = express();
const agentLayer = express();
require('openvibe-shared/agent-layer').mount(agentLayer, {
require('openvibe-shared/agent-layer').mount(agentLayer, {
descriptor: require('./agent-descriptor'),
descriptor: require('./agent-descriptor'),
- render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews).renderPublicPage,
+ render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews, { home: () => c4Routes && c4Routes.visitorHome() }).renderPublicPage,
});
});
app.use((req, res, next) => {
app.use((req, res, next) => {
const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');
const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');
@@ -273,7 +274,8 @@ async function createApp(opts = {}) {
@@ -273,7 +274,8 @@ async function createApp(opts = {}) {
app.use(require('./http/chat').createChatRoutes(ctx));
app.use(require('./http/chat').createChatRoutes(ctx));
app.use(require('./http/git').createGitRoutes(ctx)); // after the chat app: it renders in ctx.chatShell
app.use(require('./http/git').createGitRoutes(ctx)); // after the chat app: it renders in ctx.chatShell
// C4 workspace pages sit before the legacy marketing pages and share the existing owner-scoped task store.
// C4 workspace pages sit before the legacy marketing pages and share the existing owner-scoped task store.
- app.use(require('./http/c4').createC4Routes(ctx));
+ c4Routes = require('./http/c4').createC4Routes(ctx);
+ app.use(c4Routes);
app.use(pageRoutes);
app.use(pageRoutes);
app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));
app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));
@@ -379,7 +379,7 @@ ${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head
@@ -379,7 +379,7 @@ ${signedIn ? `<section class="c4-card c4-conductor-cta"><div class="c4-card-head
<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>
<section class="c4-card"><div class="c4-card-head"><h2>Recent tasks</h2><a href="/tasks">View all</a></div>${rows.slice(0, 5).map(rowHtml).join('') || '<div class="c4-emptyline">No tasks yet. <a href="/#c4-prompt">Start a task</a></div>'}</section>
${signedIn && !mine.ok && mine.problem ? note(`Your machines could not be read, so the machine picker has only Auto. ${esc(mine.problem.detail || '')}`, 'bad') : ''}
${signedIn && !mine.ok && mine.problem ? note(`Your machines could not be read, so the machine picker has only Auto. ${esc(mine.problem.detail || '')}`, 'bad') : ''}
${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h2><div class="c4-grid">${[['Any agent', 'Bring a key or plan. Auto picks a good fit and can hand over when one gets stuck.'], ['Your computer or ours', 'Pair your computer with one command. A hosted sandbox is coming soon.'], ['Proof you can inspect', 'See the changes, tests, screenshots and review before you approve.'], ['A cost you can follow', 'Track task costs and plan limits. Set caps that stop with a wrap-up.']].map(([a, b]) => `<article><h3>${a}</h3><p>${b}</p></article>`).join('')}</div></section>`}`;
${signedIn ? '' : `<section class="c4-reasons"><h2>Built for the way you work</h2><div class="c4-grid">${[['Any agent', 'Bring a key or plan. Auto picks a good fit and can hand over when one gets stuck.'], ['Your computer or ours', 'Pair your computer with one command. A hosted sandbox is coming soon.'], ['Proof you can inspect', 'See the changes, tests, screenshots and review before you approve.'], ['A cost you can follow', 'Track task costs and plan limits. Set caps that stop with a wrap-up.']].map(([a, b]) => `<article><h3>${a}</h3><p>${b}</p></article>`).join('')}</div></section>`}`;
- if (!signedIn) return page(req, res, null, wrap(visitorHome()), { index: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });
+ if (!signedIn) return page(req, res, null, wrap(visitorHome()), { index: true, agent: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });
page(req, res, null, wrap(content), {});
page(req, res, null, wrap(content), {});
});
});
@@ -795,6 +795,9 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
@@ -795,6 +795,9 @@ ${report.days.length ? `<div class="table-wrap" tabindex="0" role="region" aria-
// module and borrow this one's kit (the page, the frame, the card, the pill, the flash and the same-origin check).
// module and borrow this one's kit (the page, the frame, the card, the pill, the flash and the same-origin check).
r.use(require('./memory').createMemoryRoutes(ctx, { page, wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note }));
r.use(require('./memory').createMemoryRoutes(ctx, { page, wrap, header, pill, flash, sameOrigin, viewer, form, signIn, note }));
+ // The front door a visitor gets at /, as a page for the layout: the Markdown twin of / is this page
+ // (server/agent-pages.js), so the twin says what a visitor reads (SEO-1; it was the old marketing home).
+ r.visitorHome = () => ({ css: ['css/c4.css'], scripts: ['js/c4.js'], body: raw(wrap(visitorHome())), index: true, agent: true, styles: [showcase.STYLESHEET], jsonLd: homeJsonLd(config) });
return r;
return r;
}
}
@@ -32,6 +32,18 @@ const { boot, check, done } = require('./helpers/boot');
@@ -32,6 +32,18 @@ const { boot, check, done } = require('./helpers/boot');
assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server/http/c4.js'), 'utf8'), /if \(!signedIn\) return page\(req, res, null, wrap\(visitorHome\(\)\)/, 'the route sends a visitor to the front door');
assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server/http/c4.js'), 'utf8'), /if \(!signedIn\) return page\(req, res, null, wrap\(visitorHome\(\)\)/, 'the route sends a visitor to the front door');
});
});
+ await check('the front door names its Markdown twin, and the twin is the front door (SEO-1)', async () => {
+ const home = await t.get('/');
+ assert.match(home.text, /<link rel="alternate" type="text\/markdown" href="https:\/\/openvibe\.codes\/index\.md">/, 'the head names the twin');
+ assert.match(String(home.headers.get('link') || ''), /<https:\/\/openvibe\.codes\/index\.md>; rel="alternate"; type="text\/markdown"/, 'so does the Link header');
+ const twin = await t.get('/index.md');
+ assert.strictEqual(twin.status, 200);
+ assert.match(String(twin.headers.get('content-type') || ''), /^text\/markdown/);
+ assert.ok(twin.text.includes('Every coding agent.') && twin.text.includes('Codex took over: Claude Code hit its plan limit'), 'the twin says what a visitor reads');
+ const mine = await t.get('/', { as: ada });
+ assert.ok(!/text\/markdown/.test(mine.text), 'a signed-in home names no twin');
+ });
+
await check('task details are owner scoped and management pages honor sign-in', async () => {
await check('task details are owner scoped and management pages honor sign-in', async () => {
const settings = await t.get('/settings', { as: ada });
const settings = await t.get('/settings', { as: ada });
assert.strictEqual(settings.status, 200);
assert.strictEqual(settings.status, 200);