@bettercms-ai/next 0.23.0 → 0.25.0

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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { PageMetaJson, ContentBlock, DeliveredLayout, FieldStyles, SiteSeoDefaults, BlockStyle, FieldStyle, DeliveryComponent } from '@bettercms-ai/types';
2
- export { DeliveredLayout, PageMetaJson, RESPONSIVE_CSS, SiteSeoDefaults } from '@bettercms-ai/types';
2
+ export { DeliveredLayout, LinkValue, PageMetaJson, RESPONSIVE_CSS, SiteSeoDefaults, linkHref } from '@bettercms-ai/types';
3
3
  import { Metadata } from 'next';
4
4
  import { SeoInput, SeoLocation, DeliveryForm } from '@bettercms-ai/sdk';
5
5
  export { ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
6
- import { CSSProperties, ReactElement, ElementType, ReactNode } from 'react';
6
+ import { CSSProperties, ReactElement, ReactNode, ElementType, ImgHTMLAttributes } from 'react';
7
+ import { ImageSource, ImageFormat, ImageFit } from '@bettercms-ai/image-url';
7
8
  export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
8
9
  import * as react_jsx_runtime from 'react/jsx-runtime';
9
10
 
@@ -586,12 +587,124 @@ interface BcmsBlocksProps {
586
587
  showScheduled?: boolean;
587
588
  }
588
589
 
590
+ /**
591
+ * The data `<BcmsSections>` renders from, read from the published snapshot (`bcms-content.json`
592
+ * at build time, `BCMS_CONTENT_FILE` on a server BetterCMS hosts).
593
+ *
594
+ * Server-side: in the App Router call `sectionsFor` in the page itself; in the Pages Router the
595
+ * page component also renders in the browser, where there is no snapshot, so the list must travel
596
+ * as page props or the client would draw the source order over the server's (`sectionsStaticProps`).
597
+ */
598
+
599
+ interface SectionsData {
600
+ page: string;
601
+ /** The page's published blocks. Null when there is no snapshot or the page is not in it. */
602
+ blocks: ContentBlock[] | null;
603
+ components: DeliveryComponent[];
604
+ forms: DeliveryForm[];
605
+ }
606
+ /** Which of two pages that share a slug is read. */
607
+ interface SectionsRead {
608
+ /**
609
+ * The collection page of this slug, whose list is its item TEMPLATE's: what a dynamic route
610
+ * (`blog/[slug]`) draws on every entry. Without it the read is a page of its own route, the
611
+ * listing page beside a collection included.
612
+ */
613
+ template?: boolean;
614
+ }
615
+ /** `<BcmsSections {...sectionsFor("home")}>`. Never throws; JSON-serialisable, so it can be a page prop. */
616
+ declare function sectionsFor(page: string, read?: SectionsRead): SectionsData;
617
+ /** A data function's answer with the page's sections among its props; a redirect or `notFound` as it is. */
618
+ type WithSections<R> = R extends {
619
+ props: infer P;
620
+ } ? Omit<R, "props"> & {
621
+ props: Awaited<P> & {
622
+ bcmsSections: SectionsData;
623
+ };
624
+ } : R;
625
+ /**
626
+ * Pages Router: `export const getStaticProps = sectionsStaticProps("home")`, then
627
+ * `<BcmsSections {...bcmsSections}>` in a page that takes `{ bcmsSections }`.
628
+ *
629
+ * A page with a data function of its own passes it in: `sectionsStaticProps("home", pageStaticProps)`
630
+ * calls it with the same context and adds `bcmsSections` to the props it answers. Whatever else it
631
+ * answers (`revalidate`, a redirect, `notFound`) is the page's own and is returned unchanged.
632
+ *
633
+ * A dynamic route passes `{ template: true }` last: `sectionsStaticProps("blog", pageStaticProps,
634
+ * { template: true })` reads the collection page's list, which every entry route draws.
635
+ */
636
+ declare function sectionsStaticProps(page: string, own?: undefined, read?: SectionsRead): () => Promise<{
637
+ props: {
638
+ bcmsSections: SectionsData;
639
+ };
640
+ }>;
641
+ declare function sectionsStaticProps<C, R>(page: string, own: (context: C) => R | Promise<R>, read?: SectionsRead): (context: C) => Promise<WithSections<R>>;
642
+ /** The same merge for `getServerSideProps`. */
643
+ declare const sectionsServerSideProps: typeof sectionsStaticProps;
644
+
645
+ /**
646
+ * <BcmsSections> — a code-built page's sections, in the order its CMS page lists them.
647
+ *
648
+ * <BcmsSections {...sectionsFor("home")}>
649
+ * <BcmsSection bind="hero"><Hero /></BcmsSection>
650
+ * <BcmsSection bind="features">{(root) => <Features rootProps={root} />}</BcmsSection>
651
+ * </BcmsSections>
652
+ *
653
+ * Each section the page can draw is a `<BcmsSection>` child, named by its placement's bind: what is
654
+ * inside is the site's own and is rendered as written. The CMS page's block list decides the rest:
655
+ *
656
+ * - a block bound to a section renders it at the block's position, stamped (below);
657
+ * - a block with no bind (a section added from the library) renders through `<BcmsBlocks>`;
658
+ * - a section no block names: `code:<bind>` always renders where the source has it; a bare `<bind>`
659
+ * does too until the list names one of this page's sections, and from then on it is a removed one.
660
+ *
661
+ * With `blocks` null (a build without a snapshot) every child renders in source order, untouched.
662
+ *
663
+ * THE STAMP goes on the section's own root, never on a box of ours that changes layout:
664
+ * - one DOM element (`<section>`): `data-bcms-block`, `data-bcms-style` and the block's style are
665
+ * cloned onto it, its own style first;
666
+ * - a function child is handed those props and puts them on its root itself: the way a COMPONENT
667
+ * section takes its style, since a component may not pass unknown props on;
668
+ * - a component or several children: a box of ours carries the stamp. `display:contents` while
669
+ * the block has no style, so the page lays out as written; `display:flow-root` with the style on
670
+ * it once the block has one, because a box that draws nothing cannot show a padding. A styled
671
+ * box is ONE block box: in a flex or grid parent its children are one item, and their margins
672
+ * stop collapsing with the neighbours'. The function-child form has no box and no such limit.
673
+ *
674
+ * Server-safe (no hooks, no "use client"): a Server Component in the App Router, and plain React
675
+ * in the Pages Router, where `blocks` must come through page props so the client renders the same
676
+ * list (`sectionsStaticProps`). Twin: BcmsSections.astro in @bettercms-ai/astro; the order is
677
+ * `planSections` in @bettercms-ai/types.
678
+ */
679
+
680
+ /** What a function child receives: spread it on the section's root element. Empty when no block places the section. */
681
+ type SectionRootProps = Partial<SectionRootAttrs> & {
682
+ "data-bcms-sections"?: string;
683
+ };
684
+ interface BcmsSectionProps {
685
+ /** The placement's bind (`hero`), or `code:<bind>` for a section the CMS did not hold when the page was wrapped. */
686
+ bind: string;
687
+ children: ReactNode | ((root: SectionRootProps) => ReactNode);
688
+ }
689
+ interface BcmsSectionsProps {
690
+ /** The CMS page's slug: `home`, `services`. */
691
+ page: string;
692
+ /** The page's block list: `sectionsFor(slug).blocks`, or a draft read. Null or absent renders the source order. */
693
+ blocks?: ContentBlock[] | null;
694
+ /** Component definitions and forms for the sections `<BcmsBlocks>` draws. */
695
+ components?: DeliveryComponent[];
696
+ forms?: DeliveryForm[];
697
+ /** The instant block windows are read against. Defaults to the render time. */
698
+ now?: Date;
699
+ children?: ReactNode;
700
+ }
701
+
589
702
  interface BcmsFieldProps {
590
703
  /** Field path in the editor grammar, e.g. `title` or `facts[2].label`. */
591
704
  path: string;
592
705
  /** Kind hint the editor's reflection branches on. Omit for plain text/number. */
593
706
  kind?: "richtext" | "image";
594
- /** Wrapper element. Defaults to a `display:contents` span. */
707
+ /** Wrapper element. Without it: no wrapper around a single DOM element, a `display:contents` span around anything else. */
595
708
  as?: ElementType;
596
709
  children: ReactNode;
597
710
  }
@@ -606,6 +719,32 @@ interface BcmsButtonProps {
606
719
  }
607
720
  declare function BcmsButton({ path, value, className }: BcmsButtonProps): react_jsx_runtime.JSX.Element | null;
608
721
 
722
+ /** What delivery adds to an image value beside its url: the facts this component reads. */
723
+ type ImageFacts = {
724
+ altText?: string | null;
725
+ alt?: string | null;
726
+ width?: number | null;
727
+ height?: number | null;
728
+ focalX?: number | null;
729
+ focalY?: number | null;
730
+ };
731
+ interface BcmsImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet" | "width" | "height"> {
732
+ src: ImageSource | (Exclude<ImageSource, string> & ImageFacts);
733
+ /** The field this image renders, in the editor grammar (`cover`, `hero.image`). */
734
+ path?: string;
735
+ width?: number;
736
+ height?: number;
737
+ format?: ImageFormat;
738
+ quality?: number;
739
+ fit?: ImageFit;
740
+ /** Focal point override, 0–1. Normally omitted: the value and the asset carry their own. */
741
+ focalX?: number | null;
742
+ focalY?: number | null;
743
+ /** Media host for a `src` that is only an asset id. Defaults to the platform's. */
744
+ baseUrl?: string;
745
+ }
746
+ declare function BcmsImage({ src, path, alt, width, height, format, quality, fit, sizes, loading, focalX, focalY, baseUrl, style, ...rest }: BcmsImageProps): react_jsx_runtime.JSX.Element;
747
+
609
748
  /** A `change` frame off the live stream. */
610
749
  interface BcmsChangeEvent<T = unknown> {
611
750
  type: "entry" | "page";
@@ -687,4 +826,4 @@ interface VisualEditingProps {
687
826
  studioUrl?: string;
688
827
  }
689
828
 
690
- export { type BcmsBlocksProps, BcmsButton, type BcmsButtonProps, BcmsDraftMarker, BcmsField, type BcmsFieldProps, type BcmsFormProps, type BcmsLiveProps, type BetterCMSConfig, type BetterCMSEntry, BetterCMSError, type BetterCMSLiveConfig, type BetterCMSNext, type BetterCMSPage, type ContentSnapshot, type ContentSource, DEFAULT_SNAPSHOT_FILE, type DraftModeRouteConfig, type EntryList, type FieldAttrs, type GetEntryOptions, type ListEntriesOptions, type LlmsModel, type LlmsModelField, type LlmsTxtOptions, type LlmsTxtRouteConfig, PREVIEW_TOKEN_COOKIE, type ReadOptions, type RevalidateRouteConfig, SNAPSHOT_VERSION, type SectionColumnAttrs, type SectionRootAttrs, type SectionRootBlock, type SnapshotForms, type SnapshotPage, type VisualEditingProps, buildMetadata, buildSnapshot, createBetterCMS, createDisableDraftRoute, createDraftModeRoute, createRevalidateRoute, defineBetterCMSLive, fetchModels, fieldAttrs, fieldStyleOf, generateLlmsTxt, getComponent, getContent, getForm, getPage, getPreviewToken, isDraftEnabled, llmsTxtRoute, parseSnapshot, readComponents, readForms, readPages, readSnapshot, sectionColumnAttrs, sectionRootAttrs, serializeSnapshot, writeSnapshot };
829
+ export { type BcmsBlocksProps, BcmsButton, type BcmsButtonProps, BcmsDraftMarker, BcmsField, type BcmsFieldProps, type BcmsFormProps, BcmsImage, type BcmsImageProps, type BcmsLiveProps, type BcmsSectionProps, type BcmsSectionsProps, type BetterCMSConfig, type BetterCMSEntry, BetterCMSError, type BetterCMSLiveConfig, type BetterCMSNext, type BetterCMSPage, type ContentSnapshot, type ContentSource, DEFAULT_SNAPSHOT_FILE, type DraftModeRouteConfig, type EntryList, type FieldAttrs, type GetEntryOptions, type ListEntriesOptions, type LlmsModel, type LlmsModelField, type LlmsTxtOptions, type LlmsTxtRouteConfig, PREVIEW_TOKEN_COOKIE, type ReadOptions, type RevalidateRouteConfig, SNAPSHOT_VERSION, type SectionColumnAttrs, type SectionRootAttrs, type SectionRootBlock, type SectionRootProps, type SectionsData, type SectionsRead, type SnapshotForms, type SnapshotPage, type VisualEditingProps, buildMetadata, buildSnapshot, createBetterCMS, createDisableDraftRoute, createDraftModeRoute, createRevalidateRoute, defineBetterCMSLive, fetchModels, fieldAttrs, fieldStyleOf, generateLlmsTxt, getComponent, getContent, getForm, getPage, getPreviewToken, isDraftEnabled, llmsTxtRoute, parseSnapshot, readComponents, readForms, readPages, readSnapshot, sectionColumnAttrs, sectionRootAttrs, sectionsFor, sectionsServerSideProps, sectionsStaticProps, serializeSnapshot, writeSnapshot };
package/dist/index.js CHANGED
@@ -222,6 +222,7 @@ function buildMetadata(page, siteDefaults = {}, location = {}) {
222
222
 
223
223
  // src/index.ts
224
224
  import { RESPONSIVE_CSS } from "@bettercms-ai/types";
225
+ import { linkHref } from "@bettercms-ai/types";
225
226
 
226
227
  // src/section-root.ts
227
228
  import { blockStyleToColumnCss, blockStyleToCss, hideOnAttr } from "@bettercms-ai/types";
@@ -620,10 +621,43 @@ function getComponent(idOrSlug, path) {
620
621
  );
621
622
  }
622
623
 
624
+ // src/sections-read.ts
625
+ import { readPublishedContent } from "@bettercms-ai/sdk";
626
+ function pageRow(pages, page, read) {
627
+ const rows = (Array.isArray(pages) ? pages : []).filter((p) => p?.slug === page);
628
+ const template = rows.find((row) => row.template === true);
629
+ return read?.template ? template : rows.find((row) => row.template !== true) ?? rows[0];
630
+ }
631
+ function sectionsFor(page, read) {
632
+ const snapshot = readPublishedContent();
633
+ const row = pageRow(snapshot?.pages, page, read);
634
+ const blocks = row?.blocks ?? row?.blockJson;
635
+ return {
636
+ page,
637
+ blocks: Array.isArray(blocks) ? blocks : null,
638
+ components: Array.isArray(snapshot?.components) ? snapshot.components : [],
639
+ forms: Array.isArray(snapshot?.forms) ? snapshot.forms : []
640
+ };
641
+ }
642
+ function sectionsStaticProps(page, own, read) {
643
+ return async (context) => {
644
+ const answer = own ? await own(context) : { props: {} };
645
+ if (!answer || typeof answer !== "object" || !("props" in answer)) return answer;
646
+ return { ...answer, props: { ...await answer.props, bcmsSections: sectionsFor(page, read) } };
647
+ };
648
+ }
649
+ var sectionsServerSideProps = sectionsStaticProps;
650
+
623
651
  // src/field.tsx
652
+ import { Children, cloneElement, isValidElement } from "react";
624
653
  import { jsx } from "react/jsx-runtime";
625
654
  function BcmsField({ path, kind, as, children }) {
626
655
  const Tag = as ?? "span";
656
+ const only = as ? [] : Children.toArray(children);
657
+ const sole = only.length === 1 && isValidElement(only[0]) && typeof only[0].type === "string" ? only[0] : null;
658
+ if (sole && sole.props["data-bcms-field"] === void 0 && sole.props["data-bcms-layout-field"] === void 0) {
659
+ return cloneElement(sole, { "data-bcms-field": path, "data-bcms-kind": kind });
660
+ }
627
661
  return /* @__PURE__ */ jsx(
628
662
  Tag,
629
663
  {
@@ -661,6 +695,78 @@ function BcmsButton({ path, value, className }) {
661
695
  );
662
696
  }
663
697
 
698
+ // src/image.tsx
699
+ import imageUrlBuilder, {
700
+ focalInCrop,
701
+ normalizeImageCrop,
702
+ normalizeImageFocal,
703
+ objectPosition
704
+ } from "@bettercms-ai/image-url";
705
+ import { jsx as jsx3 } from "react/jsx-runtime";
706
+ var LADDER = [480, 768, 1080, 1440, 1920];
707
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
708
+ function BcmsImage({
709
+ src,
710
+ path,
711
+ alt,
712
+ width,
713
+ height,
714
+ format = "webp",
715
+ quality,
716
+ fit,
717
+ sizes,
718
+ loading = "lazy",
719
+ focalX,
720
+ focalY,
721
+ baseUrl,
722
+ style,
723
+ ...rest
724
+ }) {
725
+ const asset = typeof src === "object" && src !== null ? src : {};
726
+ const explicit = focalX != null || focalY != null;
727
+ const stored = normalizeImageFocal(asset.focal) ?? (num(asset.focalX) != null || num(asset.focalY) != null ? { x: num(asset.focalX) ?? 0.5, y: num(asset.focalY) ?? 0.5 } : null);
728
+ const inWindow = stored ? focalInCrop(stored, normalizeImageCrop(asset.crop)) : null;
729
+ const fx = explicit ? focalX : inWindow?.x;
730
+ const fy = explicit ? focalY : inWindow?.y;
731
+ const hasFocal = fx != null || fy != null;
732
+ const builder = imageUrlBuilder({ baseUrl });
733
+ const make = () => {
734
+ let b = builder(src);
735
+ if (width) b = b.width(width);
736
+ if (height) b = b.height(height);
737
+ if (format) b = b.format(format);
738
+ if (quality) b = b.quality(quality);
739
+ if (fit) b = b.fit(fit);
740
+ if (hasFocal) b = b.focal(fx ?? 0.5, fy ?? 0.5);
741
+ return b;
742
+ };
743
+ const src1x = make().url();
744
+ const own = num(asset.width);
745
+ const ladder = own ? [.../* @__PURE__ */ new Set([...LADDER.filter((w) => w < own), Math.min(own, LADDER[LADDER.length - 1])])] : LADDER;
746
+ const srcSet = sizes && !width ? ladder.map((w) => `${make().width(w).url()} ${w}w`).join(", ") : `${src1x} 1x, ${make().dpr(2).url()} 2x`;
747
+ const merged = {
748
+ ...width ? { width: `${width}px`, maxWidth: "100%", height: "auto" } : {},
749
+ ...hasFocal ? { objectPosition: objectPosition({ x: fx, y: fy }) } : {},
750
+ ...style
751
+ };
752
+ return /* @__PURE__ */ jsx3(
753
+ "img",
754
+ {
755
+ src: src1x,
756
+ srcSet,
757
+ alt: alt ?? (typeof asset.altText === "string" ? asset.altText : typeof asset.alt === "string" ? asset.alt : ""),
758
+ "data-bcms-field": path,
759
+ "data-bcms-kind": path ? "image" : void 0,
760
+ width,
761
+ height,
762
+ sizes,
763
+ loading,
764
+ style: Object.keys(merged).length > 0 ? merged : void 0,
765
+ ...rest
766
+ }
767
+ );
768
+ }
769
+
664
770
  // src/live-config.ts
665
771
  function defineBetterCMSLive(config) {
666
772
  const base = config.apiUrl.replace(/\/+$/, "");
@@ -705,6 +811,7 @@ export {
705
811
  BcmsButton,
706
812
  BcmsDraftMarker,
707
813
  BcmsField,
814
+ BcmsImage,
708
815
  BetterCMSError,
709
816
  DEFAULT_SNAPSHOT_FILE,
710
817
  PREVIEW_TOKEN_COOKIE,
@@ -731,6 +838,7 @@ export {
731
838
  imageValueObjectPosition,
732
839
  imageValueUrl,
733
840
  isDraftEnabled,
841
+ linkHref,
734
842
  llmsTxtRoute,
735
843
  parseSnapshot,
736
844
  readComponents,
@@ -740,6 +848,9 @@ export {
740
848
  resolveSeo2 as resolveSeo,
741
849
  sectionColumnAttrs,
742
850
  sectionRootAttrs,
851
+ sectionsFor,
852
+ sectionsServerSideProps,
853
+ sectionsStaticProps,
743
854
  serializeSnapshot,
744
855
  writeSnapshot
745
856
  };