@accelflow/space-content 0.1.0-beta.3 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +34 -11
  2. package/package.json +10 -4
  3. package/render.mjs +12 -3
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
- # Space build content SDK
1
+ # Accelflow Space Headless CMS SDK
2
2
 
3
- Node 24+ build-time content synchronization for Space Headless CMS, with optional browser-safe table-of-contents presentation. Licensed under MIT.
3
+ The official Headless CMS SDK for [Accelflow Space](https://space.accelflow.jp/en/), developed and maintained by [Accelflow Inc.](https://accelflow.jp/).
4
+
5
+ Create, review, and publish content with your team in Space, then bring it into your own website with Vite, Next.js, or another build system. The SDK synchronizes published content at build time and provides optional rendering, table-of-contents, and author-profile utilities. Requires Node.js 24 or later. Licensed under MIT.
6
+
7
+ - [Service website](https://space.accelflow.jp/en/)
8
+ - [Headless CMS use case](https://space.accelflow.jp/en/use-cases/headless-cms/)
9
+ - [Company website — Accelflow Inc.](https://accelflow.jp/)
10
+ - [Source code](https://github.com/Accelflow/space-content-sdk) · [Report an issue](https://github.com/Accelflow/space-content-sdk/issues)
4
11
 
5
12
  The beta package is available on npm.
6
13
 
@@ -64,7 +71,7 @@ repository; the tests here exercise the client with synthetic protocol fixtures.
64
71
 
65
72
  ## Full framework examples
66
73
 
67
- See [Vite and Next.js examples](examples/README.md) for complete static builds,
74
+ See [Vite and Next.js examples](https://github.com/Accelflow/space-content-sdk/tree/main/examples#readme) for complete static builds,
68
75
  CI cache handling, HTML rendering, asset headers and deployment boundaries.
69
76
 
70
77
  ## Table of contents
@@ -75,7 +82,7 @@ The browser-safe entry is separate from the authenticated Node client:
75
82
  import { mountTableOfContents } from '@accelflow/space-content/toc';
76
83
  import '@accelflow/space-content/toc.css';
77
84
 
78
- const cleanup = mountTableOfContents(articleElement, { locale: 'ja' });
85
+ const cleanup = mountTableOfContents(articleElement, { locale: 'en' });
79
86
  // Call cleanup when replacing the article or unmounting the component.
80
87
  ```
81
88
 
@@ -85,14 +92,14 @@ In Next.js, use this browser entry in a client component effect and return the c
85
92
 
86
93
  ## Release preparation
87
94
 
88
- See [RELEASING.md](RELEASING.md) for packaging checks, initial publication and OIDC setup.
95
+ See [RELEASING.md](https://github.com/Accelflow/space-content-sdk/blob/main/RELEASING.md) for packaging checks, initial publication and OIDC setup.
89
96
 
90
97
  ## JSON and HTML rendering
91
98
 
92
99
  ```js
93
100
  import {renderArticleBody} from '@accelflow/space-content/render';
94
101
  const html = renderArticleBody(article.body, {
95
- assets: content.assets, locale: 'ja', pdfMode: 'link',
102
+ assets: content.assets, locale: 'en', pdfMode: 'link',
96
103
  renderers: {paragraph: ({children}) => `<p>${children}</p>`},
97
104
  });
98
105
  ```
@@ -132,12 +139,12 @@ import '@accelflow/space-content/profile.css';
132
139
 
133
140
  const profile = createAuthorProfile(content.articles, {
134
141
  origin: 'https://your-site.example',
135
- locale: 'ja',
136
- url: '/ja/blog/authors/editorial-team/',
137
- articleHref: article => `/ja/blog/${article.slug}/`,
142
+ locale: 'en',
143
+ url: '/en/blog/authors/editorial-team/',
144
+ articleHref: article => `/en/blog/${article.slug}/`,
138
145
  });
139
146
  const html = renderAuthorProfile(profile, {
140
- labels: {about: '編集部について', articles: '編集部の記事'},
147
+ labels: {about: 'About the editorial team', articles: 'Articles by the editorial team'},
141
148
  });
142
149
  ```
143
150
 
@@ -161,7 +168,7 @@ Draft articles, other locales and articles with a different author are excluded.
161
168
 
162
169
  When a Space workspace has a CMS URL, the delivery API rewrites article links
163
170
  before returning HTML or JSON. Use the article base URL (for example,
164
- `https://example.com/ja/blog/`). Workspace references use that base plus the
171
+ `https://example.com/en/blog/`). Workspace references use that base plus the
165
172
  published page-tree path. Links with the same hostname become root-relative;
166
173
  other HTTP(S) links retain an absolute URL and carry `target="_blank"` with
167
174
  `rel="noopener noreferrer"`. The renderer preserves these safe external targets.
@@ -207,3 +214,19 @@ paragraphs and content outside this wrapper are unchanged. Customize the gap:
207
214
  ```css
208
215
  .space-content { --space-content-paragraph-gap: 0.75em; }
209
216
  ```
217
+
218
+ ### Image dimensions
219
+
220
+ `renderArticleBody` retains independently specified `width` and `height` for JSON
221
+ image nodes and raster attachment nodes. Each dimension must be an integer from
222
+ 1 through 10000 pixels. Missing dimensions retain the browser's automatic sizing.
223
+ Images keep `max-width:100%` so they fit narrower article containers.
224
+
225
+ For HTML bodies, the sanitizer retains only bounded pixel width/height, image
226
+ `width:100%`, `height:auto`, and `max-width:100%` on the image/figure elements used
227
+ by Space exports. Other inline CSS and executable attributes remain disallowed.
228
+
229
+ Run `npm run verify:local` for the standard local verification. This runs the
230
+ SDK tests, including Build API transfer-to-render image regressions for JSON and
231
+ HTML, then verifies the packed public exports and Vite/Next static builds, and
232
+ audits dependencies. The same image regressions run against the packed SDK.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelflow/space-content",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -29,7 +29,9 @@
29
29
  "scripts": {
30
30
  "test": "node --test test/*.test.mjs",
31
31
  "prepack": "npm test",
32
- "verify:package": "node scripts/verify-package.mjs"
32
+ "verify:package": "node scripts/verify-package.mjs",
33
+ "audit:dependencies": "npm audit --audit-level=moderate",
34
+ "verify:local": "npm run verify:package && npm run audit:dependencies"
33
35
  },
34
36
  "files": [
35
37
  "node.mjs",
@@ -55,12 +57,16 @@
55
57
  "bugs": {
56
58
  "url": "https://github.com/Accelflow/space-content-sdk/issues"
57
59
  },
58
- "homepage": "https://github.com/Accelflow/space-content-sdk#readme",
60
+ "homepage": "https://space.accelflow.jp/en/",
59
61
  "publishConfig": {
60
62
  "access": "public",
61
63
  "tag": "beta"
62
64
  },
63
65
  "dependencies": {
64
- "sanitize-html": "^2.18.0"
66
+ "sanitize-html": "2.18.1"
67
+ },
68
+ "author": {
69
+ "name": "Accelflow Inc.",
70
+ "url": "https://accelflow.jp/"
65
71
  }
66
72
  }
package/render.mjs CHANGED
@@ -4,6 +4,11 @@ export const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, c =>
4
4
  const linkTarget = attrs => attrs?.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
5
5
  const hash = /^[a-f0-9]{64}$/;
6
6
  const raster = new Set(['image/png','image/jpeg','image/gif','image/webp','image/avif']);
7
+ const imagePixels = /^(?:[1-9]\d{0,3}|10000)px$/;
8
+ function imageStyle(attrs) {
9
+ const dimensions=['width','height'].filter(key=>Number.isSafeInteger(attrs[key])&&attrs[key]>0&&attrs[key]<=10000).map(key=>`${key}:${attrs[key]}px`);
10
+ return dimensions.length?` style="max-width:100%;${dimensions.join(';')}"`:'';
11
+ }
7
12
  function assetsIndex(assets = []) {
8
13
  const ids = new Map(), paths = new Map();
9
14
  for (const a of assets) {
@@ -26,12 +31,16 @@ export function sanitizeArticleHtml(html, {assets = [], pdfMode = 'link'} = {})
26
31
  allowedTags: [...sanitizeHtml.defaults.allowedTags,'img','details','summary','nav',...(pdfMode==='embed'?['iframe']:[])],
27
32
  allowedAttributes: {
28
33
  '*':['id','class'], a:['href','title','data-level','download','target','rel'],
29
- img:['src','alt','title','width','height','loading','referrerpolicy'],
34
+ img:['src','alt','title','width','height','loading','referrerpolicy','style'], figure:['style'],
30
35
  nav:['data-table-of-contents','data-max-level','aria-label'], details:['open','data-space-toc'],
31
36
  li:['data-level','data-checked'], ol:['start'], td:['colspan','rowspan'], th:['colspan','rowspan','scope'],
32
37
  iframe:['src','title','sandbox','loading','referrerpolicy'],
33
38
  },
34
39
  allowedClasses:{'*':['*']},
40
+ allowedStyles:{
41
+ img:{width:[imagePixels,/^100%$/],height:[imagePixels,/^auto$/],'max-width':[/^100%$/]},
42
+ figure:{width:[imagePixels],'max-width':[/^100%$/]},
43
+ },
35
44
  allowedSchemes:['https','http','mailto','tel'], allowedSchemesByTag:{img:['https'],iframe:[]}, allowProtocolRelative:false,
36
45
  transformTags: {
37
46
  a: (tagName, attrs) => {const {target,rel,...rest}=attrs;return {tagName,attribs:{...rest,...(target==='_blank'&&/^https?:\/\//i.test(safeUrl(attrs.href))?{target:'_blank',rel:'noopener noreferrer'}:{})}};},
@@ -69,11 +78,11 @@ export function renderArticleBody(body, options = {}) {
69
78
  for(const mark of Array.isArray(n.marks)?n.marks:[]) {const tag={bold:'strong',italic:'em',underline:'u',strike:'s',code:'code'}[mark?.type];if(tag)html=wrap(tag,html);else if(mark?.type==='link'){const href=safeUrl(mark.attrs?.href);if(href)html=wrap('a',html,` href="${e(href)}"${linkTarget(mark.attrs)}`);}}
70
79
  } else if(n.type==='heading') html=wrap('h'+(Number.isInteger(a.level)&&a.level>=1&&a.level<=6?a.level:2),children,` id="${e(anchors.get(n))}"`);
71
80
  else if(n.type==='tableOfContents') html=renderTableOfContents(headings,{locale:options.locale??'ja',maxLevel:a.maxLevel===2?2:3});
72
- else if(n.type==='image') {const src=safeUrl(a.src,true);html=src?`<img src="${e(src)}" alt="${e(a.alt)}" loading="lazy" referrerpolicy="no-referrer">`:'';}
81
+ else if(n.type==='image') {const src=safeUrl(a.src,true);html=src?`<img src="${e(src)}" alt="${e(a.alt)}" loading="lazy" referrerpolicy="no-referrer"${imageStyle(a)}>`:'';}
73
82
  else if(n.type==='attachment') {
74
83
  const asset=ids.get(a.attachmentId);if(!asset)throw Error('attachment_not_in_manifest');
75
84
  const src='/space-assets/'+asset.hash, label=e(asset.filename);
76
- html=raster.has(asset.contentType)?`<img src="${src}" alt="${label}" loading="lazy">`:`<a href="${src}" download="${label}">${label}</a>`;
85
+ html=raster.has(asset.contentType)?`<img src="${src}" alt="${label}" loading="lazy"${imageStyle(a)}>`:`<a href="${src}" download="${label}">${label}</a>`;
77
86
  if(asset.contentType==='application/pdf'&&options.pdfMode==='embed')html=`<iframe class="space-pdf" src="${src}" title="${label}" sandbox="" loading="lazy" referrerpolicy="no-referrer"></iframe>`+html;
78
87
  } else if(n.type==='externalLinkCard') {const href=safeUrl(a.href);html=href?wrap('a',e(a.title||href),` href="${e(href)}"${linkTarget(a)}`):e(a.title);}
79
88
  else if(n.type==='articleReference') {const target=options.resolveArticle?.(a);const href=safeUrl(target?.href);const label=e(a.labelMode==='custom'?a.customLabel:target?.title??a.fallbackTitle??a.title??'');html=href?wrap('a',label,` href="${e(href)}"`):label;}