openvibers/openvibe.blog Commits
Use shared Publishing block styles in Blog (#259)
Changes
@@@ -345,7 +345,7 @@ Called elsewhere, as the service principal `blog`: `identity.subject.resolve` an- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).--- **Packages** (all pinned by release tarball): `openvibe-publishing` v1.6.1 (async PostgreSQL stores: revisions,++- **Packages** (all pinned by release tarball): `openvibe-publishing` v1.7.0 (async PostgreSQL stores: revisions,schedule, taxonomy, citations, media, discussion, seo, authorship, index-hooks, ssr, layout), `openvibe-contracts`v0.97.0, `openvibe-shared` v4.179.0 (Frame, app icon, footer, legal, release, metrics, ready, theme presets),`openvibe-sdk` v0.133.0 (sso, db, PostgreSQL events outbox, service tokens, per-actor limits, testing).@@@ -559,5 +559,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [- openvibe-contracts: v0.373.0- openvibe-sdk: v0.133.0- openvibe-shared: v4.179.0--- openvibe-publishing: v1.6.1++- openvibe-publishing: v1.7.0<!-- versions:end -->@@@ -11,7 +11,7 @@"sdk": "openvibe-sdk v0.133.0","shared": "openvibe-shared v4.179.0","packages": [-- "openvibe-publishing v1.6.1",++ "openvibe-publishing v1.7.0","openvibe-contracts v0.373.0","openvibe-shared v4.179.0","openvibe-sdk v0.133.0"@@@ -16,7 +16,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0","pg": "^8.23.0"@@@ -1164,9 +1164,9 @@}},"node_modules/openvibe-publishing": {-- "version": "1.6.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",-- "integrity": "sha512-47H8yCGpqPFhWwK1SZ0GkvYh+JpwbleamnEqBvhXAwj3Ep176b2J7rcCrQEqgQqpVr8QPrm2aTdTNq3k3mGCbg==",++ "version": "1.7.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",++ "integrity": "sha512-65DspLWSuy6KO1uzR88aLIVfRhEpC4J3+xxK34rcj/VoEaCSV6dTBjDqlSwM/e3Cj2SPoVj4QT/qsUkx7VDt6w==","license": "MIT","engines": {"node": ">=20"@@@ -29,7 +29,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0","pg": "^8.23.0"@@@ -1,11 +1,7 @@--/* Post reading layout, rich blocks and the editor. Loaded only where bodies are rendered. */--.post-body img, .ov-media img { max-width: 100%; height: auto; border-radius: 8px; }++/* Post reading layout and editor. Loaded only where bodies are rendered. */++.post-body img { max-width: 100%; height: auto; border-radius: 8px; }.post-body pre { overflow-x: auto; background: var(--bg-tertiary, #172238); padding: 12px; border-radius: 8px; }.post-body blockquote { border-left: 3px solid var(--accent, #3b82f6); margin: 0; padding-left: 14px; color: var(--text-secondary); }--.ov-media { margin: 1.2rem 0; }--.ov-media figcaption { font-size: .9rem; color: var(--text-muted); }--.ov-media-missing { padding: 28px 16px; border: 1px dashed var(--border-light, #2c3d5c); border-radius: 8px; text-align: center; color: var(--text-secondary); background: var(--bg-tertiary, #172238); }--/* ── The reading layout (the blog outcomes, research-2026-10-30/market/outcomes/blog.md): a 65-character measure at18 px, mobile leading at 1.5, a stated read time, the post's own sections as a contents list (a fold under the titleon phones, a rail to the left of the text from 1100 px), and the series as one line above and its neighbours below. */@@@ -52,104 +48,17 @@ body.post-page .page { max-width: 1160px; }.toc-rail a:hover { color: var(--text-primary); border-left-color: var(--accent, #3b82f6); }}--/* ── Rich blocks (stage B1): the blocks of a body, server-rendered. Every block has a plain-HTML form,-- so a reader without JavaScript sees the same thing. A section is a block: a big heading, a byline and-- the reader's own read on it (👍 or 👎) at the end. The contents rail stands beside the text on a-- computer; on a phone the sections are a row of chips under the title. */++/* The post header and its contents list belong to Blog. */.post-header { margin-bottom: 4px; }.post-title { font-size: clamp(1.9rem, 1.35rem + 2.2vw, 3rem); line-height: 1.12; letter-spacing: -.02em; margin: 0 0 10px; }.post-teaser { margin: 0 0 10px; max-width: 62ch; color: var(--text-secondary, #96a7c2); font-size: 1.12rem; line-height: 1.5; }.post-hero-art { margin: 18px 0 4px; }.post-hero-art img { display: block; width: 100%; height: auto; border-radius: 16px; }--/* A section: the heading is big, the byline says who wrote it, and the reactions sit at its end. */--.ov-section { margin: 34px 0; scroll-margin-top: calc(var(--ovnav-real-h, 56px) + 16px); }--.ov-section > h2 { font-size: clamp(1.5rem, 1.2rem + 1.1vw, 2rem); line-height: 1.2; letter-spacing: -.015em; margin: 0 0 6px; }--.ov-section > h2:target { color: var(--accent, #3b82f6); }--.ov-section-by { margin: 0 0 14px; color: var(--text-muted, #7386a3); font-size: .9rem; }--.ov-section-by a { color: var(--text-secondary, #96a7c2); }--.ov-hero { margin: 0 0 6px; }--.ov-hero .ov-kicker { margin: 0; color: var(--accent, #3b82f6); font-size: .86rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }--.ov-hero-date { margin: 2px 0 0; color: var(--text-muted, #7386a3); font-size: .86rem; }----/* The reader's read on a section: a form that posts (it works without script) and the counts in place. */--.ov-reactions { display: flex; align-items: center; gap: 8px; margin: 18px 0 0; }--.ov-reactions button { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; background: var(--bg-card, #131c2e); color: var(--text-primary); font-size: .95rem; cursor: pointer; }--.ov-reactions button:hover { border-color: var(--accent, #3b82f6); }--.ov-react-count { color: var(--text-secondary, #96a7c2); font-variant-numeric: tabular-nums; }--.ov-react-signin { min-height: 44px; display: inline-flex; align-items: center; color: var(--text-secondary, #96a7c2); font-size: .92rem; }----/* The other blocks: a callout, a stat, a quote, a compare, a gallery, a patch-notes fold, a timeline, an infobox. */--.ov-callout { margin: 26px 0; padding: 16px 18px; border-left: 4px solid var(--accent, #3b82f6); border-radius: 10px; background: var(--bg-card, #131c2e); }--.ov-callout-warn { border-left-color: #f59e0b; }--.ov-callout-tip { border-left-color: #22c55e; }--.ov-callout-title { margin: 0 0 6px; font-weight: 700; }--.ov-stat { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin: 26px 0; }--.ov-stat-value { font-size: 2.2rem; font-weight: 800; letter-spacing: -.02em; }--.ov-stat-label { color: var(--text-secondary, #96a7c2); font-size: 1.05rem; }--.ov-quote { margin: 26px 0; padding: 4px 0 4px 18px; border-left: 3px solid var(--border, #1f2d47); }--.ov-quote-by { margin: 8px 0 0; color: var(--text-muted, #7386a3); font-size: .9rem; }--.ov-compare { margin: 26px 0; }--.ov-compare-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 14px; }--.ov-gallery { margin: 26px 0; }--.ov-gallery-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 12px; }--.ov-media-credit { display: block; margin-top: 6px; color: var(--text-muted, #7386a3); font-size: .82rem; }--.ov-patchnotes { margin: 26px 0; border: 1px solid var(--border, #1f2d47); border-radius: 12px; background: var(--bg-card, #131c2e); }--.ov-patchnotes > summary { display: flex; align-items: center; min-height: 46px; padding: 0 14px; font-weight: 700; cursor: pointer; list-style: none; }--.ov-patchnotes > summary::-webkit-details-marker { display: none; }--.ov-patchnotes-site { padding: 0 14px 10px; }--.ov-patchnotes-site h3 { margin: 12px 0 4px; font-size: 1rem; }--.ov-patchnotes-site ul { margin: 0; padding-left: 20px; }--.ov-infobox, .ov-timeline { margin: 26px 0; padding: 16px 18px; border: 1px solid var(--border, #1f2d47); border-radius: 12px; background: var(--bg-card, #131c2e); }--.ov-infobox dl { margin: 0; }--.ov-infobox-row { display: flex; gap: 10px; padding: 4px 0; border-top: 1px solid var(--border, #1f2d47); }--.ov-infobox-key { color: var(--text-muted, #7386a3); }--.ov-infobox-value { margin: 0; }--.ov-timeline ol { margin: 0; padding-left: 20px; }--.ov-timeline-when { color: var(--text-muted, #7386a3); font-variant-numeric: tabular-nums; }--.ov-timeline-text { margin-left: 8px; }--/* The contents: a rail beside the text on a computer, a row of chips under the title on a phone. */.toc-chips { display: flex; gap: 8px; overflow-x: auto; margin: 14px 0 4px; padding-bottom: 4px; scrollbar-width: thin; }.toc-chip { flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 12px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; color: var(--text-secondary, #96a7c2); font-size: .88rem; text-decoration: none; white-space: nowrap; }@media (min-width: 1100px) { .toc-chips { display: none; } }--/* ── Devblog look (Opus, 11-09: "Rust-style devblogs… named updates with title art… epic") ── */--/* The hero is a banner: its title art fills it (a dark wash keeps the words readable), or an accent wash without art. The-- kicker is a pill beside the date, and the title is the biggest thing on the page. */--.ov-hero { position: relative; isolation: isolate; overflow: hidden; display: grid; gap: 10px; margin: 0 0 28px; padding: clamp(22px, 4vw, 40px);-- border-radius: 22px; border: 1px solid var(--border, #1f2d47);-- background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent, #3b82f6) 34%, transparent), transparent 62%),-- linear-gradient(160deg, color-mix(in srgb, var(--accent, #3b82f6) 14%, var(--bg-card, #131c2e)), var(--bg-card, #131c2e)); }--.ov-hero .ov-hero-art { position: absolute; inset: 0; z-index: -1; margin: 0; }--.ov-hero .ov-hero-art img { width: 100%; height: 100%; object-fit: cover; }--.ov-hero .ov-hero-art::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(4, 8, 18, .18) 0%, rgba(4, 8, 18, .78) 100%); }--.ov-hero:has(.ov-hero-art) { min-height: clamp(260px, 42vw, 420px); align-content: end; color: #fff; border-color: transparent; }--.ov-hero:has(.ov-hero-art) .ov-hero-body p { color: rgba(255, 255, 255, .86); }--.ov-hero .ov-kicker { justify-self: start; display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: .78rem;-- font-weight: 750; letter-spacing: .02em; text-transform: none; color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }--.ov-hero .ov-kicker::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }--.ov-hero:has(.ov-hero-art) .ov-kicker { color: #fff; background: rgba(255, 255, 255, .18); backdrop-filter: blur(6px); }--.ov-hero-date { font-size: .86rem; color: var(--text-muted, #7386a3); }--.ov-hero:has(.ov-kicker) .ov-hero-date { grid-row: 1; justify-self: start; margin: 0 0 0 0; padding: 4px 0; }--.ov-hero:has(.ov-kicker) { grid-template-columns: auto 1fr; column-gap: 10px; }--.ov-hero:has(.ov-kicker) .ov-kicker { grid-row: 1; grid-column: 1; }--.ov-hero:has(.ov-kicker) .ov-hero-date { grid-column: 2; }--.ov-hero:has(.ov-kicker) .ov-hero-body { grid-column: 1 / -1; }--.ov-hero-body > :is(h1, h2) { margin: 4px 0 10px; font-size: clamp(2rem, 1.35rem + 3.2vw, 3.4rem); line-height: 1.06; letter-spacing: -.028em; font-weight: 800; }--.ov-hero-body > p { margin: 0; max-width: 62ch; font-size: clamp(1.02rem, .95rem + .4vw, 1.2rem); line-height: 1.55; color: var(--text-secondary, #96a7c2); }--/* The timeline: a line with a dot per moment, the time in tabular numbers, the words beside it. */--.ov-timeline ol { position: relative; list-style: none; margin: 4px 0 0; padding: 0; }--.ov-timeline ol::before { content: ""; position: absolute; left: 5px; top: 10px; bottom: 10px; width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent); }--.ov-timeline-item { position: relative; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; padding: 7px 0 7px 24px; }--.ov-timeline-item::before { content: ""; position: absolute; left: 0; top: 13px; width: 12px; height: 12px; border-radius: 50%; background: var(--bg-card, #131c2e); box-shadow: inset 0 0 0 3px var(--accent, #3b82f6); }--.ov-timeline-when { min-width: 3.4em; font-weight: 700; color: var(--accent, #3b82f6); }--.ov-timeline-text { margin-left: 0; }--.ov-timeline-title, .ov-infobox-title { margin: 0 0 8px; font-size: 1.05rem; }--/* A stat reads as a figure, in the accent. */--.ov-stat-value { color: var(--accent, #3b82f6); font-size: clamp(2.2rem, 1.8rem + 2vw, 3rem); line-height: 1; }--.ov-infobox-row { display: grid; grid-template-columns: minmax(84px, 32%) minmax(0, 1fr); gap: 12px; padding: 7px 0; }--.ov-infobox-key { margin: 0; }--/* ── The block inserter (public/js/blog-editor.js): a "/" menu, a form per block with a live preview, anoutline dragged to reorder, picture search and Make an image. Progressive only: the textarea saves. ── */.ov-editor { margin: 6px 0 4px; }@@@ -216,6 +216,11 @@ async function createApp(opts = {}) {// ── Static assets (content-hashed ?v= → immutable) ──────// This site's own pinned copy of the OpenVibe Frame's browser files (openvibe-shared/serve).app.use('/shared', require('openvibe-shared/serve').handler());++ app.get('/css/publishing-blocks.css', (req, res) => {++ const v = req.query.v;++ res.type('css').set('Cache-Control', cache.assetHeaders('css/publishing-blocks.css', { hashed: !!v && v === assetVersion('css/publishing-blocks.css') }))++ .sendFile(require.resolve('openvibe-publishing/blocks.css'));++ });app.use(express.static(PUBLIC_DIR, {index: false, redirect: false,setHeaders(res, filePath) {@@@ -70,7 +70,8 @@ function aiDraftForm({ blog, csrf }) {function postForm({ blog, post, head, terms, series, csrf, message, action }) {const f = head ? head.fields : {};const vis = post ? post.visibility : 'public';-- return String(h`<link rel="stylesheet" href="${asset('css/blocks.css')}">++ return String(h`<link rel="stylesheet" href="${asset('css/publishing-blocks.css')}">++<link rel="stylesheet" href="${asset('css/blocks.css')}">${flash(message)}<form method="post" action="${action}" class="post-form">${csrfField(csrf)}${head ? h`<input type="hidden" name="expectedRevision" value="${head.number}">` : ''}@@@ -22,12 +22,13 @@ const SITE_NAME = 'OpenVibe.Blog';// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.const SITE_SUMMARY = 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');++const PUBLISHING_BLOCKS_CSS = require.resolve('openvibe-publishing/blocks.css');const hashes = new Map();function assetVersion(rel) {if (hashes.has(rel)) return hashes.get(rel);let v = 'dev';-- try { v = crypto.createHash('sha256').update(fs.readFileSync(path.join(PUBLIC_DIR, rel))).digest('hex').slice(0, 10); } catch { /* missing asset */ }++ try { v = crypto.createHash('sha256').update(fs.readFileSync(rel === 'css/publishing-blocks.css' ? PUBLISHING_BLOCKS_CSS : path.join(PUBLIC_DIR, rel))).digest('hex').slice(0, 10); } catch { /* missing asset */ }hashes.set(rel, v);return v;}@@@ -226,7 +226,8 @@ function postPage(o) {// summary only shows when the hero has no prose of its own.const heroHtml = rendered.heroHtml || null;const heroTitle = rendered.heroHeading || null;-- return String(h`<link rel="stylesheet" href="${asset('css/blocks.css')}">++ return String(h`<link rel="stylesheet" href="${asset('css/publishing-blocks.css')}">++<link rel="stylesheet" href="${asset('css/blocks.css')}">${raw(ssr.breadcrumbsHtml(o.breadcrumbs))}<div class="post-wrap${toc ? ' has-toc' : ''}">${raw(toc ? contentsHtml(toc) : '')}@@@ -29,6 +29,14 @@ const REVALIDATED = 'public, max-age=300, stale-while-revalidate=86400';assert.strictEqual(r.status, 200);assert.strictEqual(r.headers.get('cache-control'), REVALIDATED);});++ await check('the library block sheet is content-addressed and served as CSS', async () => {++ const rel = 'css/publishing-blocks.css';++ const r = await t.get(`/${rel}?v=${assetVersion(rel)}`);++ assert.strictEqual(r.status, 200);++ assert.match(r.headers.get('content-type'), /^text\/css/);++ assert.strictEqual(r.headers.get('cache-control'), IMMUTABLE);++ assert.match(r.text, /\.ov-infobox/);++ });await t.close();done();})().catch((err) => { console.error(err); process.exit(1); });@@@ -90,6 +90,9 @@ const body = (by) => [await check('the published page renders the blocks: the hero art, the section, the byline, the rail', async () => {await t.get(`/api/v1/posts/${post.id}/publish`, { as: alice, json: {} });const page = await t.get('/@alice/blocks');++ const sharedCss = page.text.match(/<link rel="stylesheet" href="(\/css\/publishing-blocks\.css\?v=[0-9a-f]{10})">/);++ assert.ok(sharedCss, 'the post links the library blocks sheet');++ assert.ok(page.text.indexOf(sharedCss[0]) < page.text.indexOf('<link rel="stylesheet" href="/css/blocks.css?'), 'the library sheet loads before Blog placement rules');assert.strictEqual(page.status, 200, page.text);assert.match(page.text, /class="ov-media ov-hero-art"/);assert.match(page.text, /id="blueprint-rework"/);@@@ -58,6 +58,7 @@ const idFrom = (location) => (location.match(/\/write\/posts\/(pst_[0-9A-Z]+)/)await check('new draft, edit and publish with forms', async () => {const form = await t.get('/write/@eve/new', { as: eve });assert.match(form.text, /<textarea id="body" name="body"/);++ assert.ok(form.text.indexOf('href="/css/publishing-blocks.css?v=') < form.text.indexOf('href="/css/blocks.css?v='), 'the editor loads shared block styles before its own');let r = await t.get('/write/@eve/new', { as: eve, form: { _csrf: csrf, title: 'Notes without script', body: `# Heading\n\nPlain *text*. ${LONG}`, tags: 'nojs', series: 'Field notes', seriesPosition: '1', visibility: 'public', allowComments: '1' } });assert.strictEqual(r.status, 303, r.text);postId = idFrom(r.headers.get('location'));
@@ -345,7 +345,7 @@ Called elsewhere, as the service principal `blog`: `identity.subject.resolve` an
@@ -345,7 +345,7 @@ Called elsewhere, as the service principal `blog`: `identity.subject.resolve` an
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
-- **Packages** (all pinned by release tarball): `openvibe-publishing` v1.6.1 (async PostgreSQL stores: revisions,
+- **Packages** (all pinned by release tarball): `openvibe-publishing` v1.7.0 (async PostgreSQL stores: revisions,
schedule, taxonomy, citations, media, discussion, seo, authorship, index-hooks, ssr, layout), `openvibe-contracts`
schedule, taxonomy, citations, media, discussion, seo, authorship, index-hooks, ssr, layout), `openvibe-contracts`
v0.97.0, `openvibe-shared` v4.179.0 (Frame, app icon, footer, legal, release, metrics, ready, theme presets),
v0.97.0, `openvibe-shared` v4.179.0 (Frame, app icon, footer, legal, release, metrics, ready, theme presets),
`openvibe-sdk` v0.133.0 (sso, db, PostgreSQL events outbox, service tokens, per-actor limits, testing).
`openvibe-sdk` v0.133.0 (sso, db, PostgreSQL events outbox, service tokens, per-actor limits, testing).
@@ -559,5 +559,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -559,5 +559,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
- openvibe-contracts: v0.373.0
- openvibe-contracts: v0.373.0
- openvibe-sdk: v0.133.0
- openvibe-sdk: v0.133.0
- openvibe-shared: v4.179.0
- openvibe-shared: v4.179.0
-- openvibe-publishing: v1.6.1
+- openvibe-publishing: v1.7.0
<!-- versions:end -->
<!-- versions:end -->
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
"sdk": "openvibe-sdk v0.133.0",
"sdk": "openvibe-sdk v0.133.0",
"shared": "openvibe-shared v4.179.0",
"shared": "openvibe-shared v4.179.0",
"packages": [
"packages": [
- "openvibe-publishing v1.6.1",
+ "openvibe-publishing v1.7.0",
"openvibe-contracts v0.373.0",
"openvibe-contracts v0.373.0",
"openvibe-shared v4.179.0",
"openvibe-shared v4.179.0",
"openvibe-sdk v0.133.0"
"openvibe-sdk v0.133.0"
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",
- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",
+ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1164,9 +1164,9 @@
@@ -1164,9 +1164,9 @@
}
}
},
},
"node_modules/openvibe-publishing": {
"node_modules/openvibe-publishing": {
- "version": "1.6.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",
- "integrity": "sha512-47H8yCGpqPFhWwK1SZ0GkvYh+JpwbleamnEqBvhXAwj3Ep176b2J7rcCrQEqgQqpVr8QPrm2aTdTNq3k3mGCbg==",
+ "version": "1.7.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
+ "integrity": "sha512-65DspLWSuy6KO1uzR88aLIVfRhEpC4J3+xxK34rcj/VoEaCSV6dTBjDqlSwM/e3Cj2SPoVj4QT/qsUkx7VDt6w==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",
- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.6.1",
+ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1,11 +1,7 @@
@@ -1,11 +1,7 @@
-/* Post reading layout, rich blocks and the editor. Loaded only where bodies are rendered. */
-.post-body img, .ov-media img { max-width: 100%; height: auto; border-radius: 8px; }
+/* Post reading layout and editor. Loaded only where bodies are rendered. */
+.post-body img { max-width: 100%; height: auto; border-radius: 8px; }
.post-body pre { overflow-x: auto; background: var(--bg-tertiary, #172238); padding: 12px; border-radius: 8px; }
.post-body pre { overflow-x: auto; background: var(--bg-tertiary, #172238); padding: 12px; border-radius: 8px; }
.post-body blockquote { border-left: 3px solid var(--accent, #3b82f6); margin: 0; padding-left: 14px; color: var(--text-secondary); }
.post-body blockquote { border-left: 3px solid var(--accent, #3b82f6); margin: 0; padding-left: 14px; color: var(--text-secondary); }
-.ov-media { margin: 1.2rem 0; }
-.ov-media figcaption { font-size: .9rem; color: var(--text-muted); }
-.ov-media-missing { padding: 28px 16px; border: 1px dashed var(--border-light, #2c3d5c); border-radius: 8px; text-align: center; color: var(--text-secondary); background: var(--bg-tertiary, #172238); }
-
/* ── The reading layout (the blog outcomes, research-2026-10-30/market/outcomes/blog.md): a 65-character measure at
/* ── The reading layout (the blog outcomes, research-2026-10-30/market/outcomes/blog.md): a 65-character measure at
18 px, mobile leading at 1.5, a stated read time, the post's own sections as a contents list (a fold under the title
18 px, mobile leading at 1.5, a stated read time, the post's own sections as a contents list (a fold under the title
on phones, a rail to the left of the text from 1100 px), and the series as one line above and its neighbours below. */
on phones, a rail to the left of the text from 1100 px), and the series as one line above and its neighbours below. */
@@ -52,104 +48,17 @@ body.post-page .page { max-width: 1160px; }
@@ -52,104 +48,17 @@ body.post-page .page { max-width: 1160px; }
.toc-rail a:hover { color: var(--text-primary); border-left-color: var(--accent, #3b82f6); }
.toc-rail a:hover { color: var(--text-primary); border-left-color: var(--accent, #3b82f6); }
}
}
-/* ── Rich blocks (stage B1): the blocks of a body, server-rendered. Every block has a plain-HTML form,
- so a reader without JavaScript sees the same thing. A section is a block: a big heading, a byline and
- the reader's own read on it (👍 or 👎) at the end. The contents rail stands beside the text on a
- computer; on a phone the sections are a row of chips under the title. */
+/* The post header and its contents list belong to Blog. */
.post-header { margin-bottom: 4px; }
.post-header { margin-bottom: 4px; }
.post-title { font-size: clamp(1.9rem, 1.35rem + 2.2vw, 3rem); line-height: 1.12; letter-spacing: -.02em; margin: 0 0 10px; }
.post-title { font-size: clamp(1.9rem, 1.35rem + 2.2vw, 3rem); line-height: 1.12; letter-spacing: -.02em; margin: 0 0 10px; }
.post-teaser { margin: 0 0 10px; max-width: 62ch; color: var(--text-secondary, #96a7c2); font-size: 1.12rem; line-height: 1.5; }
.post-teaser { margin: 0 0 10px; max-width: 62ch; color: var(--text-secondary, #96a7c2); font-size: 1.12rem; line-height: 1.5; }
.post-hero-art { margin: 18px 0 4px; }
.post-hero-art { margin: 18px 0 4px; }
.post-hero-art img { display: block; width: 100%; height: auto; border-radius: 16px; }
.post-hero-art img { display: block; width: 100%; height: auto; border-radius: 16px; }
-/* A section: the heading is big, the byline says who wrote it, and the reactions sit at its end. */
-.ov-section { margin: 34px 0; scroll-margin-top: calc(var(--ovnav-real-h, 56px) + 16px); }
-.ov-section > h2 { font-size: clamp(1.5rem, 1.2rem + 1.1vw, 2rem); line-height: 1.2; letter-spacing: -.015em; margin: 0 0 6px; }
-.ov-section > h2:target { color: var(--accent, #3b82f6); }
-.ov-section-by { margin: 0 0 14px; color: var(--text-muted, #7386a3); font-size: .9rem; }
-.ov-section-by a { color: var(--text-secondary, #96a7c2); }
-.ov-hero { margin: 0 0 6px; }
-.ov-hero .ov-kicker { margin: 0; color: var(--accent, #3b82f6); font-size: .86rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
-.ov-hero-date { margin: 2px 0 0; color: var(--text-muted, #7386a3); font-size: .86rem; }
-
-/* The reader's read on a section: a form that posts (it works without script) and the counts in place. */
-.ov-reactions { display: flex; align-items: center; gap: 8px; margin: 18px 0 0; }
-.ov-reactions button { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; background: var(--bg-card, #131c2e); color: var(--text-primary); font-size: .95rem; cursor: pointer; }
-.ov-reactions button:hover { border-color: var(--accent, #3b82f6); }
-.ov-react-count { color: var(--text-secondary, #96a7c2); font-variant-numeric: tabular-nums; }
-.ov-react-signin { min-height: 44px; display: inline-flex; align-items: center; color: var(--text-secondary, #96a7c2); font-size: .92rem; }
-
-/* The other blocks: a callout, a stat, a quote, a compare, a gallery, a patch-notes fold, a timeline, an infobox. */
-.ov-callout { margin: 26px 0; padding: 16px 18px; border-left: 4px solid var(--accent, #3b82f6); border-radius: 10px; background: var(--bg-card, #131c2e); }
-.ov-callout-warn { border-left-color: #f59e0b; }
-.ov-callout-tip { border-left-color: #22c55e; }
-.ov-callout-title { margin: 0 0 6px; font-weight: 700; }
-.ov-stat { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin: 26px 0; }
-.ov-stat-value { font-size: 2.2rem; font-weight: 800; letter-spacing: -.02em; }
-.ov-stat-label { color: var(--text-secondary, #96a7c2); font-size: 1.05rem; }
-.ov-quote { margin: 26px 0; padding: 4px 0 4px 18px; border-left: 3px solid var(--border, #1f2d47); }
-.ov-quote-by { margin: 8px 0 0; color: var(--text-muted, #7386a3); font-size: .9rem; }
-.ov-compare { margin: 26px 0; }
-.ov-compare-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 14px; }
-.ov-gallery { margin: 26px 0; }
-.ov-gallery-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 12px; }
-.ov-media-credit { display: block; margin-top: 6px; color: var(--text-muted, #7386a3); font-size: .82rem; }
-.ov-patchnotes { margin: 26px 0; border: 1px solid var(--border, #1f2d47); border-radius: 12px; background: var(--bg-card, #131c2e); }
-.ov-patchnotes > summary { display: flex; align-items: center; min-height: 46px; padding: 0 14px; font-weight: 700; cursor: pointer; list-style: none; }
-.ov-patchnotes > summary::-webkit-details-marker { display: none; }
-.ov-patchnotes-site { padding: 0 14px 10px; }
-.ov-patchnotes-site h3 { margin: 12px 0 4px; font-size: 1rem; }
-.ov-patchnotes-site ul { margin: 0; padding-left: 20px; }
-.ov-infobox, .ov-timeline { margin: 26px 0; padding: 16px 18px; border: 1px solid var(--border, #1f2d47); border-radius: 12px; background: var(--bg-card, #131c2e); }
-.ov-infobox dl { margin: 0; }
-.ov-infobox-row { display: flex; gap: 10px; padding: 4px 0; border-top: 1px solid var(--border, #1f2d47); }
-.ov-infobox-key { color: var(--text-muted, #7386a3); }
-.ov-infobox-value { margin: 0; }
-.ov-timeline ol { margin: 0; padding-left: 20px; }
-.ov-timeline-when { color: var(--text-muted, #7386a3); font-variant-numeric: tabular-nums; }
-.ov-timeline-text { margin-left: 8px; }
-
/* The contents: a rail beside the text on a computer, a row of chips under the title on a phone. */
/* The contents: a rail beside the text on a computer, a row of chips under the title on a phone. */
.toc-chips { display: flex; gap: 8px; overflow-x: auto; margin: 14px 0 4px; padding-bottom: 4px; scrollbar-width: thin; }
.toc-chips { display: flex; gap: 8px; overflow-x: auto; margin: 14px 0 4px; padding-bottom: 4px; scrollbar-width: thin; }
.toc-chip { flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 12px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; color: var(--text-secondary, #96a7c2); font-size: .88rem; text-decoration: none; white-space: nowrap; }
.toc-chip { flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 12px; border: 1px solid var(--border, #1f2d47); border-radius: 999px; color: var(--text-secondary, #96a7c2); font-size: .88rem; text-decoration: none; white-space: nowrap; }
@media (min-width: 1100px) { .toc-chips { display: none; } }
@media (min-width: 1100px) { .toc-chips { display: none; } }
-/* ── Devblog look (Opus, 11-09: "Rust-style devblogs… named updates with title art… epic") ── */
-/* The hero is a banner: its title art fills it (a dark wash keeps the words readable), or an accent wash without art. The
- kicker is a pill beside the date, and the title is the biggest thing on the page. */
-.ov-hero { position: relative; isolation: isolate; overflow: hidden; display: grid; gap: 10px; margin: 0 0 28px; padding: clamp(22px, 4vw, 40px);
- border-radius: 22px; border: 1px solid var(--border, #1f2d47);
- background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent, #3b82f6) 34%, transparent), transparent 62%),
- linear-gradient(160deg, color-mix(in srgb, var(--accent, #3b82f6) 14%, var(--bg-card, #131c2e)), var(--bg-card, #131c2e)); }
-.ov-hero .ov-hero-art { position: absolute; inset: 0; z-index: -1; margin: 0; }
-.ov-hero .ov-hero-art img { width: 100%; height: 100%; object-fit: cover; }
-.ov-hero .ov-hero-art::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(4, 8, 18, .18) 0%, rgba(4, 8, 18, .78) 100%); }
-.ov-hero:has(.ov-hero-art) { min-height: clamp(260px, 42vw, 420px); align-content: end; color: #fff; border-color: transparent; }
-.ov-hero:has(.ov-hero-art) .ov-hero-body p { color: rgba(255, 255, 255, .86); }
-.ov-hero .ov-kicker { justify-self: start; display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: .78rem;
- font-weight: 750; letter-spacing: .02em; text-transform: none; color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
-.ov-hero .ov-kicker::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
-.ov-hero:has(.ov-hero-art) .ov-kicker { color: #fff; background: rgba(255, 255, 255, .18); backdrop-filter: blur(6px); }
-.ov-hero-date { font-size: .86rem; color: var(--text-muted, #7386a3); }
-.ov-hero:has(.ov-kicker) .ov-hero-date { grid-row: 1; justify-self: start; margin: 0 0 0 0; padding: 4px 0; }
-.ov-hero:has(.ov-kicker) { grid-template-columns: auto 1fr; column-gap: 10px; }
-.ov-hero:has(.ov-kicker) .ov-kicker { grid-row: 1; grid-column: 1; }
-.ov-hero:has(.ov-kicker) .ov-hero-date { grid-column: 2; }
-.ov-hero:has(.ov-kicker) .ov-hero-body { grid-column: 1 / -1; }
-.ov-hero-body > :is(h1, h2) { margin: 4px 0 10px; font-size: clamp(2rem, 1.35rem + 3.2vw, 3.4rem); line-height: 1.06; letter-spacing: -.028em; font-weight: 800; }
-.ov-hero-body > p { margin: 0; max-width: 62ch; font-size: clamp(1.02rem, .95rem + .4vw, 1.2rem); line-height: 1.55; color: var(--text-secondary, #96a7c2); }
-/* The timeline: a line with a dot per moment, the time in tabular numbers, the words beside it. */
-.ov-timeline ol { position: relative; list-style: none; margin: 4px 0 0; padding: 0; }
-.ov-timeline ol::before { content: ""; position: absolute; left: 5px; top: 10px; bottom: 10px; width: 2px; border-radius: 2px; background: color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent); }
-.ov-timeline-item { position: relative; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; padding: 7px 0 7px 24px; }
-.ov-timeline-item::before { content: ""; position: absolute; left: 0; top: 13px; width: 12px; height: 12px; border-radius: 50%; background: var(--bg-card, #131c2e); box-shadow: inset 0 0 0 3px var(--accent, #3b82f6); }
-.ov-timeline-when { min-width: 3.4em; font-weight: 700; color: var(--accent, #3b82f6); }
-.ov-timeline-text { margin-left: 0; }
-.ov-timeline-title, .ov-infobox-title { margin: 0 0 8px; font-size: 1.05rem; }
-/* A stat reads as a figure, in the accent. */
-.ov-stat-value { color: var(--accent, #3b82f6); font-size: clamp(2.2rem, 1.8rem + 2vw, 3rem); line-height: 1; }
-.ov-infobox-row { display: grid; grid-template-columns: minmax(84px, 32%) minmax(0, 1fr); gap: 12px; padding: 7px 0; }
-.ov-infobox-key { margin: 0; }
-
/* ── The block inserter (public/js/blog-editor.js): a "/" menu, a form per block with a live preview, an
/* ── The block inserter (public/js/blog-editor.js): a "/" menu, a form per block with a live preview, an
outline dragged to reorder, picture search and Make an image. Progressive only: the textarea saves. ── */
outline dragged to reorder, picture search and Make an image. Progressive only: the textarea saves. ── */
.ov-editor { margin: 6px 0 4px; }
.ov-editor { margin: 6px 0 4px; }
@@ -216,6 +216,11 @@ async function createApp(opts = {}) {
@@ -216,6 +216,11 @@ async function createApp(opts = {}) {
// ── Static assets (content-hashed ?v= → immutable) ──────
// ── Static assets (content-hashed ?v= → immutable) ──────
// This site's own pinned copy of the OpenVibe Frame's browser files (openvibe-shared/serve).
// This site's own pinned copy of the OpenVibe Frame's browser files (openvibe-shared/serve).
app.use('/shared', require('openvibe-shared/serve').handler());
app.use('/shared', require('openvibe-shared/serve').handler());
+ app.get('/css/publishing-blocks.css', (req, res) => {
+ const v = req.query.v;
+ res.type('css').set('Cache-Control', cache.assetHeaders('css/publishing-blocks.css', { hashed: !!v && v === assetVersion('css/publishing-blocks.css') }))
+ .sendFile(require.resolve('openvibe-publishing/blocks.css'));
+ });
app.use(express.static(PUBLIC_DIR, {
app.use(express.static(PUBLIC_DIR, {
index: false, redirect: false,
index: false, redirect: false,
setHeaders(res, filePath) {
setHeaders(res, filePath) {
@@ -70,7 +70,8 @@ function aiDraftForm({ blog, csrf }) {
@@ -70,7 +70,8 @@ function aiDraftForm({ blog, csrf }) {
function postForm({ blog, post, head, terms, series, csrf, message, action }) {
function postForm({ blog, post, head, terms, series, csrf, message, action }) {
const f = head ? head.fields : {};
const f = head ? head.fields : {};
const vis = post ? post.visibility : 'public';
const vis = post ? post.visibility : 'public';
- return String(h`<link rel="stylesheet" href="${asset('css/blocks.css')}">
+ return String(h`<link rel="stylesheet" href="${asset('css/publishing-blocks.css')}">
+<link rel="stylesheet" href="${asset('css/blocks.css')}">
${flash(message)}
${flash(message)}
<form method="post" action="${action}" class="post-form">${csrfField(csrf)}
<form method="post" action="${action}" class="post-form">${csrfField(csrf)}
${head ? h`<input type="hidden" name="expectedRevision" value="${head.number}">` : ''}
${head ? h`<input type="hidden" name="expectedRevision" value="${head.number}">` : ''}
@@ -22,12 +22,13 @@ const SITE_NAME = 'OpenVibe.Blog';
@@ -22,12 +22,13 @@ const SITE_NAME = 'OpenVibe.Blog';
// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.
// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.
const SITE_SUMMARY = 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.';
const SITE_SUMMARY = 'The official OpenVibe blog and a blog for every OpenVibe member: server-rendered posts with feeds, sitemaps and a JSON representation of every post.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
+const PUBLISHING_BLOCKS_CSS = require.resolve('openvibe-publishing/blocks.css');
const hashes = new Map();
const hashes = new Map();
function assetVersion(rel) {
function assetVersion(rel) {
if (hashes.has(rel)) return hashes.get(rel);
if (hashes.has(rel)) return hashes.get(rel);
let v = 'dev';
let v = 'dev';
- try { v = crypto.createHash('sha256').update(fs.readFileSync(path.join(PUBLIC_DIR, rel))).digest('hex').slice(0, 10); } catch { /* missing asset */ }
+ try { v = crypto.createHash('sha256').update(fs.readFileSync(rel === 'css/publishing-blocks.css' ? PUBLISHING_BLOCKS_CSS : path.join(PUBLIC_DIR, rel))).digest('hex').slice(0, 10); } catch { /* missing asset */ }
hashes.set(rel, v);
hashes.set(rel, v);
return v;
return v;
}
}
@@ -226,7 +226,8 @@ function postPage(o) {
@@ -226,7 +226,8 @@ function postPage(o) {
// summary only shows when the hero has no prose of its own.
// summary only shows when the hero has no prose of its own.
const heroHtml = rendered.heroHtml || null;
const heroHtml = rendered.heroHtml || null;
const heroTitle = rendered.heroHeading || null;
const heroTitle = rendered.heroHeading || null;
- return String(h`<link rel="stylesheet" href="${asset('css/blocks.css')}">
+ return String(h`<link rel="stylesheet" href="${asset('css/publishing-blocks.css')}">
+<link rel="stylesheet" href="${asset('css/blocks.css')}">
${raw(ssr.breadcrumbsHtml(o.breadcrumbs))}
${raw(ssr.breadcrumbsHtml(o.breadcrumbs))}
<div class="post-wrap${toc ? ' has-toc' : ''}">
<div class="post-wrap${toc ? ' has-toc' : ''}">
${raw(toc ? contentsHtml(toc) : '')}
${raw(toc ? contentsHtml(toc) : '')}
@@ -29,6 +29,14 @@ const REVALIDATED = 'public, max-age=300, stale-while-revalidate=86400';
@@ -29,6 +29,14 @@ const REVALIDATED = 'public, max-age=300, stale-while-revalidate=86400';
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.headers.get('cache-control'), REVALIDATED);
assert.strictEqual(r.headers.get('cache-control'), REVALIDATED);
});
});
+ await check('the library block sheet is content-addressed and served as CSS', async () => {
+ const rel = 'css/publishing-blocks.css';
+ const r = await t.get(`/${rel}?v=${assetVersion(rel)}`);
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers.get('content-type'), /^text\/css/);
+ assert.strictEqual(r.headers.get('cache-control'), IMMUTABLE);
+ assert.match(r.text, /\.ov-infobox/);
+ });
await t.close();
await t.close();
done();
done();
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -90,6 +90,9 @@ const body = (by) => [
@@ -90,6 +90,9 @@ const body = (by) => [
await check('the published page renders the blocks: the hero art, the section, the byline, the rail', async () => {
await check('the published page renders the blocks: the hero art, the section, the byline, the rail', async () => {
await t.get(`/api/v1/posts/${post.id}/publish`, { as: alice, json: {} });
await t.get(`/api/v1/posts/${post.id}/publish`, { as: alice, json: {} });
const page = await t.get('/@alice/blocks');
const page = await t.get('/@alice/blocks');
+ const sharedCss = page.text.match(/<link rel="stylesheet" href="(\/css\/publishing-blocks\.css\?v=[0-9a-f]{10})">/);
+ assert.ok(sharedCss, 'the post links the library blocks sheet');
+ assert.ok(page.text.indexOf(sharedCss[0]) < page.text.indexOf('<link rel="stylesheet" href="/css/blocks.css?'), 'the library sheet loads before Blog placement rules');
assert.strictEqual(page.status, 200, page.text);
assert.strictEqual(page.status, 200, page.text);
assert.match(page.text, /class="ov-media ov-hero-art"/);
assert.match(page.text, /class="ov-media ov-hero-art"/);
assert.match(page.text, /id="blueprint-rework"/);
assert.match(page.text, /id="blueprint-rework"/);
@@ -58,6 +58,7 @@ const idFrom = (location) => (location.match(/\/write\/posts\/(pst_[0-9A-Z]+)/)
@@ -58,6 +58,7 @@ const idFrom = (location) => (location.match(/\/write\/posts\/(pst_[0-9A-Z]+)/)
await check('new draft, edit and publish with forms', async () => {
await check('new draft, edit and publish with forms', async () => {
const form = await t.get('/write/@eve/new', { as: eve });
const form = await t.get('/write/@eve/new', { as: eve });
assert.match(form.text, /<textarea id="body" name="body"/);
assert.match(form.text, /<textarea id="body" name="body"/);
+ assert.ok(form.text.indexOf('href="/css/publishing-blocks.css?v=') < form.text.indexOf('href="/css/blocks.css?v='), 'the editor loads shared block styles before its own');
let r = await t.get('/write/@eve/new', { as: eve, form: { _csrf: csrf, title: 'Notes without script', body: `# Heading\n\nPlain *text*. ${LONG}`, tags: 'nojs', series: 'Field notes', seriesPosition: '1', visibility: 'public', allowComments: '1' } });
let r = await t.get('/write/@eve/new', { as: eve, form: { _csrf: csrf, title: 'Notes without script', body: `# Heading\n\nPlain *text*. ${LONG}`, tags: 'nojs', series: 'Field notes', seriesPosition: '1', visibility: 'public', allowComments: '1' } });
assert.strictEqual(r.status, 303, r.text);
assert.strictEqual(r.status, 303, r.text);
postId = idFrom(r.headers.get('location'));
postId = idFrom(r.headers.get('location'));