@sevenfold/setto-client 0.34.0 → 0.36.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.
@@ -21461,8 +21461,9 @@ class ThemeStore {
21461
21461
  }
21462
21462
  const PAGES_MANIFEST_VERSION = 1;
21463
21463
  const PAGES_PATH = "src/content/pages.json";
21464
- const BUILTIN_SECTION_TYPES = ["text"];
21464
+ const BUILTIN_SECTION_TYPES = ["text", "blog", "blog-latest"];
21465
21465
  const ID = /^[a-z0-9][a-z0-9-]{0,62}$/;
21466
+ const POST_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21466
21467
  const PATH = /^\/(?:[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*)?$/;
21467
21468
  function isRecord$1(value) {
21468
21469
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -21529,6 +21530,15 @@ function parseManifest(input) {
21529
21530
  sections
21530
21531
  });
21531
21532
  });
21533
+ const blogs = pages.flatMap(
21534
+ (p, i) => p.sections.flatMap((s, j) => s.type === "blog" ? [{ page: p, at: `pages[${i}].sections[${j}]` }] : [])
21535
+ );
21536
+ if (blogs.length > 1) {
21537
+ issues.push({ at: blogs[1].at, message: "a site has one blog; another blog section is already on a page" });
21538
+ }
21539
+ for (const blog of blogs) {
21540
+ if (blog.page.path === "/") issues.push({ at: blog.at, message: "the blog cannot be the front page" });
21541
+ }
21532
21542
  const menu = parseMenu(input.menu, "menu", issues);
21533
21543
  const footer2 = parseMenu(input.footer, "footer", issues);
21534
21544
  if (issues.length > 0) return { ok: false, issues };
@@ -21572,7 +21582,23 @@ function normalisePath(path2) {
21572
21582
  }
21573
21583
  function pageForPath(manifest, path2) {
21574
21584
  const wanted = normalisePath(path2);
21575
- return manifest.pages.find((p) => p.path === wanted) ?? null;
21585
+ return manifest.pages.find((p) => p.path === wanted) ?? (blogSlugForPath(manifest, path2) ? blogPage(manifest).page : null);
21586
+ }
21587
+ function blogPage(manifest) {
21588
+ for (const page of manifest.pages) {
21589
+ const section = page.sections.find((s) => s.type === "blog");
21590
+ if (section) return { page, section };
21591
+ }
21592
+ return null;
21593
+ }
21594
+ function blogSlugForPath(manifest, path2) {
21595
+ const blog = blogPage(manifest);
21596
+ if (!blog || blog.page.path === "/") return null;
21597
+ const wanted = normalisePath(path2);
21598
+ const prefix = `${blog.page.path}/`;
21599
+ if (!wanted.startsWith(prefix)) return null;
21600
+ const slug = wanted.slice(prefix.length);
21601
+ return slug.length <= 80 && POST_SLUG.test(slug) ? slug : null;
21576
21602
  }
21577
21603
  function pageById(manifest, id) {
21578
21604
  return manifest.pages.find((p) => p.id === id) ?? null;
@@ -22124,7 +22150,7 @@ function withoutCheckoutIntent() {
22124
22150
  }
22125
22151
  const FAB_SIZE = 52;
22126
22152
  const FAB_INSET = 16;
22127
- const STYLE_ID$2 = "setto-edit-layout";
22153
+ const STYLE_ID$3 = "setto-edit-layout";
22128
22154
  const HTML_CLASS = "setto-edit-mode";
22129
22155
  function useSettoDocumentLayout(active) {
22130
22156
  useEffect(() => {
@@ -22133,10 +22159,10 @@ function useSettoDocumentLayout(active) {
22133
22159
  html2.classList.add(HTML_CLASS);
22134
22160
  html2.style.setProperty("--setto-fab-size", `${FAB_SIZE}px`);
22135
22161
  html2.style.setProperty("--setto-fab-inset", `${FAB_INSET}px`);
22136
- let styleEl = document.getElementById(STYLE_ID$2);
22162
+ let styleEl = document.getElementById(STYLE_ID$3);
22137
22163
  if (!styleEl) {
22138
22164
  styleEl = document.createElement("style");
22139
- styleEl.id = STYLE_ID$2;
22165
+ styleEl.id = STYLE_ID$3;
22140
22166
  styleEl.textContent = `
22141
22167
  html.setto-edit-mode {
22142
22168
  width: 100%;
@@ -33040,7 +33066,7 @@ function formatHeadingAsSetext(node2, state) {
33040
33066
  );
33041
33067
  }
33042
33068
  function heading(node2, _, state, info) {
33043
- const rank = Math.max(Math.min(6, node2.depth || 1), 1);
33069
+ const rank2 = Math.max(Math.min(6, node2.depth || 1), 1);
33044
33070
  const tracker = state.createTracker(info);
33045
33071
  if (formatHeadingAsSetext(node2, state)) {
33046
33072
  const exit3 = state.enter("headingSetext");
@@ -33052,14 +33078,14 @@ function heading(node2, _, state, info) {
33052
33078
  });
33053
33079
  subexit2();
33054
33080
  exit3();
33055
- return value2 + "\n" + (rank === 1 ? "=" : "-").repeat(
33081
+ return value2 + "\n" + (rank2 === 1 ? "=" : "-").repeat(
33056
33082
  // The whole size…
33057
33083
  value2.length - // Minus the position of the character after the last EOL (or
33058
33084
  // 0 if there is none)…
33059
33085
  (Math.max(value2.lastIndexOf("\r"), value2.lastIndexOf("\n")) + 1)
33060
33086
  );
33061
33087
  }
33062
- const sequence = "#".repeat(rank);
33088
+ const sequence = "#".repeat(rank2);
33063
33089
  const exit2 = state.enter("headingAtx");
33064
33090
  const subexit = state.enter("phrasing");
33065
33091
  tracker.move(sequence + " ");
@@ -37714,6 +37740,18 @@ function SettoIcon({ k, icons, size = 24, className, style }) {
37714
37740
  )
37715
37741
  ] });
37716
37742
  }
37743
+ const SETTO_MEDIA_HOST = "3adyw42hy72dtzjo.public.blob.vercel-storage.com";
37744
+ const SETTO_MEDIA_BLOG_PREFIX = "/blog/";
37745
+ function isSettoMediaUrl(src) {
37746
+ if (!src) return false;
37747
+ try {
37748
+ const url = new URL(src);
37749
+ return url.protocol === "https:" && url.hostname === SETTO_MEDIA_HOST && !url.username && !url.password && url.port === "" && url.pathname.startsWith(SETTO_MEDIA_BLOG_PREFIX);
37750
+ } catch {
37751
+ return false;
37752
+ }
37753
+ }
37754
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37717
37755
  const SETTO_IMAGE_WIDTHS = [384, 640, 828, 1080, 1440, 1920, 2560];
37718
37756
  const SETTO_IMAGE_QUALITY = 75;
37719
37757
  const settoVercelImagesConfig = {
@@ -37721,12 +37759,23 @@ const settoVercelImagesConfig = {
37721
37759
  qualities: [SETTO_IMAGE_QUALITY],
37722
37760
  formats: ["image/avif", "image/webp"],
37723
37761
  localPatterns: [{ pathname: "^/images/.*$", search: "" }],
37762
+ // Vercel reads hostname and pathname as regular expressions; anchored so a
37763
+ // look-alike host cannot borrow the optimizer. Checked on a deployment
37764
+ // (R24): this form resizes, an unlisted host or path is refused with 400.
37765
+ remotePatterns: [
37766
+ {
37767
+ protocol: "https",
37768
+ hostname: `^${escapeRegExp(SETTO_MEDIA_HOST)}$`,
37769
+ pathname: `^${escapeRegExp(SETTO_MEDIA_BLOG_PREFIX)}.*$`
37770
+ }
37771
+ ],
37724
37772
  minimumCacheTTL: 2678400
37725
37773
  };
37726
37774
  const OPTIMIZABLE = /\.(jpe?g|png|webp|avif)$/i;
37727
37775
  function isOptimizable(src) {
37728
- if (!src || !src.startsWith("/images/")) return false;
37729
- return OPTIMIZABLE.test(src.split(/[?#]/)[0]);
37776
+ if (!src) return false;
37777
+ if (src.startsWith("/images/")) return OPTIMIZABLE.test(src.split(/[?#]/)[0]);
37778
+ return isSettoMediaUrl(src) && OPTIMIZABLE.test(new URL(src).pathname);
37730
37779
  }
37731
37780
  function hasNoOptimizer(hostname) {
37732
37781
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
@@ -37980,12 +38029,12 @@ function SettoImage({ srcKey, alt, className, style, ...rest }) {
37980
38029
  )
37981
38030
  ] });
37982
38031
  }
37983
- const STYLE_ID$1 = "setto-repeater-styles";
38032
+ const STYLE_ID$2 = "setto-repeater-styles";
37984
38033
  function useRepeaterStyles() {
37985
38034
  useEffect(() => {
37986
- if (document.getElementById(STYLE_ID$1)) return;
38035
+ if (document.getElementById(STYLE_ID$2)) return;
37987
38036
  const el = document.createElement("style");
37988
- el.id = STYLE_ID$1;
38037
+ el.id = STYLE_ID$2;
37989
38038
  el.textContent = `
37990
38039
  .setto-repeater-remove {
37991
38040
  opacity: 0;
@@ -38111,6 +38160,695 @@ const removeBtnStyle = {
38111
38160
  boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
38112
38161
  fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif'
38113
38162
  };
38163
+ function usePageManifest() {
38164
+ const { pageStore, config } = useSetto();
38165
+ const snap = useSyncExternalStore(
38166
+ (cb) => pageStore?.subscribe(() => cb()) ?? (() => {
38167
+ }),
38168
+ () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT,
38169
+ () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT
38170
+ );
38171
+ return pageStore ? snap.manifest : bundledManifest(config.pages);
38172
+ }
38173
+ function useSettoMenu(list2 = "menu") {
38174
+ const manifest = usePageManifest();
38175
+ return manifest ? visibleMenu(manifest, list2) : null;
38176
+ }
38177
+ function SettoPage({ pageId, path: path2, sections = {}, builtins, fallback = null }) {
38178
+ const manifest = usePageManifest();
38179
+ const { editMode } = useSetto();
38180
+ if (!manifest) {
38181
+ if (!pageId && path2 !== void 0 && path2 !== "/" && path2 !== "") return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38182
+ return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
38183
+ }
38184
+ const page = pageId ? pageById(manifest, pageId) : pageForPath(manifest, path2 ?? "/");
38185
+ if (!page) {
38186
+ if (pageId) {
38187
+ return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
38188
+ }
38189
+ return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38190
+ }
38191
+ if (page.hidden && !editMode) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
38192
+ return /* @__PURE__ */ jsx(PageBody, { page, sections, builtins, editMode, manifest });
38193
+ }
38194
+ function PageBody({
38195
+ page,
38196
+ sections,
38197
+ builtins,
38198
+ editMode,
38199
+ manifest
38200
+ }) {
38201
+ usePageTitle(page);
38202
+ const HiddenPageNote = useSettoChromeSlot("hidden-page");
38203
+ const listed = new Set(manifest.pages.flatMap((p) => p.sections.map((s) => s.id)));
38204
+ const unlisted = Object.keys(sections).filter((id) => !listed.has(id));
38205
+ const firstVisible = page.sections.find((s) => !s.hidden && (s.type || sections[s.id]))?.id;
38206
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
38207
+ page.hidden && editMode ? HiddenPageNote ? /* @__PURE__ */ jsx(HiddenPageNote, { pageId: page.id }) : /* @__PURE__ */ jsx(HiddenNote, { text: "Denne siden er skjult for besøkende." }) : null,
38208
+ page.sections.map((section) => /* @__PURE__ */ jsx(Fragment$1, { children: renderSection(section, sections, builtins, editMode, section.id === firstVisible) }, section.id)),
38209
+ unlisted.map((id) => {
38210
+ const Component = sections[id];
38211
+ return /* @__PURE__ */ jsx(Component, {}, id);
38212
+ })
38213
+ ] });
38214
+ }
38215
+ function renderSection(section, sections, builtins, editMode, first) {
38216
+ if (section.hidden) {
38217
+ return editMode ? /* @__PURE__ */ jsx(HiddenSection, { section }) : null;
38218
+ }
38219
+ if (section.type) {
38220
+ const Builtin = builtins?.[section.type] ?? BUILTIN_RENDERERS[section.type];
38221
+ return /* @__PURE__ */ jsx(Builtin, { sectionId: section.id, first });
38222
+ }
38223
+ const Component = sections[section.id];
38224
+ if (!Component) {
38225
+ warnOnce(section.id);
38226
+ return null;
38227
+ }
38228
+ return /* @__PURE__ */ jsx(Component, {});
38229
+ }
38230
+ const warned = /* @__PURE__ */ new Set();
38231
+ function warnOnce(id) {
38232
+ if (warned.has(id)) return;
38233
+ warned.add(id);
38234
+ console.warn(`[setto] section "${id}" is in pages.json but the page does not provide it`);
38235
+ }
38236
+ function HiddenSection({ section }) {
38237
+ const { pageStore } = useSetto();
38238
+ const Offered = useSettoChromeSlot("hidden-section");
38239
+ const label = section.label ?? section.id;
38240
+ if (Offered) return /* @__PURE__ */ jsx(Offered, { sectionId: section.id, label });
38241
+ return /* @__PURE__ */ jsxs("div", { "data-setto-ui": true, style: hiddenBarStyle, children: [
38242
+ /* @__PURE__ */ jsxs("span", { children: [
38243
+ "Skjult: ",
38244
+ label
38245
+ ] }),
38246
+ pageStore ? /* @__PURE__ */ jsx(
38247
+ "button",
38248
+ {
38249
+ type: "button",
38250
+ "data-setto-ui": true,
38251
+ style: hiddenBtnStyle,
38252
+ onClick: () => pageStore.apply([{ op: "show_section", sectionId: section.id }]),
38253
+ children: "Vis"
38254
+ }
38255
+ ) : null
38256
+ ] });
38257
+ }
38258
+ function HiddenNote({ text: text2 }) {
38259
+ return /* @__PURE__ */ jsx("div", { "data-setto-ui": true, style: hiddenBarStyle, children: /* @__PURE__ */ jsx("span", { children: text2 }) });
38260
+ }
38261
+ function usePageTitle(page) {
38262
+ const { t } = useTranslation();
38263
+ const original = useRef(null);
38264
+ const title = page.path === "/" ? null : t(page.titleKey);
38265
+ useEffect(() => {
38266
+ if (!title || typeof document === "undefined") return;
38267
+ original.current ??= document.title;
38268
+ const base = original.current;
38269
+ document.title = base ? `${title} · ${base}` : title;
38270
+ return () => {
38271
+ document.title = base;
38272
+ };
38273
+ }, [title]);
38274
+ }
38275
+ function SettoTextSection({ sectionId, first }) {
38276
+ const keys2 = builtinKeys(sectionId);
38277
+ const { i18n } = useTranslation();
38278
+ const { store, editMode } = useSetto();
38279
+ const intro2 = store ? store.get(keys2.intro, i18n.language) : i18n.t(keys2.intro, { defaultValue: "" });
38280
+ const Heading = first ? "h1" : "h2";
38281
+ return /* @__PURE__ */ jsx(SettoSection, { sectionId, style: textSectionStyle, children: /* @__PURE__ */ jsxs("div", { style: textInnerStyle, children: [
38282
+ /* @__PURE__ */ jsx(Heading, { style: first ? textH1Style : textH2Style, children: /* @__PURE__ */ jsx(T, { k: keys2.heading }) }),
38283
+ intro2 || editMode ? /* @__PURE__ */ jsx("p", { style: textIntroStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.intro }) }) : null,
38284
+ /* @__PURE__ */ jsx(
38285
+ SettoRepeater,
38286
+ {
38287
+ itemsKey: keys2.items,
38288
+ defaultItem: { heading: "Overskrift", text: "Tekst" },
38289
+ itemLabel: "avsnitt",
38290
+ children: (itemKey) => /* @__PURE__ */ jsx(TextItem, { sectionId, itemKey })
38291
+ }
38292
+ )
38293
+ ] }) });
38294
+ }
38295
+ function TextItem({ sectionId, itemKey }) {
38296
+ const keys2 = builtinKeys(sectionId);
38297
+ const { i18n } = useTranslation();
38298
+ const { store, editMode } = useSetto();
38299
+ const heading2 = store ? store.get(keys2.itemHeading(itemKey), i18n.language) : i18n.t(keys2.itemHeading(itemKey), { defaultValue: "" });
38300
+ return /* @__PURE__ */ jsxs("div", { style: textItemStyle, children: [
38301
+ heading2 || editMode ? /* @__PURE__ */ jsx("h3", { style: textH3Style, children: /* @__PURE__ */ jsx(T, { k: keys2.itemHeading(itemKey) }) }) : null,
38302
+ /* @__PURE__ */ jsx("p", { style: textParagraphStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.itemText(itemKey) }) })
38303
+ ] });
38304
+ }
38305
+ const BUILTIN_RENDERERS = {
38306
+ text: SettoTextSection,
38307
+ blog: SettoBlogSection,
38308
+ "blog-latest": SettoBlogLatest
38309
+ };
38310
+ const textSectionStyle = { padding: "clamp(48px, 8vw, 96px) 16px" };
38311
+ const textInnerStyle = { maxWidth: "68ch", margin: "0 auto" };
38312
+ const textH1Style = {
38313
+ fontFamily: "var(--setto-display-font, inherit)",
38314
+ fontSize: "clamp(2rem, 4vw, 2.75rem)",
38315
+ lineHeight: 1.15,
38316
+ fontWeight: 600,
38317
+ margin: "0 0 0.75em"
38318
+ };
38319
+ const textH2Style = { ...textH1Style, fontSize: "clamp(1.5rem, 3vw, 2rem)" };
38320
+ const textH3Style = {
38321
+ fontFamily: "var(--setto-display-font, inherit)",
38322
+ fontSize: "1.125rem",
38323
+ lineHeight: 1.3,
38324
+ fontWeight: 600,
38325
+ margin: "0 0 0.4em"
38326
+ };
38327
+ const textIntroStyle = { fontSize: "1.125rem", lineHeight: 1.6, opacity: 0.85, margin: "0 0 2em" };
38328
+ const textItemStyle = { margin: "0 0 1.75em" };
38329
+ const textParagraphStyle = { lineHeight: 1.7, margin: 0, whiteSpace: "pre-line" };
38330
+ const hiddenBarStyle = {
38331
+ display: "flex",
38332
+ alignItems: "center",
38333
+ justifyContent: "center",
38334
+ gap: 12,
38335
+ padding: "10px 16px",
38336
+ margin: "4px 0",
38337
+ border: "1px dashed rgba(100, 10, 255, 0.45)",
38338
+ borderRadius: 8,
38339
+ background: "rgba(100, 10, 255, 0.05)",
38340
+ color: "#3b2a66",
38341
+ font: '500 13px system-ui, -apple-system, "Segoe UI", sans-serif'
38342
+ };
38343
+ const hiddenBtnStyle = {
38344
+ border: "none",
38345
+ borderRadius: 999,
38346
+ padding: "4px 12px",
38347
+ background: "#640AFF",
38348
+ color: "#fff",
38349
+ font: '600 12px system-ui, -apple-system, "Segoe UI", sans-serif',
38350
+ cursor: "pointer"
38351
+ };
38352
+ const BLOG_ROUTE = "/blogg";
38353
+ const BLOG_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
38354
+ const BLOG_SLUG_MAX = 80;
38355
+ const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
38356
+ function isValidBlogSlug(slug) {
38357
+ return slug.length <= BLOG_SLUG_MAX && BLOG_SLUG_PATTERN.test(slug);
38358
+ }
38359
+ function byDateDesc(a, b) {
38360
+ return a.date === b.date ? a.slug.localeCompare(b.slug) : a.date < b.date ? 1 : -1;
38361
+ }
38362
+ function postsForLanguage(posts, language, fallbackLanguage) {
38363
+ const bySlug = /* @__PURE__ */ new Map();
38364
+ for (const post2 of posts) {
38365
+ const current = bySlug.get(post2.slug);
38366
+ if (!current || rank(post2, language, fallbackLanguage) < rank(current, language, fallbackLanguage)) {
38367
+ bySlug.set(post2.slug, post2);
38368
+ }
38369
+ }
38370
+ return [...bySlug.values()].sort(byDateDesc);
38371
+ }
38372
+ function postForSlug(posts, slug, language, fallbackLanguage) {
38373
+ return postsForLanguage(posts.filter((p) => p.slug === slug), language, fallbackLanguage)[0] ?? null;
38374
+ }
38375
+ function rank(post2, language, fallbackLanguage) {
38376
+ if (post2.locale === language) return 0;
38377
+ if (post2.locale === fallbackLanguage) return 1;
38378
+ return 2;
38379
+ }
38380
+ function formatPostDate(date, language) {
38381
+ const parsed = /* @__PURE__ */ new Date(`${date}T12:00:00Z`);
38382
+ if (Number.isNaN(parsed.getTime())) return date;
38383
+ const locale = language === "no" || language === "nb" || language === "nn" ? "nb-NO" : language;
38384
+ try {
38385
+ return new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }).format(parsed);
38386
+ } catch {
38387
+ return date;
38388
+ }
38389
+ }
38390
+ const NO = {
38391
+ blog: "Blogg",
38392
+ latest: "Fra bloggen",
38393
+ allPosts: "Alle innlegg",
38394
+ readMore: "Les mer",
38395
+ back: "Alle innlegg",
38396
+ empty: "Ingen innlegg ennå.",
38397
+ notFound: "Fant ikke innlegget.",
38398
+ loading: "Henter innlegget …",
38399
+ share: "Del",
38400
+ moreShare: "Flere valg …",
38401
+ copyLink: "Kopier lenke",
38402
+ copied: "Lenken er kopiert",
38403
+ email: "E-post",
38404
+ video: "Video",
38405
+ play: "Spill av"
38406
+ };
38407
+ const EN = {
38408
+ blog: "Blog",
38409
+ latest: "From the blog",
38410
+ allPosts: "All posts",
38411
+ readMore: "Read more",
38412
+ back: "All posts",
38413
+ empty: "No posts yet.",
38414
+ notFound: "That post could not be found.",
38415
+ loading: "Loading the post …",
38416
+ share: "Share",
38417
+ moreShare: "More …",
38418
+ copyLink: "Copy link",
38419
+ copied: "Link copied",
38420
+ email: "Email",
38421
+ video: "Video",
38422
+ play: "Play"
38423
+ };
38424
+ function blogStrings(language) {
38425
+ return language === "no" || language === "nb" || language === "nn" ? NO : EN;
38426
+ }
38427
+ function BlogBody({ nodes, language }) {
38428
+ return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node2, i) => renderFlow(node2, i, language)) });
38429
+ }
38430
+ function renderFlow(node2, key, language) {
38431
+ switch (node2.type) {
38432
+ case "paragraph":
38433
+ return /* @__PURE__ */ jsx("p", { children: renderPhrasing(node2.children) }, key);
38434
+ case "heading": {
38435
+ const Tag = node2.depth === 2 ? "h2" : node2.depth === 3 ? "h3" : "h4";
38436
+ return /* @__PURE__ */ jsx(Tag, { children: renderPhrasing(node2.children) }, key);
38437
+ }
38438
+ case "list": {
38439
+ const items = node2.children.map((item, i) => /* @__PURE__ */ jsx("li", { children: item.children.map((child, j) => renderListChild(child, j, item.spread === true, language)) }, i));
38440
+ return node2.ordered ? /* @__PURE__ */ jsx("ol", { start: node2.start ?? void 0, children: items }, key) : /* @__PURE__ */ jsx("ul", { children: items }, key);
38441
+ }
38442
+ case "blockquote":
38443
+ return /* @__PURE__ */ jsx("blockquote", { children: node2.children.map((child, i) => renderFlow(child, i, language)) }, key);
38444
+ case "thematicBreak":
38445
+ return /* @__PURE__ */ jsx("hr", {}, key);
38446
+ case "code":
38447
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: node2.value }) }, key);
38448
+ case "table":
38449
+ return /* @__PURE__ */ jsx(BlogTable, { table: node2 }, key);
38450
+ case "mdxJsxFlowElement":
38451
+ return /* @__PURE__ */ jsx(BlogComponent, { element: node2, language }, key);
38452
+ default:
38453
+ return null;
38454
+ }
38455
+ }
38456
+ function renderListChild(child, key, spread, language) {
38457
+ if (child.type === "paragraph" && !spread) return /* @__PURE__ */ jsx("span", { children: renderPhrasing(child.children) }, key);
38458
+ return renderFlow(child, key, language);
38459
+ }
38460
+ function renderPhrasing(children) {
38461
+ return children.map((child, i) => {
38462
+ switch (child.type) {
38463
+ case "text":
38464
+ return child.value;
38465
+ case "emphasis":
38466
+ return /* @__PURE__ */ jsx("em", { children: renderPhrasing(child.children) }, i);
38467
+ case "strong":
38468
+ return /* @__PURE__ */ jsx("strong", { children: renderPhrasing(child.children) }, i);
38469
+ case "delete":
38470
+ return /* @__PURE__ */ jsx("del", { children: renderPhrasing(child.children) }, i);
38471
+ case "inlineCode":
38472
+ return /* @__PURE__ */ jsx("code", { children: child.value }, i);
38473
+ case "break":
38474
+ return /* @__PURE__ */ jsx("br", {}, i);
38475
+ case "link": {
38476
+ const safe = isSafeHref(child.url);
38477
+ if (!safe) return /* @__PURE__ */ jsx("span", { children: renderPhrasing(child.children) }, i);
38478
+ const external = /^https?:\/\//i.test(child.url);
38479
+ return /* @__PURE__ */ jsx("a", { href: child.url, ...external ? { rel: "noopener noreferrer" } : {}, children: renderPhrasing(child.children) }, i);
38480
+ }
38481
+ default:
38482
+ return null;
38483
+ }
38484
+ });
38485
+ }
38486
+ function BlogTable({ table: table2 }) {
38487
+ const [head2, ...rows] = table2.children;
38488
+ const align = (i) => table2.align?.[i] ?? void 0;
38489
+ return /* @__PURE__ */ jsx("div", { className: "setto-blog__table", children: /* @__PURE__ */ jsxs("table", { children: [
38490
+ head2 ? /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: head2.children.map((cell, i) => /* @__PURE__ */ jsx("th", { style: { textAlign: align(i) }, children: renderPhrasing(cell.children) }, i)) }) }) : null,
38491
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, r) => /* @__PURE__ */ jsx("tr", { children: row.children.map((cell, i) => /* @__PURE__ */ jsx("td", { style: { textAlign: align(i) }, children: renderPhrasing(cell.children) }, i)) }, r)) })
38492
+ ] }) });
38493
+ }
38494
+ function attrs(element2) {
38495
+ const out = {};
38496
+ for (const attribute of element2.attributes) {
38497
+ const a = attribute;
38498
+ if (a.type === "mdxJsxAttribute" && typeof a.value === "string") out[a.name] = a.value;
38499
+ }
38500
+ return out;
38501
+ }
38502
+ function BlogComponent({ element: element2, language }) {
38503
+ const props = attrs(element2);
38504
+ switch (element2.name) {
38505
+ case "Image":
38506
+ return props.src && props.alt !== void 0 ? /* @__PURE__ */ jsxs("figure", { className: "setto-blog__figure", children: [
38507
+ /* @__PURE__ */ jsx(BlogImg, { src: props.src, alt: props.alt, sizes: "(min-width: 760px) 720px, 100vw" }),
38508
+ props.caption ? /* @__PURE__ */ jsx("figcaption", { children: props.caption }) : null
38509
+ ] }) : null;
38510
+ case "Gallery": {
38511
+ const images = element2.children.filter(
38512
+ (child) => child.type === "mdxJsxFlowElement" && child.name === "Image"
38513
+ );
38514
+ return /* @__PURE__ */ jsxs("figure", { className: "setto-blog__gallery", children: [
38515
+ /* @__PURE__ */ jsx("div", { className: "setto-blog__gallery-grid", children: images.map((image2, i) => {
38516
+ const a = attrs(image2);
38517
+ return a.src ? /* @__PURE__ */ jsxs("figure", { className: "setto-blog__gallery-item", children: [
38518
+ /* @__PURE__ */ jsx(BlogImg, { src: a.src, alt: a.alt ?? "", sizes: "(min-width: 760px) 360px, 50vw" }),
38519
+ a.caption ? /* @__PURE__ */ jsx("figcaption", { children: a.caption }) : null
38520
+ ] }, i) : null;
38521
+ }) }),
38522
+ props.caption ? /* @__PURE__ */ jsx("figcaption", { children: props.caption }) : null
38523
+ ] });
38524
+ }
38525
+ case "YouTube":
38526
+ return props.id && YOUTUBE_ID_PATTERN.test(props.id) ? /* @__PURE__ */ jsx(YouTubeEmbed, { id: props.id, title: props.title, language }) : null;
38527
+ case "Callout":
38528
+ return /* @__PURE__ */ jsx("aside", { className: "setto-blog__callout", "data-tone": props.tone ?? "info", children: element2.children.map((child, i) => renderFlow(child, i, language)) });
38529
+ default:
38530
+ return null;
38531
+ }
38532
+ }
38533
+ function BlogImg({ src, alt, sizes, className }) {
38534
+ const { config } = useSetto();
38535
+ const [failed, setFailed] = useState(false);
38536
+ const optimize = config.images?.optimize === true && !failed && isOptimizable(src) && !(typeof window !== "undefined" && hasNoOptimizer(window.location.hostname));
38537
+ if (!optimize) return /* @__PURE__ */ jsx("img", { src, alt, loading: "lazy", decoding: "async", className });
38538
+ return /* @__PURE__ */ jsx(
38539
+ "img",
38540
+ {
38541
+ src: optimizedUrl(src, 1080),
38542
+ srcSet: optimizedSrcSet(src),
38543
+ sizes,
38544
+ alt,
38545
+ loading: "lazy",
38546
+ decoding: "async",
38547
+ className,
38548
+ onError: () => setFailed(true)
38549
+ }
38550
+ );
38551
+ }
38552
+ function YouTubeEmbed({ id, title, language }) {
38553
+ const [playing, setPlaying] = useState(false);
38554
+ const strings = blogStrings(language);
38555
+ const label = title ?? strings.video;
38556
+ return /* @__PURE__ */ jsxs("figure", { className: "setto-blog__video", children: [
38557
+ /* @__PURE__ */ jsx("div", { className: "setto-blog__video-frame", children: playing ? /* @__PURE__ */ jsx(
38558
+ "iframe",
38559
+ {
38560
+ src: `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0`,
38561
+ title: label,
38562
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
38563
+ allowFullScreen: true,
38564
+ referrerPolicy: "strict-origin-when-cross-origin"
38565
+ }
38566
+ ) : /* @__PURE__ */ jsxs("button", { type: "button", className: "setto-blog__video-play", onClick: () => setPlaying(true), "aria-label": `${strings.play}: ${label}`, children: [
38567
+ /* @__PURE__ */ jsx("img", { src: `https://i.ytimg.com/vi/${id}/hqdefault.jpg`, alt: "", loading: "lazy", decoding: "async" }),
38568
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "setto-blog__video-icon", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 68 48", width: "68", height: "48", children: [
38569
+ /* @__PURE__ */ jsx("path", { d: "M66.5 7.7A8.5 8.5 0 0 0 60.5 1.7C55.2.3 34 .3 34 .3s-21.2 0-26.5 1.4A8.5 8.5 0 0 0 1.5 7.7C.1 13 .1 24 .1 24s0 11 1.4 16.3a8.5 8.5 0 0 0 6 6c5.3 1.4 26.5 1.4 26.5 1.4s21.2 0 26.5-1.4a8.5 8.5 0 0 0 6-6C67.9 35 67.9 24 67.9 24s0-11-1.4-16.3z", fill: "#f00" }),
38570
+ /* @__PURE__ */ jsx("path", { d: "M45 24 27 14v20", fill: "#fff" })
38571
+ ] }) })
38572
+ ] }) }),
38573
+ title ? /* @__PURE__ */ jsx("figcaption", { children: title }) : null
38574
+ ] });
38575
+ }
38576
+ const LATEST_COUNT = 3;
38577
+ function useBlogContext() {
38578
+ const { config } = useSetto();
38579
+ const { i18n, t } = useTranslation();
38580
+ const manifest = usePageManifest();
38581
+ const language = i18n.language || config.languages?.[0] || "no";
38582
+ const fallbackLanguage = config.languages?.[0] ?? "no";
38583
+ const route = typeof window === "undefined" ? { basename: "", rest: "/" } : languageFromPath(window.location.pathname, config.languages ?? [fallbackLanguage]);
38584
+ const href = (path2) => `${route.basename}${path2}`;
38585
+ return { config, t, manifest, language, fallbackLanguage, strings: blogStrings(language), path: route.rest, href };
38586
+ }
38587
+ function blogPathOf(manifest) {
38588
+ return manifest && blogPage(manifest)?.page.path || BLOG_ROUTE;
38589
+ }
38590
+ function SettoBlogSection({ sectionId, first }) {
38591
+ useBlogStyles();
38592
+ const ctx = useBlogContext();
38593
+ const slug = ctx.manifest ? blogSlugForPath(ctx.manifest, ctx.path) : null;
38594
+ const page = ctx.manifest ? findSection(ctx.manifest, sectionId)?.page : void 0;
38595
+ const title = page ? ctx.t(page.titleKey, { defaultValue: ctx.strings.blog }) : ctx.strings.blog;
38596
+ const blogPath = blogPathOf(ctx.manifest);
38597
+ const inner = useRef(null);
38598
+ const clear = useClearFixedHeader(inner, first);
38599
+ return /* @__PURE__ */ jsx(SettoSection, { sectionId, className: "setto-blog", children: /* @__PURE__ */ jsx("div", { className: "setto-blog__inner", ref: inner, style: clear ? { paddingTop: clear } : void 0, children: slug ? /* @__PURE__ */ jsx(BlogPostView, { slug, blogPath, ctx }) : /* @__PURE__ */ jsx(BlogIndexView, { title, first, blogPath, ctx }) }) });
38600
+ }
38601
+ function SettoBlog({ basePath = BLOG_ROUTE, title }) {
38602
+ useBlogStyles();
38603
+ const ctx = useBlogContext();
38604
+ const base = normalisePath(basePath);
38605
+ const here = normalisePath(ctx.path);
38606
+ const rest = here.startsWith(`${base}/`) ? here.slice(base.length + 1) : "";
38607
+ const slug = rest && isValidBlogSlug(rest) ? rest : null;
38608
+ const inner = useRef(null);
38609
+ const clear = useClearFixedHeader(inner, true);
38610
+ return /* @__PURE__ */ jsx("section", { className: "setto-blog", children: /* @__PURE__ */ jsx("div", { className: "setto-blog__inner", ref: inner, style: clear ? { paddingTop: clear } : void 0, children: slug ? /* @__PURE__ */ jsx(BlogPostView, { slug, blogPath: base, ctx }) : /* @__PURE__ */ jsx(BlogIndexView, { title: title ?? ctx.strings.blog, first: true, blogPath: base, ctx }) }) });
38611
+ }
38612
+ function useClearFixedHeader(ref, enabled) {
38613
+ const [clear, setClear] = useState(0);
38614
+ useLayoutEffect(() => {
38615
+ if (!enabled || typeof window === "undefined") return;
38616
+ const measure = () => {
38617
+ const el = ref.current;
38618
+ if (!el) return;
38619
+ const covering = document.elementsFromPoint(window.innerWidth / 2, 1).map((node2) => fixedAncestor(node2)).find((node2) => node2 !== null && !node2.contains(el));
38620
+ const headerBottom = covering ? covering.getBoundingClientRect().bottom : 0;
38621
+ const start = el.getBoundingClientRect().top + window.scrollY;
38622
+ const overlap = Math.ceil(headerBottom + 16 - start);
38623
+ setClear(overlap > 0 ? overlap : 0);
38624
+ };
38625
+ measure();
38626
+ window.addEventListener("resize", measure);
38627
+ return () => window.removeEventListener("resize", measure);
38628
+ }, [ref, enabled]);
38629
+ return clear;
38630
+ }
38631
+ function fixedAncestor(node2) {
38632
+ for (let at = node2; at && at !== document.body; at = at.parentElement) {
38633
+ if (getComputedStyle(at).position === "fixed") return at;
38634
+ }
38635
+ return null;
38636
+ }
38637
+ function BlogIndexView({ title, first, blogPath, ctx }) {
38638
+ const posts = ctx.config.blog ? postsForLanguage(ctx.config.blog.posts, ctx.language, ctx.fallbackLanguage) : [];
38639
+ const Heading = first ? "h1" : "h2";
38640
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
38641
+ /* @__PURE__ */ jsx(Heading, { className: "setto-blog__title", children: title }),
38642
+ posts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "setto-blog__empty", children: ctx.strings.empty }) : /* @__PURE__ */ jsx("div", { className: "setto-blog__list", children: posts.map((post2) => /* @__PURE__ */ jsx(PostCard, { post: post2, blogPath, ctx, headingLevel: first ? "h2" : "h3" }, post2.slug)) })
38643
+ ] });
38644
+ }
38645
+ function PostCard({
38646
+ post: post2,
38647
+ blogPath,
38648
+ ctx,
38649
+ headingLevel
38650
+ }) {
38651
+ const Heading = headingLevel;
38652
+ const href = ctx.href(`${blogPath}/${post2.slug}`);
38653
+ return /* @__PURE__ */ jsxs("article", { className: "setto-blog__card", children: [
38654
+ post2.cover ? /* @__PURE__ */ jsx("a", { href, className: "setto-blog__card-cover", tabIndex: -1, "aria-hidden": "true", children: /* @__PURE__ */ jsx(BlogImg, { src: post2.cover, alt: "", sizes: "(min-width: 960px) 360px, (min-width: 640px) 50vw, 100vw" }) }) : null,
38655
+ /* @__PURE__ */ jsxs("div", { className: "setto-blog__card-body", children: [
38656
+ /* @__PURE__ */ jsx("time", { className: "setto-blog__date", dateTime: post2.date, children: formatPostDate(post2.date, ctx.language) }),
38657
+ /* @__PURE__ */ jsx(Heading, { className: "setto-blog__card-title", children: /* @__PURE__ */ jsx("a", { href, children: post2.title }) }),
38658
+ /* @__PURE__ */ jsx("p", { className: "setto-blog__card-lead", children: post2.description }),
38659
+ /* @__PURE__ */ jsxs("a", { href, className: "setto-blog__more", "aria-label": `${ctx.strings.readMore}: ${post2.title}`, children: [
38660
+ ctx.strings.readMore,
38661
+ " ",
38662
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "→" })
38663
+ ] })
38664
+ ] })
38665
+ ] });
38666
+ }
38667
+ function BlogPostView({ slug, blogPath, ctx }) {
38668
+ const blog = ctx.config.blog;
38669
+ const summary = blog ? postForSlug(blog.posts, slug, ctx.language, ctx.fallbackLanguage) : null;
38670
+ const [post2, setPost] = useState(null);
38671
+ const [state, setState] = useState(summary ? "loading" : "missing");
38672
+ useEffect(() => {
38673
+ if (!blog || !summary) {
38674
+ setState("missing");
38675
+ return;
38676
+ }
38677
+ let live = true;
38678
+ setState("loading");
38679
+ blog.load(summary.slug, summary.locale).then((loaded) => {
38680
+ if (!live) return;
38681
+ setPost(loaded);
38682
+ setState(loaded ? "ready" : "missing");
38683
+ }).catch(() => live && setState("missing"));
38684
+ return () => {
38685
+ live = false;
38686
+ };
38687
+ }, [blog, summary?.slug, summary?.locale]);
38688
+ usePostTitle(summary?.title ?? null, ctx.config.displayName);
38689
+ const back = /* @__PURE__ */ jsxs("a", { href: ctx.href(blogPath), className: "setto-blog__back", children: [
38690
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "←" }),
38691
+ " ",
38692
+ ctx.strings.back
38693
+ ] });
38694
+ if (!summary || state === "missing") {
38695
+ return /* @__PURE__ */ jsxs("div", { className: "setto-blog__post", children: [
38696
+ back,
38697
+ /* @__PURE__ */ jsx("p", { className: "setto-blog__empty", children: ctx.strings.notFound })
38698
+ ] });
38699
+ }
38700
+ const fm = post2?.frontmatter ?? summary;
38701
+ return /* @__PURE__ */ jsxs("article", { className: "setto-blog__post", lang: fm.locale, children: [
38702
+ back,
38703
+ /* @__PURE__ */ jsxs("header", { className: "setto-blog__post-header", children: [
38704
+ /* @__PURE__ */ jsx("time", { className: "setto-blog__date", dateTime: fm.date, children: formatPostDate(fm.date, ctx.language) }),
38705
+ /* @__PURE__ */ jsx("h1", { className: "setto-blog__post-title", children: fm.title }),
38706
+ /* @__PURE__ */ jsx("p", { className: "setto-blog__lead", children: fm.description })
38707
+ ] }),
38708
+ fm.cover ? /* @__PURE__ */ jsx("figure", { className: "setto-blog__cover", children: /* @__PURE__ */ jsx(BlogImg, { src: fm.cover, alt: "", sizes: "(min-width: 1040px) 960px, 100vw" }) }) : null,
38709
+ state === "ready" && post2 ? /* @__PURE__ */ jsx("div", { className: "setto-blog__prose", children: /* @__PURE__ */ jsx(BlogBody, { nodes: post2.body.children, language: ctx.language }) }) : /* @__PURE__ */ jsx("p", { className: "setto-blog__loading", "aria-live": "polite", children: ctx.strings.loading }),
38710
+ /* @__PURE__ */ jsx(ShareBar, { title: fm.title, strings: ctx.strings })
38711
+ ] });
38712
+ }
38713
+ function usePostTitle(title, siteName) {
38714
+ useEffect(() => {
38715
+ if (!title || typeof document === "undefined") return;
38716
+ const previous2 = document.title;
38717
+ const timer = window.setTimeout(() => {
38718
+ document.title = siteName ? `${title} · ${siteName}` : title;
38719
+ }, 0);
38720
+ return () => {
38721
+ window.clearTimeout(timer);
38722
+ document.title = previous2;
38723
+ };
38724
+ }, [title, siteName]);
38725
+ }
38726
+ function ShareBar({ title, strings }) {
38727
+ const [copied, setCopied] = useState(false);
38728
+ const [native, setNative] = useState(false);
38729
+ useEffect(() => setNative(typeof navigator !== "undefined" && typeof navigator.share === "function"), []);
38730
+ const url = typeof window === "undefined" ? "" : `${window.location.origin}${window.location.pathname}`;
38731
+ const u = encodeURIComponent(url);
38732
+ const t = encodeURIComponent(title);
38733
+ return /* @__PURE__ */ jsxs("div", { className: "setto-blog__share", "aria-label": strings.share, children: [
38734
+ /* @__PURE__ */ jsx("span", { className: "setto-blog__share-label", children: strings.share }),
38735
+ native ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void navigator.share({ title, url }).catch(() => void 0), children: strings.moreShare }) : null,
38736
+ /* @__PURE__ */ jsx("a", { href: `https://www.facebook.com/sharer/sharer.php?u=${u}`, target: "_blank", rel: "noopener noreferrer", children: "Facebook" }),
38737
+ /* @__PURE__ */ jsx("a", { href: `https://www.linkedin.com/sharing/share-offsite/?url=${u}`, target: "_blank", rel: "noopener noreferrer", children: "LinkedIn" }),
38738
+ /* @__PURE__ */ jsx("a", { href: `mailto:?subject=${t}&body=${u}`, children: strings.email }),
38739
+ /* @__PURE__ */ jsx(
38740
+ "button",
38741
+ {
38742
+ type: "button",
38743
+ onClick: () => {
38744
+ void navigator.clipboard?.writeText(url).then(() => {
38745
+ setCopied(true);
38746
+ window.setTimeout(() => setCopied(false), 2e3);
38747
+ });
38748
+ },
38749
+ children: copied ? strings.copied : strings.copyLink
38750
+ }
38751
+ )
38752
+ ] });
38753
+ }
38754
+ function SettoBlogLatest({ sectionId, first }) {
38755
+ useBlogStyles();
38756
+ const ctx = useBlogContext();
38757
+ if (!ctx.config.blog) return null;
38758
+ const posts = postsForLanguage(ctx.config.blog.posts, ctx.language, ctx.fallbackLanguage).slice(0, LATEST_COUNT);
38759
+ if (posts.length === 0) return null;
38760
+ const blogPath = blogPathOf(ctx.manifest);
38761
+ const Heading = first ? "h1" : "h2";
38762
+ return /* @__PURE__ */ jsx(SettoSection, { sectionId, className: "setto-blog setto-blog--latest", children: /* @__PURE__ */ jsxs("div", { className: "setto-blog__inner setto-blog__inner--wide", children: [
38763
+ /* @__PURE__ */ jsxs("div", { className: "setto-blog__latest-head", children: [
38764
+ /* @__PURE__ */ jsx(Heading, { className: "setto-blog__title", children: ctx.strings.latest }),
38765
+ /* @__PURE__ */ jsxs("a", { href: ctx.href(blogPath), className: "setto-blog__more", children: [
38766
+ ctx.strings.allPosts,
38767
+ " ",
38768
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "→" })
38769
+ ] })
38770
+ ] }),
38771
+ /* @__PURE__ */ jsx("div", { className: "setto-blog__list", children: posts.map((post2) => /* @__PURE__ */ jsx(PostCard, { post: post2, blogPath, ctx, headingLevel: "h3" }, post2.slug)) })
38772
+ ] }) });
38773
+ }
38774
+ const STYLE_ID$1 = "setto-blog-styles";
38775
+ const BLOG_CSS = `
38776
+ .setto-blog{padding:clamp(48px,8vw,96px) 16px}
38777
+ .setto-blog :where(.setto-blog__inner){max-width:68ch;margin:0 auto}
38778
+ .setto-blog :where(.setto-blog__inner--wide){max-width:1120px}
38779
+ .setto-blog :where(.setto-blog__title){font-family:var(--setto-display-font,inherit);font-size:clamp(2rem,4vw,2.75rem);line-height:1.15;font-weight:600;margin:0 0 .75em}
38780
+ .setto-blog :where(.setto-blog__list){display:grid;gap:clamp(1.5rem,3vw,2.5rem);grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))}
38781
+ .setto-blog :where(.setto-blog__card){display:grid;gap:.9rem;align-content:start}
38782
+ .setto-blog :where(.setto-blog__card-cover){display:block;overflow:hidden;border-radius:.9rem;aspect-ratio:16/10}
38783
+ .setto-blog :where(.setto-blog__card-cover img,.setto-blog__cover img,.setto-blog__figure img,.setto-blog__gallery-item img){display:block;width:100%;height:100%;object-fit:cover}
38784
+ .setto-blog :where(.setto-blog__card-body){display:grid;gap:.45rem}
38785
+ .setto-blog :where(.setto-blog__card-title){font-family:var(--setto-display-font,inherit);font-size:1.3rem;line-height:1.25;font-weight:600;margin:0}
38786
+ .setto-blog :where(.setto-blog__card-lead){margin:0;line-height:1.55}
38787
+ .setto-blog :where(.setto-blog__date){font-size:.875rem;letter-spacing:.01em}
38788
+ .setto-blog :where(.setto-blog__more,.setto-blog__back){display:inline-flex;gap:.35em;align-items:center;font-weight:500;width:fit-content}
38789
+ .setto-blog :where(.setto-blog__back){margin-bottom:2rem}
38790
+ .setto-blog :where(.setto-blog__latest-head){display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.5rem 1.5rem;margin-bottom:1.75rem}
38791
+ .setto-blog :where(.setto-blog__latest-head .setto-blog__title){margin:0}
38792
+ .setto-blog :where(.setto-blog__post-header){display:grid;gap:.6rem;margin-bottom:2rem}
38793
+ .setto-blog :where(.setto-blog__post-title){font-family:var(--setto-display-font,inherit);font-size:clamp(2rem,5vw,3rem);line-height:1.1;font-weight:600;margin:0}
38794
+ .setto-blog :where(.setto-blog__lead){font-size:1.2rem;line-height:1.5;margin:0}
38795
+ .setto-blog :where(.setto-blog__cover){margin:0 0 2.5rem;overflow:hidden;border-radius:1rem;aspect-ratio:16/9}
38796
+ .setto-blog :where(.setto-blog__prose){font-size:1.075rem;line-height:1.7}
38797
+ .setto-blog :where(.setto-blog__prose > * + *){margin-top:1.15em}
38798
+ .setto-blog :where(.setto-blog__prose h2,.setto-blog__prose h3,.setto-blog__prose h4){font-family:var(--setto-display-font,inherit);line-height:1.25;font-weight:600;margin:1.8em 0 0}
38799
+ .setto-blog :where(.setto-blog__prose h2){font-size:1.6rem}
38800
+ .setto-blog :where(.setto-blog__prose h3){font-size:1.3rem}
38801
+ .setto-blog :where(.setto-blog__prose h4){font-size:1.1rem}
38802
+ .setto-blog :where(.setto-blog__prose ul,.setto-blog__prose ol){padding-left:1.4em}
38803
+ .setto-blog :where(.setto-blog__prose ul){list-style:disc}
38804
+ .setto-blog :where(.setto-blog__prose ol){list-style:decimal}
38805
+ .setto-blog :where(.setto-blog__prose li + li){margin-top:.35em}
38806
+ .setto-blog :where(.setto-blog__prose blockquote){margin-left:0;padding-left:1.1em;border-left-width:3px;border-left-style:solid;font-style:italic}
38807
+ .setto-blog :where(.setto-blog__prose pre){overflow-x:auto;padding:1em;border-radius:.6rem;font-size:.9em}
38808
+ .setto-blog :where(.setto-blog__prose code){font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em}
38809
+ .setto-blog :where(.setto-blog__prose hr){border:0;border-top-width:1px;border-top-style:solid;margin:2.5em 0}
38810
+ .setto-blog :where(.setto-blog__prose a){text-decoration:underline;text-underline-offset:.18em}
38811
+ .setto-blog :where(.setto-blog__table){overflow-x:auto}
38812
+ .setto-blog :where(.setto-blog__table table){border-collapse:collapse;width:100%;font-size:.95em}
38813
+ .setto-blog :where(.setto-blog__table th,.setto-blog__table td){padding:.55em .8em;border-bottom-width:1px;border-bottom-style:solid;text-align:left}
38814
+ .setto-blog :where(.setto-blog__figure,.setto-blog__gallery,.setto-blog__video){margin:2em 0}
38815
+ .setto-blog :where(.setto-blog__figure img){border-radius:.8rem;height:auto}
38816
+ .setto-blog :where(figcaption){font-size:.9rem;margin-top:.6em;opacity:.75}
38817
+ .setto-blog :where(.setto-blog__gallery-grid){display:grid;gap:.6rem;grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr))}
38818
+ .setto-blog :where(.setto-blog__gallery-item){margin:0}
38819
+ .setto-blog :where(.setto-blog__gallery-item img){border-radius:.6rem;aspect-ratio:4/3}
38820
+ .setto-blog :where(.setto-blog__video-frame){position:relative;aspect-ratio:16/9;border-radius:.8rem;overflow:hidden}
38821
+ .setto-blog :where(.setto-blog__video-frame iframe){position:absolute;inset:0;width:100%;height:100%;border:0}
38822
+ .setto-blog :where(.setto-blog__video-play){position:absolute;inset:0;width:100%;height:100%;padding:0;border:0;cursor:pointer;display:grid;place-items:center}
38823
+ .setto-blog :where(.setto-blog__video-play img){position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
38824
+ .setto-blog :where(.setto-blog__video-icon){position:relative;display:block;line-height:0}
38825
+ .setto-blog :where(.setto-blog__callout){padding:1.1em 1.3em;border-radius:.8rem;border-left-width:4px;border-left-style:solid}
38826
+ .setto-blog :where(.setto-blog__callout > * + *){margin-top:.7em}
38827
+ .setto-blog :where(.setto-blog__callout ul,.setto-blog__callout ol){padding-left:1.3em;list-style:disc}
38828
+ .setto-blog :where(.setto-blog__share){display:flex;flex-wrap:wrap;gap:.5rem .9rem;align-items:center;margin-top:3rem;padding-top:1.5rem;border-top-width:1px;border-top-style:solid}
38829
+ .setto-blog :where(.setto-blog__share-label){font-weight:600}
38830
+ .setto-blog :where(.setto-blog__share a,.setto-blog__share button){font:inherit;padding:.35rem .85rem;border-radius:999px;border-width:1px;border-style:solid;cursor:pointer;text-decoration:none;line-height:1.4}
38831
+ .setto-blog :where(.setto-blog__empty,.setto-blog__loading){opacity:.75}
38832
+ :where(.setto-blog){color:inherit}
38833
+ :where(.setto-blog__date){opacity:.7}
38834
+ :where(.setto-blog a){color:inherit}
38835
+ :where(.setto-blog__prose blockquote,.setto-blog__prose hr,.setto-blog__table th,.setto-blog__table td,.setto-blog__share){border-color:color-mix(in srgb,currentColor 18%,transparent)}
38836
+ :where(.setto-blog__prose pre,.setto-blog__card-cover){background:color-mix(in srgb,currentColor 6%,transparent)}
38837
+ :where(.setto-blog__share a,.setto-blog__share button){border-color:color-mix(in srgb,currentColor 30%,transparent);background:transparent;color:inherit}
38838
+ :where(.setto-blog__callout){background:color-mix(in srgb,currentColor 5%,transparent);border-left-color:color-mix(in srgb,currentColor 40%,transparent)}
38839
+ :where(.setto-blog__callout[data-tone="tip"]){border-left-color:#0f7b4f}
38840
+ :where(.setto-blog__callout[data-tone="warning"]){border-left-color:#b54708}
38841
+ :where(.setto-blog__video-frame){background:#000}
38842
+ `;
38843
+ function useBlogStyles() {
38844
+ useInsertionEffect(() => {
38845
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID$1)) return;
38846
+ const el = document.createElement("style");
38847
+ el.id = STYLE_ID$1;
38848
+ el.textContent = BLOG_CSS;
38849
+ document.head.appendChild(el);
38850
+ }, []);
38851
+ }
38114
38852
  const FORM_FIELD_TYPES = [
38115
38853
  "text",
38116
38854
  "email",
@@ -39113,193 +39851,6 @@ const iframeStyle = {
39113
39851
  height: "100%",
39114
39852
  border: 0
39115
39853
  };
39116
- function usePageManifest() {
39117
- const { pageStore, config } = useSetto();
39118
- const snap = useSyncExternalStore(
39119
- (cb) => pageStore?.subscribe(() => cb()) ?? (() => {
39120
- }),
39121
- () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT,
39122
- () => pageStore?.snapshot() ?? EMPTY_PAGE_STORE_SNAPSHOT
39123
- );
39124
- return pageStore ? snap.manifest : bundledManifest(config.pages);
39125
- }
39126
- function useSettoMenu(list2 = "menu") {
39127
- const manifest = usePageManifest();
39128
- return manifest ? visibleMenu(manifest, list2) : null;
39129
- }
39130
- function SettoPage({ pageId, path: path2, sections = {}, builtins, fallback = null }) {
39131
- const manifest = usePageManifest();
39132
- const { editMode } = useSetto();
39133
- if (!manifest) {
39134
- if (!pageId && path2 !== void 0 && path2 !== "/" && path2 !== "") return /* @__PURE__ */ jsx(Fragment, { children: fallback });
39135
- return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
39136
- }
39137
- const page = pageId ? pageById(manifest, pageId) : pageForPath(manifest, path2 ?? "/");
39138
- if (!page) {
39139
- if (pageId) {
39140
- return /* @__PURE__ */ jsx(Fragment, { children: Object.entries(sections).map(([id, Component]) => /* @__PURE__ */ jsx(Component, {}, id)) });
39141
- }
39142
- return /* @__PURE__ */ jsx(Fragment, { children: fallback });
39143
- }
39144
- if (page.hidden && !editMode) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
39145
- return /* @__PURE__ */ jsx(PageBody, { page, sections, builtins, editMode, manifest });
39146
- }
39147
- function PageBody({
39148
- page,
39149
- sections,
39150
- builtins,
39151
- editMode,
39152
- manifest
39153
- }) {
39154
- usePageTitle(page);
39155
- const HiddenPageNote = useSettoChromeSlot("hidden-page");
39156
- const listed = new Set(manifest.pages.flatMap((p) => p.sections.map((s) => s.id)));
39157
- const unlisted = Object.keys(sections).filter((id) => !listed.has(id));
39158
- const firstVisible = page.sections.find((s) => !s.hidden && (s.type || sections[s.id]))?.id;
39159
- return /* @__PURE__ */ jsxs(Fragment, { children: [
39160
- page.hidden && editMode ? HiddenPageNote ? /* @__PURE__ */ jsx(HiddenPageNote, { pageId: page.id }) : /* @__PURE__ */ jsx(HiddenNote, { text: "Denne siden er skjult for besøkende." }) : null,
39161
- page.sections.map((section) => /* @__PURE__ */ jsx(Fragment$1, { children: renderSection(section, sections, builtins, editMode, section.id === firstVisible) }, section.id)),
39162
- unlisted.map((id) => {
39163
- const Component = sections[id];
39164
- return /* @__PURE__ */ jsx(Component, {}, id);
39165
- })
39166
- ] });
39167
- }
39168
- function renderSection(section, sections, builtins, editMode, first) {
39169
- if (section.hidden) {
39170
- return editMode ? /* @__PURE__ */ jsx(HiddenSection, { section }) : null;
39171
- }
39172
- if (section.type) {
39173
- const Builtin = builtins?.[section.type] ?? BUILTIN_RENDERERS[section.type];
39174
- return /* @__PURE__ */ jsx(Builtin, { sectionId: section.id, first });
39175
- }
39176
- const Component = sections[section.id];
39177
- if (!Component) {
39178
- warnOnce(section.id);
39179
- return null;
39180
- }
39181
- return /* @__PURE__ */ jsx(Component, {});
39182
- }
39183
- const warned = /* @__PURE__ */ new Set();
39184
- function warnOnce(id) {
39185
- if (warned.has(id)) return;
39186
- warned.add(id);
39187
- console.warn(`[setto] section "${id}" is in pages.json but the page does not provide it`);
39188
- }
39189
- function HiddenSection({ section }) {
39190
- const { pageStore } = useSetto();
39191
- const Offered = useSettoChromeSlot("hidden-section");
39192
- const label = section.label ?? section.id;
39193
- if (Offered) return /* @__PURE__ */ jsx(Offered, { sectionId: section.id, label });
39194
- return /* @__PURE__ */ jsxs("div", { "data-setto-ui": true, style: hiddenBarStyle, children: [
39195
- /* @__PURE__ */ jsxs("span", { children: [
39196
- "Skjult: ",
39197
- label
39198
- ] }),
39199
- pageStore ? /* @__PURE__ */ jsx(
39200
- "button",
39201
- {
39202
- type: "button",
39203
- "data-setto-ui": true,
39204
- style: hiddenBtnStyle,
39205
- onClick: () => pageStore.apply([{ op: "show_section", sectionId: section.id }]),
39206
- children: "Vis"
39207
- }
39208
- ) : null
39209
- ] });
39210
- }
39211
- function HiddenNote({ text: text2 }) {
39212
- return /* @__PURE__ */ jsx("div", { "data-setto-ui": true, style: hiddenBarStyle, children: /* @__PURE__ */ jsx("span", { children: text2 }) });
39213
- }
39214
- function usePageTitle(page) {
39215
- const { t } = useTranslation();
39216
- const original = useRef(null);
39217
- const title = page.path === "/" ? null : t(page.titleKey);
39218
- useEffect(() => {
39219
- if (!title || typeof document === "undefined") return;
39220
- original.current ??= document.title;
39221
- const base = original.current;
39222
- document.title = base ? `${title} · ${base}` : title;
39223
- return () => {
39224
- document.title = base;
39225
- };
39226
- }, [title]);
39227
- }
39228
- function SettoTextSection({ sectionId, first }) {
39229
- const keys2 = builtinKeys(sectionId);
39230
- const { i18n } = useTranslation();
39231
- const { store, editMode } = useSetto();
39232
- const intro2 = store ? store.get(keys2.intro, i18n.language) : i18n.t(keys2.intro, { defaultValue: "" });
39233
- const Heading = first ? "h1" : "h2";
39234
- return /* @__PURE__ */ jsx(SettoSection, { sectionId, style: textSectionStyle, children: /* @__PURE__ */ jsxs("div", { style: textInnerStyle, children: [
39235
- /* @__PURE__ */ jsx(Heading, { style: first ? textH1Style : textH2Style, children: /* @__PURE__ */ jsx(T, { k: keys2.heading }) }),
39236
- intro2 || editMode ? /* @__PURE__ */ jsx("p", { style: textIntroStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.intro }) }) : null,
39237
- /* @__PURE__ */ jsx(
39238
- SettoRepeater,
39239
- {
39240
- itemsKey: keys2.items,
39241
- defaultItem: { heading: "Overskrift", text: "Tekst" },
39242
- itemLabel: "avsnitt",
39243
- children: (itemKey) => /* @__PURE__ */ jsx(TextItem, { sectionId, itemKey })
39244
- }
39245
- )
39246
- ] }) });
39247
- }
39248
- function TextItem({ sectionId, itemKey }) {
39249
- const keys2 = builtinKeys(sectionId);
39250
- const { i18n } = useTranslation();
39251
- const { store, editMode } = useSetto();
39252
- const heading2 = store ? store.get(keys2.itemHeading(itemKey), i18n.language) : i18n.t(keys2.itemHeading(itemKey), { defaultValue: "" });
39253
- return /* @__PURE__ */ jsxs("div", { style: textItemStyle, children: [
39254
- heading2 || editMode ? /* @__PURE__ */ jsx("h3", { style: textH3Style, children: /* @__PURE__ */ jsx(T, { k: keys2.itemHeading(itemKey) }) }) : null,
39255
- /* @__PURE__ */ jsx("p", { style: textParagraphStyle, children: /* @__PURE__ */ jsx(T, { k: keys2.itemText(itemKey) }) })
39256
- ] });
39257
- }
39258
- const BUILTIN_RENDERERS = {
39259
- text: SettoTextSection
39260
- };
39261
- const textSectionStyle = { padding: "clamp(48px, 8vw, 96px) 16px" };
39262
- const textInnerStyle = { maxWidth: "68ch", margin: "0 auto" };
39263
- const textH1Style = {
39264
- fontFamily: "var(--setto-display-font, inherit)",
39265
- fontSize: "clamp(2rem, 4vw, 2.75rem)",
39266
- lineHeight: 1.15,
39267
- fontWeight: 600,
39268
- margin: "0 0 0.75em"
39269
- };
39270
- const textH2Style = { ...textH1Style, fontSize: "clamp(1.5rem, 3vw, 2rem)" };
39271
- const textH3Style = {
39272
- fontFamily: "var(--setto-display-font, inherit)",
39273
- fontSize: "1.125rem",
39274
- lineHeight: 1.3,
39275
- fontWeight: 600,
39276
- margin: "0 0 0.4em"
39277
- };
39278
- const textIntroStyle = { fontSize: "1.125rem", lineHeight: 1.6, opacity: 0.85, margin: "0 0 2em" };
39279
- const textItemStyle = { margin: "0 0 1.75em" };
39280
- const textParagraphStyle = { lineHeight: 1.7, margin: 0, whiteSpace: "pre-line" };
39281
- const hiddenBarStyle = {
39282
- display: "flex",
39283
- alignItems: "center",
39284
- justifyContent: "center",
39285
- gap: 12,
39286
- padding: "10px 16px",
39287
- margin: "4px 0",
39288
- border: "1px dashed rgba(100, 10, 255, 0.45)",
39289
- borderRadius: 8,
39290
- background: "rgba(100, 10, 255, 0.05)",
39291
- color: "#3b2a66",
39292
- font: '500 13px system-ui, -apple-system, "Segoe UI", sans-serif'
39293
- };
39294
- const hiddenBtnStyle = {
39295
- border: "none",
39296
- borderRadius: 999,
39297
- padding: "4px 12px",
39298
- background: "#640AFF",
39299
- color: "#fff",
39300
- font: '600 12px system-ui, -apple-system, "Segoe UI", sans-serif',
39301
- cursor: "pointer"
39302
- };
39303
39854
  const DEFAULT_DURATION = {
39304
39855
  fadeIn: 600,
39305
39856
  slideUp: 700,
@@ -40262,6 +40813,7 @@ export {
40262
40813
  AuthGate,
40263
40814
  BLOCK_REGISTRY,
40264
40815
  BLOCK_TYPES,
40816
+ BLOG_CSS,
40265
40817
  BUILTIN_SECTION_TYPES,
40266
40818
  CHECKOUT_INTENT,
40267
40819
  DEFAULT_LANGUAGE,
@@ -40280,11 +40832,15 @@ export {
40280
40832
  SETTO_BASE,
40281
40833
  SETTO_IMAGE_QUALITY,
40282
40834
  SETTO_IMAGE_WIDTHS,
40835
+ SETTO_MEDIA_HOST,
40283
40836
  SETTO_MODULES,
40284
40837
  SettoAdminApp,
40285
40838
  SettoAnimation,
40286
40839
  SettoBlock,
40287
40840
  SettoBlockTree,
40841
+ SettoBlog,
40842
+ SettoBlogLatest,
40843
+ SettoBlogSection,
40288
40844
  SettoChat,
40289
40845
  SettoChrome,
40290
40846
  SettoContext,
@@ -40306,6 +40862,8 @@ export {
40306
40862
  alternateLinks,
40307
40863
  applyOp,
40308
40864
  applyOps,
40865
+ blogPage,
40866
+ blogSlugForPath,
40309
40867
  builtinKeys,
40310
40868
  checkoutIntentUrl,
40311
40869
  countChanges,
@@ -40315,6 +40873,7 @@ export {
40315
40873
  fieldTypeHasPlaceholder,
40316
40874
  findSection,
40317
40875
  formFieldsPrefix,
40876
+ formatPostDate,
40318
40877
  isAdminDeploymentView,
40319
40878
  isAdminPanelView,
40320
40879
  isAdminRoute,
@@ -40322,6 +40881,7 @@ export {
40322
40881
  isLinkContext,
40323
40882
  isOptimizable,
40324
40883
  isSafeHref,
40884
+ isSettoMediaUrl,
40325
40885
  languageFromPath,
40326
40886
  languageName,
40327
40887
  nodeChildren,
@@ -40335,6 +40895,8 @@ export {
40335
40895
  parseManifest,
40336
40896
  pathForLanguage,
40337
40897
  pickInitialLanguage,
40898
+ postForSlug,
40899
+ postsForLanguage,
40338
40900
  readLocaleFields,
40339
40901
  redirectForVisitor,
40340
40902
  serialiseManifest,