openvibers/openvibe.space Commits
openvibe.website starts a site with no account (#151)
Changes
@@@ -19,12 +19,13 @@ const { asset, escapeHtml: esc } = require('./layout');const SPACE = 'https://openvibe.space';const NAME = 'OpenVibe.Website';const DESCRIPTION = 'Websites that look finished from the first preview: start from a real shop, portfolio, event, link page or launch page, or describe yours, and change anything with the preview beside it.';--const startHref = (id) => `${SPACE}/auth/login?next=${encodeURIComponent(`/spaces/new${id ? `?template=${id}` : ''}`)}`;++// No account to start (guest drafts, Space #150): a private edit link, and an OpenVibe account only to keep it.++const startHref = (id) => `${SPACE}/spaces/guest${id ? `?template=${encodeURIComponent(id)}` : '#sp-prompt'}`;const FAQ = [['Do I need to know how to code?', 'No. Pick a template and change the words, colours and pictures, or describe the site and OpenVibe AI writes a first version. The code is all there when you want it.'],['When can I put it on my own address?', 'Publishing at yourname.openvibe.website or on your own domain opens with public hosting, once the automatic abuse checks, a designated DMCA agent and the domain\'s Public Suffix List entry are in place. Until then you build, edit and preview, and OpenVibe staff publish.'],-- ['Why do I sign in on openvibe.space?', 'Your sites are kept with your OpenVibe account on OpenVibe.Space, the engine that builds and hosts them. openvibe.website itself keeps no cookies, so no site here can ever touch your sign-in.'],++ ['Do I need an account?', 'Not to start. Pick a template or describe your site and you get a private link to edit it, good for a week. Sign in with OpenVibe on openvibe.space to keep it; openvibe.website itself keeps no cookies, so no site here can touch your sign-in.'],['Can I take my site somewhere else?', 'Yes. A site is plain HTML, CSS and JavaScript; copy its files out of the editor whenever you like.'],];@@@ -70,7 +71,7 @@ ${sc.hero({{ label: 'Pick a template', href: '#templates', primary: true },{ label: 'Describe your site', href: startHref('') },],-- note: 'Building and previewing are open to everyone with an OpenVibe account. Publishing at your own address opens with public hosting.',++ note: 'No account needed to start: you get a private link to edit your site, and keep it with an OpenVibe account. Publishing at your own address opens with public hosting.',aside: { html: stage() },})}<section class="sc-sec" id="templates" aria-labelledby="wb-tpl-h">@@@ -217,7 +217,10 @@ function createSpacesPages(ctx) {// A visitor who is not signed in makes a draft here, on openvibe.space (never on openvibe.website, which// reads no cookie and sets none). The answer is the secret edit link; the cookie only remembers it.router.get('/spaces/guest', view(async (req, res) => {-- html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled() }));++ // ?template=<id>: a website template picked on openvibe.website leads the page, one press from a draft.++ const picked = req.query.template ? templates.get(String(req.query.template)) : null;++ const pick = picked && picked.kind === 'site' ? picked : null;++ html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled(), pick }));}));router.post('/spaces/guest', ...guestAction(async (req, res) => {const body = req.body || {};@@@ -233,7 +233,12 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.*/--function guestPage({ templates, error, prompt, aiOn }) {++function guestPage({ templates, error, prompt, aiOn, pick = null }) {++ const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">++ <span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>++ <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>++ <form method="post" action="/spaces/guest"><input type="hidden" name="template" value="${esc(pick.id)}"><button class="sp-btn">Start from this</button></form>++</section>` : '';const cards = templates.map((t) => `<div class="sp-template" style="--tpl:${esc(t.accent)}"><span class="sp-template-icon"><i class="fa-solid ${esc(t.icon)}" aria-hidden="true"></i></span><h3>${esc(t.name)}</h3><p>${esc(t.description)}</p>@@@ -244,6 +249,7 @@ function guestPage({ templates, error, prompt, aiOn }) {<h1 class="sp-title">Make a site.</h1><p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}++ ${picked}<form class="sp-ai-form" method="post" action="/spaces/guest"><label for="sp-prompt">Describe it and OpenVibe AI builds it</label><textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>@@@ -31,9 +31,10 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'weassert.strictEqual(sites.length, 5, 'five website templates');for (const s of sites) {assert.ok(r.text.includes(`<iframe src="/templates/${s.id}"`) && r.text.includes('sandbox=""'), `${s.id} previews with no script`);-- assert.ok(r.text.includes(`https://openvibe.space/auth/login?next=${encodeURIComponent(`/spaces/new?template=${s.id}`)}`), `${s.id} is one press from a new space`);++ assert.ok(r.text.includes(`https://openvibe.space/spaces/guest?template=${s.id}`), `${s.id} starts a draft with no account`);}assert.match(r.text, /Publishing at your own address opens with public hosting/, 'it claims no publishing it does not have');++ assert.match(r.text, /No account needed to start/, 'and says what is true now: no account to start');assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.website\/">/);assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');@@@ -80,6 +81,14 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'wefor (const s of templates.sites()) assert.ok(llms.includes(`https://openvibe.website/templates/${s.id}`), s.id);});++ await check('the guest start page leads with a website template picked on the front door', async () => {++ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=site-event' });++ assert.strictEqual(r.status, 200, r.text.slice(0, 200));++ assert.match(r.text, /<section class="sp-pick"[^>]*>[\s\S]*?<h2 id="sp-pick-h">Event page<\/h2>[\s\S]*?name="template" value="site-event"/);++ const app = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=poll' });++ assert.ok(!app.text.includes('class="sp-pick"'), 'an app template is not a website to start from here');++ });++await check('openvibe.space leads with a template picked on the front door', async () => {const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=site-portfolio', session: alice });assert.strictEqual(r.status, 200, r.text.slice(0, 200));
@@ -19,12 +19,13 @@ const { asset, escapeHtml: esc } = require('./layout');
@@ -19,12 +19,13 @@ const { asset, escapeHtml: esc } = require('./layout');
const SPACE = 'https://openvibe.space';
const SPACE = 'https://openvibe.space';
const NAME = 'OpenVibe.Website';
const NAME = 'OpenVibe.Website';
const DESCRIPTION = 'Websites that look finished from the first preview: start from a real shop, portfolio, event, link page or launch page, or describe yours, and change anything with the preview beside it.';
const DESCRIPTION = 'Websites that look finished from the first preview: start from a real shop, portfolio, event, link page or launch page, or describe yours, and change anything with the preview beside it.';
-const startHref = (id) => `${SPACE}/auth/login?next=${encodeURIComponent(`/spaces/new${id ? `?template=${id}` : ''}`)}`;
+// No account to start (guest drafts, Space #150): a private edit link, and an OpenVibe account only to keep it.
+const startHref = (id) => `${SPACE}/spaces/guest${id ? `?template=${encodeURIComponent(id)}` : '#sp-prompt'}`;
const FAQ = [
const FAQ = [
['Do I need to know how to code?', 'No. Pick a template and change the words, colours and pictures, or describe the site and OpenVibe AI writes a first version. The code is all there when you want it.'],
['Do I need to know how to code?', 'No. Pick a template and change the words, colours and pictures, or describe the site and OpenVibe AI writes a first version. The code is all there when you want it.'],
['When can I put it on my own address?', 'Publishing at yourname.openvibe.website or on your own domain opens with public hosting, once the automatic abuse checks, a designated DMCA agent and the domain\'s Public Suffix List entry are in place. Until then you build, edit and preview, and OpenVibe staff publish.'],
['When can I put it on my own address?', 'Publishing at yourname.openvibe.website or on your own domain opens with public hosting, once the automatic abuse checks, a designated DMCA agent and the domain\'s Public Suffix List entry are in place. Until then you build, edit and preview, and OpenVibe staff publish.'],
- ['Why do I sign in on openvibe.space?', 'Your sites are kept with your OpenVibe account on OpenVibe.Space, the engine that builds and hosts them. openvibe.website itself keeps no cookies, so no site here can ever touch your sign-in.'],
+ ['Do I need an account?', 'Not to start. Pick a template or describe your site and you get a private link to edit it, good for a week. Sign in with OpenVibe on openvibe.space to keep it; openvibe.website itself keeps no cookies, so no site here can touch your sign-in.'],
['Can I take my site somewhere else?', 'Yes. A site is plain HTML, CSS and JavaScript; copy its files out of the editor whenever you like.'],
['Can I take my site somewhere else?', 'Yes. A site is plain HTML, CSS and JavaScript; copy its files out of the editor whenever you like.'],
];
];
@@ -70,7 +71,7 @@ ${sc.hero({
@@ -70,7 +71,7 @@ ${sc.hero({
{ label: 'Pick a template', href: '#templates', primary: true },
{ label: 'Pick a template', href: '#templates', primary: true },
{ label: 'Describe your site', href: startHref('') },
{ label: 'Describe your site', href: startHref('') },
],
],
- note: 'Building and previewing are open to everyone with an OpenVibe account. Publishing at your own address opens with public hosting.',
+ note: 'No account needed to start: you get a private link to edit your site, and keep it with an OpenVibe account. Publishing at your own address opens with public hosting.',
aside: { html: stage() },
aside: { html: stage() },
})}
})}
<section class="sc-sec" id="templates" aria-labelledby="wb-tpl-h">
<section class="sc-sec" id="templates" aria-labelledby="wb-tpl-h">
@@ -217,7 +217,10 @@ function createSpacesPages(ctx) {
@@ -217,7 +217,10 @@ function createSpacesPages(ctx) {
// A visitor who is not signed in makes a draft here, on openvibe.space (never on openvibe.website, which
// A visitor who is not signed in makes a draft here, on openvibe.space (never on openvibe.website, which
// reads no cookie and sets none). The answer is the secret edit link; the cookie only remembers it.
// reads no cookie and sets none). The answer is the secret edit link; the cookie only remembers it.
router.get('/spaces/guest', view(async (req, res) => {
router.get('/spaces/guest', view(async (req, res) => {
- html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled() }));
+ // ?template=<id>: a website template picked on openvibe.website leads the page, one press from a draft.
+ const picked = req.query.template ? templates.get(String(req.query.template)) : null;
+ const pick = picked && picked.kind === 'site' ? picked : null;
+ html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled(), pick }));
}));
}));
router.post('/spaces/guest', ...guestAction(async (req, res) => {
router.post('/spaces/guest', ...guestAction(async (req, res) => {
const body = req.body || {};
const body = req.body || {};
@@ -233,7 +233,12 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
@@ -233,7 +233,12 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a
* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a
* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.
* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.
*/
*/
-function guestPage({ templates, error, prompt, aiOn }) {
+function guestPage({ templates, error, prompt, aiOn, pick = null }) {
+ const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
+ <span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
+ <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
+ <form method="post" action="/spaces/guest"><input type="hidden" name="template" value="${esc(pick.id)}"><button class="sp-btn">Start from this</button></form>
+</section>` : '';
const cards = templates.map((t) => `<div class="sp-template" style="--tpl:${esc(t.accent)}">
const cards = templates.map((t) => `<div class="sp-template" style="--tpl:${esc(t.accent)}">
<span class="sp-template-icon"><i class="fa-solid ${esc(t.icon)}" aria-hidden="true"></i></span>
<span class="sp-template-icon"><i class="fa-solid ${esc(t.icon)}" aria-hidden="true"></i></span>
<h3>${esc(t.name)}</h3><p>${esc(t.description)}</p>
<h3>${esc(t.name)}</h3><p>${esc(t.description)}</p>
@@ -244,6 +249,7 @@ function guestPage({ templates, error, prompt, aiOn }) {
@@ -244,6 +249,7 @@ function guestPage({ templates, error, prompt, aiOn }) {
<h1 class="sp-title">Make a site.</h1>
<h1 class="sp-title">Make a site.</h1>
<p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>
<p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
+ ${picked}
<form class="sp-ai-form" method="post" action="/spaces/guest">
<form class="sp-ai-form" method="post" action="/spaces/guest">
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
@@ -31,9 +31,10 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
@@ -31,9 +31,10 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
assert.strictEqual(sites.length, 5, 'five website templates');
assert.strictEqual(sites.length, 5, 'five website templates');
for (const s of sites) {
for (const s of sites) {
assert.ok(r.text.includes(`<iframe src="/templates/${s.id}"`) && r.text.includes('sandbox=""'), `${s.id} previews with no script`);
assert.ok(r.text.includes(`<iframe src="/templates/${s.id}"`) && r.text.includes('sandbox=""'), `${s.id} previews with no script`);
- assert.ok(r.text.includes(`https://openvibe.space/auth/login?next=${encodeURIComponent(`/spaces/new?template=${s.id}`)}`), `${s.id} is one press from a new space`);
+ assert.ok(r.text.includes(`https://openvibe.space/spaces/guest?template=${s.id}`), `${s.id} starts a draft with no account`);
}
}
assert.match(r.text, /Publishing at your own address opens with public hosting/, 'it claims no publishing it does not have');
assert.match(r.text, /Publishing at your own address opens with public hosting/, 'it claims no publishing it does not have');
+ assert.match(r.text, /No account needed to start/, 'and says what is true now: no account to start');
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.website\/">/);
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe\.website\/">/);
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');
assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');
@@ -80,6 +81,14 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
@@ -80,6 +81,14 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
for (const s of templates.sites()) assert.ok(llms.includes(`https://openvibe.website/templates/${s.id}`), s.id);
for (const s of templates.sites()) assert.ok(llms.includes(`https://openvibe.website/templates/${s.id}`), s.id);
});
});
+ await check('the guest start page leads with a website template picked on the front door', async () => {
+ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=site-event' });
+ assert.strictEqual(r.status, 200, r.text.slice(0, 200));
+ assert.match(r.text, /<section class="sp-pick"[^>]*>[\s\S]*?<h2 id="sp-pick-h">Event page<\/h2>[\s\S]*?name="template" value="site-event"/);
+ const app = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=poll' });
+ assert.ok(!app.text.includes('class="sp-pick"'), 'an app template is not a website to start from here');
+ });
+
await check('openvibe.space leads with a template picked on the front door', async () => {
await check('openvibe.space leads with a template picked on the front door', async () => {
const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=site-portfolio', session: alice });
const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=site-portfolio', session: alice });
assert.strictEqual(r.status, 200, r.text.slice(0, 200));
assert.strictEqual(r.status, 200, r.text.slice(0, 200));