@sevenfold/setto-client 0.35.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.
- package/dist/blog/SettoBlog.d.ts +20 -0
- package/dist/blog/data.d.ts +36 -0
- package/dist/blog/render.d.ts +16 -0
- package/dist/blog/strings.d.ts +25 -0
- package/dist/index.d.ts +4 -1
- package/dist/pages/manifest.d.ts +18 -1
- package/dist/setto-client.js +735 -198
- package/dist/setto-client.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/plugin.d.ts +32 -0
- package/dist/vite/static.d.ts +78 -0
- package/dist/vite.js +550 -0
- package/dist/vite.js.map +1 -0
- package/package.json +10 -2
- package/virtual.d.ts +11 -0
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.js","sources":["../src/blog/data.ts","../src/blog/strings.ts","../src/pages/manifest.ts","../src/vite/static.ts","../src/vite/plugin.ts"],"sourcesContent":["/**\n * The posts a site's build hands to the library, and how a page chooses among\n * them. No parser and no React: the build (`@setto/client/vite`) has already\n * validated every file against the contract and turned its body into a syntax\n * tree, so the browser only walks data.\n */\nimport type { Root } from 'mdast';\nimport type { BlogFrontmatter } from './contract';\n\n/** A post as the list knows it: its frontmatter, and nothing to download. */\nexport type BlogPostSummary = BlogFrontmatter;\n\n/** A post as its page knows it: frontmatter and the body's syntax tree. */\nexport interface BlogPostData {\n frontmatter: BlogFrontmatter;\n body: Root;\n}\n\n/**\n * What `virtual:setto-blog` exports and `SettoConfig.blog` carries. Bodies are\n * loaded one at a time, so a site with a hundred posts does not ship a\n * hundred articles to read one.\n */\nexport interface SettoBlogData {\n posts: readonly BlogPostSummary[];\n load(slug: string, locale: string): Promise<BlogPostData | null>;\n}\n\n/** Newest first; a tie breaks on slug so the order never flickers. */\nexport function byDateDesc(a: BlogPostSummary, b: BlogPostSummary): number {\n return a.date === b.date ? a.slug.localeCompare(b.slug) : a.date < b.date ? 1 : -1;\n}\n\n/**\n * One entry per post, in the reader's language where it exists and in the\n * site's own otherwise. A post written only in Norwegian still shows to an\n * English reader — untranslated is better than missing.\n */\nexport function postsForLanguage(\n posts: readonly BlogPostSummary[],\n language: string,\n fallbackLanguage: string,\n): BlogPostSummary[] {\n const bySlug = new Map<string, BlogPostSummary>();\n for (const post of posts) {\n const current = bySlug.get(post.slug);\n if (!current || rank(post, language, fallbackLanguage) < rank(current, language, fallbackLanguage)) {\n bySlug.set(post.slug, post);\n }\n }\n return [...bySlug.values()].sort(byDateDesc);\n}\n\n/** The version of one post to show this reader, or null if there is none. */\nexport function postForSlug(\n posts: readonly BlogPostSummary[],\n slug: string,\n language: string,\n fallbackLanguage: string,\n): BlogPostSummary | null {\n return postsForLanguage(posts.filter((p) => p.slug === slug), language, fallbackLanguage)[0] ?? null;\n}\n\nfunction rank(post: BlogPostSummary, language: string, fallbackLanguage: string): number {\n if (post.locale === language) return 0;\n if (post.locale === fallbackLanguage) return 1;\n return 2;\n}\n\n/** `2026-10-01` as «1. oktober 2026» or «1 October 2026». */\nexport function formatPostDate(date: string, language: string): string {\n const parsed = new Date(`${date}T12:00:00Z`);\n if (Number.isNaN(parsed.getTime())) return date;\n const locale = language === 'no' || language === 'nb' || language === 'nn' ? 'nb-NO' : language;\n try {\n return new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' }).format(parsed);\n } catch {\n return date;\n }\n}\n","/**\n * The few words the blog says itself — around the posts, never in them. In\n * Norwegian and English; any other language reads English, which is what a\n * visitor who chose it can most likely read.\n */\nconst NO = {\n blog: 'Blogg',\n latest: 'Fra bloggen',\n allPosts: 'Alle innlegg',\n readMore: 'Les mer',\n back: 'Alle innlegg',\n empty: 'Ingen innlegg ennå.',\n notFound: 'Fant ikke innlegget.',\n loading: 'Henter innlegget …',\n share: 'Del',\n moreShare: 'Flere valg …',\n copyLink: 'Kopier lenke',\n copied: 'Lenken er kopiert',\n email: 'E-post',\n video: 'Video',\n play: 'Spill av',\n};\n\nexport type BlogStrings = typeof NO;\n\nconst EN: BlogStrings = {\n blog: 'Blog',\n latest: 'From the blog',\n allPosts: 'All posts',\n readMore: 'Read more',\n back: 'All posts',\n empty: 'No posts yet.',\n notFound: 'That post could not be found.',\n loading: 'Loading the post …',\n share: 'Share',\n moreShare: 'More …',\n copyLink: 'Copy link',\n copied: 'Link copied',\n email: 'Email',\n video: 'Video',\n play: 'Play',\n};\n\nexport function blogStrings(language: string): BlogStrings {\n return language === 'no' || language === 'nb' || language === 'nn' ? NO : EN;\n}\n","/**\n * The shape of a site: which pages it has, which sections each page shows and\n * in what order, and what the menu and footer link to.\n *\n * Data rather than TSX, because the owner has to be able to change it. A page\n * written as `<Hero /><Services /><Contact />` can only be reordered by someone\n * who can edit code, so moving a section up, hiding it for the summer or adding\n * a «Vilkår» page used to mean a developer. Kept in\n * `src/content/pages.json`, which the inline editor publishes like any other\n * content file.\n *\n * Nothing is ever deleted from here. «Remove» hides: a section, a page or a\n * menu link that is hidden keeps its place and its text, and showing it again\n * puts it back exactly as it was. The owner asked for undo, and a hidden entry\n * is undo that survives a publish.\n *\n * Every function below is pure and returns a new manifest, so a draft can be\n * compared with its baseline and a failed operation leaves nothing half-done.\n */\n\nexport const PAGES_MANIFEST_VERSION = 1;\n\n/** Where the manifest lives in a site repository. */\nexport const PAGES_PATH = 'src/content/pages.json';\n\n/**\n * Sections Setto can render on its own, without code from the site.\n *\n * `text` is a heading, an optional intro and a list of paragraphs with\n * optional headings: enough for terms, privacy, an «about» page. It is what\n * lets Pixel add a «Vilkår» page in the chat instead of in a sandbox.\n *\n * `blog` is the blog (R24): the list of posts on its page, and a single post\n * at `<page path>/<slug>` — the one page whose path has room below it. At\n * most one per site, and never on the front page, which would swallow every\n * address. `blog-latest` is a band of the newest posts, for the front page.\n * Both read the posts the site's build bundles (`SettoConfig.blog`).\n */\nexport const BUILTIN_SECTION_TYPES = ['text', 'blog', 'blog-latest'] as const;\nexport type BuiltinSectionType = (typeof BUILTIN_SECTION_TYPES)[number];\n\nexport interface PageSection {\n /** Unique across the whole site: it is also the theme id and the anchor. */\n id: string;\n /** A built-in section. Absent means a component the site registers. */\n type?: BuiltinSectionType;\n hidden?: boolean;\n /** What the editor calls it («Tjenester»). Falls back to the id. */\n label?: string;\n}\n\nexport interface PageEntry {\n id: string;\n /** `/`, `/vilkar`. Never under `/setto`, which is the editor's. */\n path: string;\n /** Locale key for the page's name, used by the menu and the tab title. */\n titleKey: string;\n hidden?: boolean;\n sections: PageSection[];\n}\n\nexport interface MenuItem {\n id: string;\n /** `/vilkar`, `/#tjenester`, or an absolute `https://` link. */\n href: string;\n labelKey: string;\n hidden?: boolean;\n}\n\nexport type MenuList = 'menu' | 'footer';\n\nexport interface PagesManifest {\n version: typeof PAGES_MANIFEST_VERSION;\n pages: PageEntry[];\n menu: MenuItem[];\n footer: MenuItem[];\n}\n\nconst ID = /^[a-z0-9][a-z0-9-]{0,62}$/;\n/*\n * A blog post's slug, the same rule as `isValidBlogSlug` in\n * `../blog/contract.ts` (a test holds them together). Written out rather than\n * imported so this file stays self-contained: setto-agent vendors it whole.\n */\nconst POST_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst PATH = /^\\/(?:[a-z0-9][a-z0-9-]*(?:\\/[a-z0-9][a-z0-9-]*)*)?$/;\n\nexport interface ManifestIssue {\n at: string;\n message: string;\n}\n\nexport type ManifestParse =\n | { ok: true; manifest: PagesManifest }\n | { ok: false; issues: ManifestIssue[] };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Check a manifest from disk, from the network or from a model.\n *\n * Strict on what would break a page — duplicate ids, a path the router cannot\n * hold, a section type nothing renders — and silent about fields it does not\n * know, so a newer editor's manifest still renders on an older site.\n */\nexport function parseManifest(input: unknown): ManifestParse {\n const issues: ManifestIssue[] = [];\n if (!isRecord(input)) return { ok: false, issues: [{ at: '$', message: 'not an object' }] };\n if (input.version !== PAGES_MANIFEST_VERSION) {\n issues.push({ at: 'version', message: `expected ${PAGES_MANIFEST_VERSION}` });\n }\n\n const pages: PageEntry[] = [];\n const pageIds = new Set<string>();\n const paths = new Set<string>();\n const sectionIds = new Set<string>();\n const rawPages = Array.isArray(input.pages) ? input.pages : null;\n if (!rawPages) issues.push({ at: 'pages', message: 'must be a list' });\n\n (rawPages ?? []).forEach((raw, i) => {\n const at = `pages[${i}]`;\n if (!isRecord(raw)) {\n issues.push({ at, message: 'not an object' });\n return;\n }\n const id = String(raw.id ?? '');\n const path = String(raw.path ?? '');\n const titleKey = String(raw.titleKey ?? '');\n if (!ID.test(id)) issues.push({ at: `${at}.id`, message: `invalid id \"${id}\"` });\n else if (pageIds.has(id)) issues.push({ at: `${at}.id`, message: `duplicate page \"${id}\"` });\n pageIds.add(id);\n if (!PATH.test(path)) issues.push({ at: `${at}.path`, message: `invalid path \"${path}\"` });\n else if (path === '/setto' || path.startsWith('/setto/')) {\n issues.push({ at: `${at}.path`, message: '/setto belongs to the editor' });\n } else if (paths.has(path)) issues.push({ at: `${at}.path`, message: `duplicate path \"${path}\"` });\n paths.add(path);\n if (!titleKey) issues.push({ at: `${at}.titleKey`, message: 'required' });\n\n const sections: PageSection[] = [];\n const rawSections = Array.isArray(raw.sections) ? raw.sections : null;\n if (!rawSections) issues.push({ at: `${at}.sections`, message: 'must be a list' });\n (rawSections ?? []).forEach((s, j) => {\n const sat = `${at}.sections[${j}]`;\n if (!isRecord(s)) {\n issues.push({ at: sat, message: 'not an object' });\n return;\n }\n const sid = String(s.id ?? '');\n if (!ID.test(sid)) issues.push({ at: `${sat}.id`, message: `invalid id \"${sid}\"` });\n else if (sectionIds.has(sid)) issues.push({ at: `${sat}.id`, message: `duplicate section \"${sid}\"` });\n sectionIds.add(sid);\n const type = s.type;\n if (type !== undefined && !BUILTIN_SECTION_TYPES.includes(type as BuiltinSectionType)) {\n issues.push({ at: `${sat}.type`, message: `unknown section type \"${String(type)}\"` });\n }\n sections.push({\n id: sid,\n ...(type !== undefined ? { type: type as BuiltinSectionType } : {}),\n ...(s.hidden === true ? { hidden: true } : {}),\n ...(typeof s.label === 'string' && s.label.trim() ? { label: s.label.trim() } : {}),\n });\n });\n\n pages.push({\n id,\n path,\n titleKey,\n ...(raw.hidden === true ? { hidden: true } : {}),\n sections,\n });\n });\n\n const blogs = pages.flatMap((p, i) =>\n p.sections.flatMap((s, j) => (s.type === 'blog' ? [{ page: p, at: `pages[${i}].sections[${j}]` }] : [])),\n );\n if (blogs.length > 1) {\n issues.push({ at: blogs[1]!.at, message: 'a site has one blog; another blog section is already on a page' });\n }\n for (const blog of blogs) {\n if (blog.page.path === '/') issues.push({ at: blog.at, message: 'the blog cannot be the front page' });\n }\n\n const menu = parseMenu(input.menu, 'menu', issues);\n const footer = parseMenu(input.footer, 'footer', issues);\n\n if (issues.length > 0) return { ok: false, issues };\n return { ok: true, manifest: { version: PAGES_MANIFEST_VERSION, pages, menu, footer } };\n}\n\nfunction parseMenu(raw: unknown, at: MenuList, issues: ManifestIssue[]): MenuItem[] {\n if (raw === undefined) return [];\n if (!Array.isArray(raw)) {\n issues.push({ at, message: 'must be a list' });\n return [];\n }\n const ids = new Set<string>();\n const items: MenuItem[] = [];\n raw.forEach((item, i) => {\n const iat = `${at}[${i}]`;\n if (!isRecord(item)) {\n issues.push({ at: iat, message: 'not an object' });\n return;\n }\n const id = String(item.id ?? '');\n const href = String(item.href ?? '');\n const labelKey = String(item.labelKey ?? '');\n if (!ID.test(id)) issues.push({ at: `${iat}.id`, message: `invalid id \"${id}\"` });\n else if (ids.has(id)) issues.push({ at: `${iat}.id`, message: `duplicate item \"${id}\"` });\n ids.add(id);\n if (!isSafeHref(href)) issues.push({ at: `${iat}.href`, message: `invalid link \"${href}\"` });\n if (!labelKey) issues.push({ at: `${iat}.labelKey`, message: 'required' });\n items.push({ id, href, labelKey, ...(item.hidden === true ? { hidden: true } : {}) });\n });\n return items;\n}\n\n/** A path on this site, an anchor, or an http(s)/mailto/tel link. Never `javascript:`. */\nexport function isSafeHref(href: string): boolean {\n if (!href) return false;\n if (href.startsWith('/') && !href.startsWith('//')) return true;\n if (href.startsWith('#')) return true;\n return /^(https?:\\/\\/|mailto:|tel:)/i.test(href);\n}\n\n// ── Reading ─────────────────────────────────────────────────────────────────\n\n/** `/vilkar/` and `/vilkar` are one page; the empty path is the front page. */\nexport function normalisePath(path: string): string {\n const bare = (path.split(/[?#]/)[0] ?? '').replace(/\\/+$/, '');\n return bare === '' ? '/' : bare.toLowerCase();\n}\n\nexport function pageForPath(manifest: PagesManifest, path: string): PageEntry | null {\n const wanted = normalisePath(path);\n return manifest.pages.find((p) => p.path === wanted) ?? (blogSlugForPath(manifest, path) ? blogPage(manifest)!.page : null);\n}\n\n/** The page holding the site's blog, and the section on it. */\nexport function blogPage(manifest: PagesManifest): { page: PageEntry; section: PageSection } | null {\n for (const page of manifest.pages) {\n const section = page.sections.find((s) => s.type === 'blog');\n if (section) return { page, section };\n }\n return null;\n}\n\n/**\n * The post a path names: `/blogg/ny-kaffemaskin` → `ny-kaffemaskin` when the\n * blog lives at `/blogg`. One segment below the blog page and a valid slug;\n * anything else is not a post.\n */\nexport function blogSlugForPath(manifest: PagesManifest, path: string): string | null {\n const blog = blogPage(manifest);\n if (!blog || blog.page.path === '/') return null;\n const wanted = normalisePath(path);\n const prefix = `${blog.page.path}/`;\n if (!wanted.startsWith(prefix)) return null;\n const slug = wanted.slice(prefix.length);\n return slug.length <= 80 && POST_SLUG.test(slug) ? slug : null;\n}\n\nexport function pageById(manifest: PagesManifest, id: string): PageEntry | null {\n return manifest.pages.find((p) => p.id === id) ?? null;\n}\n\n/** The page a section is on, and where. */\nexport function findSection(\n manifest: PagesManifest,\n sectionId: string,\n): { page: PageEntry; index: number; section: PageSection } | null {\n for (const page of manifest.pages) {\n const index = page.sections.findIndex((s) => s.id === sectionId);\n if (index >= 0) return { page, index, section: page.sections[index]! };\n }\n return null;\n}\n\n/**\n * The links a visitor should see.\n *\n * Hidden items go, and so do items that would lead nowhere: a link to a\n * hidden page, or an anchor to a hidden section. Hiding «Tjenester» for the\n * winter should not leave a menu entry that scrolls to nothing.\n */\nexport function visibleMenu(manifest: PagesManifest, list: MenuList): MenuItem[] {\n return manifest[list].filter((item) => !item.hidden && !leadsNowhere(manifest, item.href));\n}\n\nfunction leadsNowhere(manifest: PagesManifest, href: string): boolean {\n if (!href.startsWith('/') && !href.startsWith('#')) return false;\n const [pathPart, fragment] = href.split('#') as [string, string | undefined];\n const page = pageForPath(manifest, pathPart || '/');\n if (pathPart && !page) return false; // a route the site handles in code\n if (page?.hidden) return true;\n if (!fragment) return false;\n const target = findSection(manifest, fragment);\n return target ? Boolean(target.section.hidden || target.page.hidden) : false;\n}\n\n// ── Changing ────────────────────────────────────────────────────────────────\n\nexport class ManifestError extends Error {\n constructor(\n public readonly code:\n | 'page_not_found'\n | 'section_not_found'\n | 'item_not_found'\n | 'duplicate_id'\n | 'duplicate_path'\n | 'invalid',\n message: string,\n ) {\n super(message);\n this.name = 'ManifestError';\n }\n}\n\nfunction clone(manifest: PagesManifest): PagesManifest {\n return JSON.parse(JSON.stringify(manifest)) as PagesManifest;\n}\n\nfunction mustPage(manifest: PagesManifest, pageId: string): PageEntry {\n const page = pageById(manifest, pageId);\n if (!page) throw new ManifestError('page_not_found', `no page \"${pageId}\"`);\n return page;\n}\n\n/**\n * Move a section up (negative) or down (positive) on its page.\n *\n * Skips over hidden neighbours: moving «Om oss» up past a hidden section\n * would otherwise do nothing the owner can see, and look broken. Returns the\n * manifest unchanged at either end rather than throwing — the button is\n * simply at the edge.\n */\nexport function moveSection(manifest: PagesManifest, sectionId: string, delta: -1 | 1): PagesManifest {\n const next = clone(manifest);\n const found = findSection(next, sectionId);\n if (!found) throw new ManifestError('section_not_found', `no section \"${sectionId}\"`);\n const { page, index } = found;\n let target = index + delta;\n while (target >= 0 && target < page.sections.length && page.sections[target]!.hidden) target += delta;\n if (target < 0 || target >= page.sections.length) return manifest;\n const [moved] = page.sections.splice(index, 1);\n page.sections.splice(target, 0, moved!);\n return next;\n}\n\n/** Put a section at an exact position among its page's sections. */\nexport function placeSection(manifest: PagesManifest, sectionId: string, index: number): PagesManifest {\n const next = clone(manifest);\n const found = findSection(next, sectionId);\n if (!found) throw new ManifestError('section_not_found', `no section \"${sectionId}\"`);\n const [moved] = found.page.sections.splice(found.index, 1);\n const at = Math.max(0, Math.min(index, found.page.sections.length));\n found.page.sections.splice(at, 0, moved!);\n return next;\n}\n\nexport function setSectionHidden(manifest: PagesManifest, sectionId: string, hidden: boolean): PagesManifest {\n const next = clone(manifest);\n const found = findSection(next, sectionId);\n if (!found) throw new ManifestError('section_not_found', `no section \"${sectionId}\"`);\n if (hidden) found.section.hidden = true;\n else delete found.section.hidden;\n return next;\n}\n\nexport function addSection(\n manifest: PagesManifest,\n pageId: string,\n section: PageSection,\n index?: number,\n): PagesManifest {\n if (!ID.test(section.id)) throw new ManifestError('invalid', `invalid section id \"${section.id}\"`);\n if (findSection(manifest, section.id)) {\n throw new ManifestError('duplicate_id', `section \"${section.id}\" already exists`);\n }\n const next = clone(manifest);\n const page = mustPage(next, pageId);\n const at = index === undefined ? page.sections.length : Math.max(0, Math.min(index, page.sections.length));\n page.sections.splice(at, 0, { ...section });\n return next;\n}\n\nexport function addPage(manifest: PagesManifest, page: PageEntry): PagesManifest {\n const path = normalisePath(page.path);\n if (!ID.test(page.id) || !PATH.test(path) || path === '/setto' || path.startsWith('/setto/')) {\n throw new ManifestError('invalid', `invalid page \"${page.id}\" at \"${page.path}\"`);\n }\n if (pageById(manifest, page.id)) throw new ManifestError('duplicate_id', `page \"${page.id}\" already exists`);\n if (pageForPath(manifest, path)) throw new ManifestError('duplicate_path', `a page already lives at ${path}`);\n let next = clone(manifest);\n next.pages.push({ id: page.id, path, titleKey: page.titleKey, sections: [] });\n for (const section of page.sections) next = addSection(next, page.id, section);\n return next;\n}\n\nexport function setPageHidden(manifest: PagesManifest, pageId: string, hidden: boolean): PagesManifest {\n const next = clone(manifest);\n const page = mustPage(next, pageId);\n if (page.path === '/' && hidden) throw new ManifestError('invalid', 'the front page cannot be hidden');\n if (hidden) page.hidden = true;\n else delete page.hidden;\n return next;\n}\n\n/** Add a link, or change the one with the same id. */\nexport function upsertMenuItem(\n manifest: PagesManifest,\n list: MenuList,\n item: MenuItem,\n index?: number,\n): PagesManifest {\n if (!ID.test(item.id) || !isSafeHref(item.href) || !item.labelKey) {\n throw new ManifestError('invalid', `invalid link \"${item.id}\"`);\n }\n const next = clone(manifest);\n const items = next[list];\n const existing = items.findIndex((i) => i.id === item.id);\n const clean: MenuItem = { id: item.id, href: item.href, labelKey: item.labelKey };\n if (item.hidden) clean.hidden = true;\n if (existing >= 0) items[existing] = clean;\n else items.splice(index === undefined ? items.length : Math.max(0, Math.min(index, items.length)), 0, clean);\n return next;\n}\n\nexport function moveMenuItem(manifest: PagesManifest, list: MenuList, itemId: string, delta: -1 | 1): PagesManifest {\n const next = clone(manifest);\n const items = next[list];\n const index = items.findIndex((i) => i.id === itemId);\n if (index < 0) throw new ManifestError('item_not_found', `no link \"${itemId}\" in ${list}`);\n const target = index + delta;\n if (target < 0 || target >= items.length) return manifest;\n const [moved] = items.splice(index, 1);\n items.splice(target, 0, moved!);\n return next;\n}\n\nexport function setMenuItemHidden(\n manifest: PagesManifest,\n list: MenuList,\n itemId: string,\n hidden: boolean,\n): PagesManifest {\n const next = clone(manifest);\n const item = next[list].find((i) => i.id === itemId);\n if (!item) throw new ManifestError('item_not_found', `no link \"${itemId}\" in ${list}`);\n if (hidden) item.hidden = true;\n else delete item.hidden;\n return next;\n}\n\n/**\n * One change, as data.\n *\n * What Pixel's structure tools return and what the editor applies, so a\n * suggestion from the chat and a click in the toolbar go through the same\n * functions — and a change the model gets wrong fails here, in a way the\n * owner never sees half of.\n */\nexport type ManifestOp =\n | { op: 'move_section'; sectionId: string; delta: -1 | 1 }\n | { op: 'place_section'; sectionId: string; index: number }\n | { op: 'hide_section'; sectionId: string }\n | { op: 'show_section'; sectionId: string }\n | { op: 'add_section'; pageId: string; section: PageSection; index?: number }\n | { op: 'add_page'; page: PageEntry }\n | { op: 'hide_page'; pageId: string }\n | { op: 'show_page'; pageId: string }\n | { op: 'upsert_link'; list: MenuList; item: MenuItem; index?: number }\n | { op: 'move_link'; list: MenuList; itemId: string; delta: -1 | 1 }\n | { op: 'hide_link'; list: MenuList; itemId: string }\n | { op: 'show_link'; list: MenuList; itemId: string };\n\nexport function applyOp(manifest: PagesManifest, op: ManifestOp): PagesManifest {\n switch (op.op) {\n case 'move_section':\n return moveSection(manifest, op.sectionId, op.delta);\n case 'place_section':\n return placeSection(manifest, op.sectionId, op.index);\n case 'hide_section':\n return setSectionHidden(manifest, op.sectionId, true);\n case 'show_section':\n return setSectionHidden(manifest, op.sectionId, false);\n case 'add_section':\n return addSection(manifest, op.pageId, op.section, op.index);\n case 'add_page':\n return addPage(manifest, op.page);\n case 'hide_page':\n return setPageHidden(manifest, op.pageId, true);\n case 'show_page':\n return setPageHidden(manifest, op.pageId, false);\n case 'upsert_link':\n return upsertMenuItem(manifest, op.list, op.item, op.index);\n case 'move_link':\n return moveMenuItem(manifest, op.list, op.itemId, op.delta);\n case 'hide_link':\n return setMenuItemHidden(manifest, op.list, op.itemId, true);\n case 'show_link':\n return setMenuItemHidden(manifest, op.list, op.itemId, false);\n }\n}\n\n/** Every op or none: a list that fails halfway returns the error and no manifest. */\nexport function applyOps(\n manifest: PagesManifest,\n ops: readonly ManifestOp[],\n): { ok: true; manifest: PagesManifest } | { ok: false; error: string; at: number } {\n let next = manifest;\n for (let i = 0; i < ops.length; i++) {\n try {\n next = applyOp(next, ops[i]!);\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err), at: i };\n }\n }\n return { ok: true, manifest: next };\n}\n\n// ── Built-in sections' content ──────────────────────────────────────────────\n\n/** Where a built-in section keeps its words in the locale files. */\nexport function builtinKeys(sectionId: string) {\n const base = `sections.${sectionId}`;\n return {\n heading: `${base}.heading`,\n intro: `${base}.intro`,\n items: `${base}.items`,\n itemHeading: (itemKey: string) => `${base}.items.${itemKey}.heading`,\n itemText: (itemKey: string) => `${base}.items.${itemKey}.text`,\n };\n}\n\n/** The default locale key for a page's name. */\nexport function pageTitleKey(pageId: string): string {\n return `pages.${pageId}.title`;\n}\n\n/** Stable JSON for the file, so an unchanged manifest is an unchanged file. */\nexport function serialiseManifest(manifest: PagesManifest): string {\n return JSON.stringify(manifest, null, 2) + '\\n';\n}\n","/**\n * The static files a blog adds to a site's build: one HTML page per post with\n * its own head, the blog's index page, an RSS feed, and the posts in\n * `sitemap.xml`.\n *\n * Why: the sites are single-page apps — every path is the same `index.html`.\n * Google runs the JavaScript; Facebook, LinkedIn, X, Slack, Messenger and\n * iMessage do not. They fetch the address and read the `og:` tags in the raw\n * HTML, so without a file of its own a shared post shows the front page's\n * title and picture. Vercel serves a real file before it applies the\n * catch-all rewrite, so `/blogg/<slug>` gets this one and the app takes over\n * in the browser as before.\n *\n * Pure functions over strings, so they can be tested without a build.\n */\nimport type { BlogFrontmatter } from '../blog/contract';\n\nexport interface HeadMeta {\n title: string;\n description: string;\n /** Absolute, when the site's address is known. */\n url?: string;\n /** Absolute. */\n image?: string;\n type: 'article' | 'website';\n locale: string;\n siteName?: string;\n publishedTime?: string;\n jsonLd?: Record<string, unknown>;\n rssUrl?: string;\n /** The same page in other languages, absolute; `x-default` for the primary. */\n alternates?: { hreflang: string; href: string }[];\n}\n\nconst escapeAttr = (value: string) =>\n value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\n\nexport const escapeXml = escapeAttr;\n\n/** JSON inside `<script>` must not be able to close the tag. */\nconst safeJson = (value: unknown) =>\n JSON.stringify(value).replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e').replace(/&/g, '\\\\u0026');\n\n/** As the sites write it on `<html>`: Norwegian is `nb-NO`, the rest as they are. */\nfunction htmlLang(locale: string): string {\n return locale === 'no' || locale === 'nb' ? 'nb-NO' : locale === 'nn' ? 'nn-NO' : locale;\n}\n\nconst OG_LOCALE: Record<string, string> = { no: 'nb_NO', nb: 'nb_NO', nn: 'nn_NO', en: 'en_GB', sv: 'sv_SE', da: 'da_DK', de: 'de_DE' };\n\n/**\n * The site's `index.html` with a page's own head: its title, description,\n * canonical address, Open Graph and Twitter tags and structured data. Any of\n * those the front page already had are taken out first, so a crawler never\n * reads two answers.\n */\nexport function withHead(indexHtml: string, meta: HeadMeta): string {\n let html = indexHtml\n .replace(/<title>[\\s\\S]*?<\\/title>\\s*/gi, '')\n .replace(/<meta\\s+name=[\"']description[\"'][^>]*>\\s*/gi, '')\n .replace(/<meta\\s+(?:property|name)=[\"'](?:og|twitter|article):[^\"']*[\"'][^>]*>\\s*/gi, '')\n .replace(/<link\\s+rel=[\"']canonical[\"'][^>]*>\\s*/gi, '')\n .replace(/<link\\s+rel=[\"']alternate[\"']\\s+type=[\"']application\\/rss\\+xml[\"'][^>]*>\\s*/gi, '')\n .replace(/<link\\b(?=[^>]*\\bhreflang=)[^>]*>\\s*/gi, '')\n .replace(/<script\\s+type=[\"']application\\/ld\\+json[\"'][^>]*>[\\s\\S]*?<\\/script>\\s*/gi, '');\n\n const lang = escapeAttr(htmlLang(meta.locale));\n html = /<html\\b[^>]*\\blang=/i.test(html)\n ? html.replace(/(<html\\b[^>]*\\blang=)([\"'])[^\"']*\\2/i, `$1\"${lang}\"`)\n : html.replace(/<html\\b/i, `<html lang=\"${lang}\"`);\n\n const tags = [\n `<title>${escapeAttr(meta.title)}</title>`,\n `<meta name=\"description\" content=\"${escapeAttr(meta.description)}\" />`,\n meta.url ? `<link rel=\"canonical\" href=\"${escapeAttr(meta.url)}\" />` : '',\n ...(meta.alternates ?? []).map(\n (a) => `<link rel=\"alternate\" hreflang=\"${escapeAttr(a.hreflang === 'x-default' ? a.hreflang : htmlLang(a.hreflang))}\" href=\"${escapeAttr(a.href)}\" />`,\n ),\n meta.rssUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${escapeAttr(meta.siteName ?? meta.title)}\" href=\"${escapeAttr(meta.rssUrl)}\" />` : '',\n `<meta property=\"og:type\" content=\"${meta.type}\" />`,\n `<meta property=\"og:title\" content=\"${escapeAttr(meta.title)}\" />`,\n `<meta property=\"og:description\" content=\"${escapeAttr(meta.description)}\" />`,\n meta.url ? `<meta property=\"og:url\" content=\"${escapeAttr(meta.url)}\" />` : '',\n meta.image ? `<meta property=\"og:image\" content=\"${escapeAttr(meta.image)}\" />` : '',\n meta.siteName ? `<meta property=\"og:site_name\" content=\"${escapeAttr(meta.siteName)}\" />` : '',\n `<meta property=\"og:locale\" content=\"${OG_LOCALE[meta.locale] ?? meta.locale}\" />`,\n meta.publishedTime ? `<meta property=\"article:published_time\" content=\"${escapeAttr(meta.publishedTime)}\" />` : '',\n `<meta name=\"twitter:card\" content=\"${meta.image ? 'summary_large_image' : 'summary'}\" />`,\n `<meta name=\"twitter:title\" content=\"${escapeAttr(meta.title)}\" />`,\n `<meta name=\"twitter:description\" content=\"${escapeAttr(meta.description)}\" />`,\n meta.image ? `<meta name=\"twitter:image\" content=\"${escapeAttr(meta.image)}\" />` : '',\n meta.jsonLd ? `<script type=\"application/ld+json\">${safeJson(meta.jsonLd)}</script>` : '',\n ].filter(Boolean);\n\n return html.replace(/<\\/head>/i, ` ${tags.join('\\n ')}\\n </head>`);\n}\n\n/** `https://x.no` + `/blogg/a` — or the path alone when the address is unknown. */\nexport function absolute(siteUrl: string | null, pathOrUrl: string): string {\n if (/^https?:\\/\\//i.test(pathOrUrl)) return pathOrUrl;\n return siteUrl ? `${siteUrl.replace(/\\/+$/, '')}${pathOrUrl}` : pathOrUrl;\n}\n\nexport interface PostHeadInput {\n post: BlogFrontmatter;\n blogPath: string;\n siteUrl: string | null;\n siteName?: string;\n}\n\nexport function postHead({ post, blogPath, siteUrl, siteName }: PostHeadInput): HeadMeta {\n const path = `${blogPath}/${post.slug}`;\n const url = siteUrl ? absolute(siteUrl, path) : undefined;\n const image = post.cover ? absolute(siteUrl, post.cover) : undefined;\n return {\n title: siteName ? `${post.title} · ${siteName}` : post.title,\n description: post.description,\n url,\n image,\n type: 'article',\n locale: post.locale,\n siteName,\n publishedTime: post.date,\n rssUrl: siteUrl ? absolute(siteUrl, `${blogPath}/rss.xml`) : undefined,\n jsonLd: {\n '@context': 'https://schema.org',\n '@type': 'BlogPosting',\n headline: post.title,\n description: post.description,\n datePublished: post.date,\n inLanguage: post.locale,\n ...(image ? { image } : {}),\n ...(url ? { url, mainEntityOfPage: url } : {}),\n ...(siteName ? { publisher: { '@type': 'Organization', name: siteName } } : {}),\n ...(post.tags?.length ? { keywords: post.tags.join(', ') } : {}),\n },\n };\n}\n\nexport function indexHead({\n title,\n description,\n blogPath,\n siteUrl,\n siteName,\n locale,\n}: {\n title: string;\n description: string;\n blogPath: string;\n siteUrl: string | null;\n siteName?: string;\n locale: string;\n}): HeadMeta {\n const url = siteUrl ? absolute(siteUrl, blogPath) : undefined;\n return {\n title: siteName ? `${title} · ${siteName}` : title,\n description,\n url,\n type: 'website',\n locale,\n siteName,\n rssUrl: siteUrl ? absolute(siteUrl, `${blogPath}/rss.xml`) : undefined,\n jsonLd: {\n '@context': 'https://schema.org',\n '@type': 'Blog',\n name: title,\n ...(url ? { url } : {}),\n ...(siteName ? { publisher: { '@type': 'Organization', name: siteName } } : {}),\n },\n };\n}\n\n/** RFC 822, as RSS wants it. */\nfunction rssDate(date: string): string {\n return new Date(`${date}T12:00:00Z`).toUTCString();\n}\n\nexport function rssFeed({\n posts,\n blogPath,\n siteUrl,\n title,\n description,\n locale,\n}: {\n posts: readonly BlogFrontmatter[];\n blogPath: string;\n siteUrl: string;\n title: string;\n description: string;\n locale: string;\n}): string {\n const link = absolute(siteUrl, blogPath);\n const self = absolute(siteUrl, `${blogPath}/rss.xml`);\n const items = posts\n .map((post) => {\n const url = absolute(siteUrl, `${blogPath}/${post.slug}`);\n return [\n ' <item>',\n ` <title>${escapeXml(post.title)}</title>`,\n ` <link>${escapeXml(url)}</link>`,\n ` <guid isPermaLink=\"true\">${escapeXml(url)}</guid>`,\n ` <pubDate>${rssDate(post.date)}</pubDate>`,\n ` <description>${escapeXml(post.description)}</description>`,\n ' </item>',\n ].join('\\n');\n })\n .join('\\n');\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">',\n ' <channel>',\n ` <title>${escapeXml(title)}</title>`,\n ` <link>${escapeXml(link)}</link>`,\n ` <description>${escapeXml(description)}</description>`,\n ` <language>${escapeXml(locale === 'no' ? 'nb' : locale)}</language>`,\n ` <atom:link href=\"${escapeXml(self)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n items,\n ' </channel>',\n '</rss>',\n '',\n ]\n .filter((line) => line !== '')\n .join('\\n') + '\\n';\n}\n\n/**\n * The site's `sitemap.xml` with the blog and its posts added — before the\n * closing tag, and only those not already listed.\n */\nexport function withSitemapEntries(sitemap: string, entries: readonly { loc: string; lastmod?: string }[]): string {\n const missing = entries.filter((entry) => !sitemap.includes(`<loc>${escapeXml(entry.loc)}</loc>`));\n if (missing.length === 0 || !/<\\/urlset>/i.test(sitemap)) return sitemap;\n const block = missing\n .map((entry) =>\n [\n ' <url>',\n ` <loc>${escapeXml(entry.loc)}</loc>`,\n entry.lastmod ? ` <lastmod>${escapeXml(entry.lastmod)}</lastmod>` : '',\n ' </url>',\n ]\n .filter(Boolean)\n .join('\\n'),\n )\n .join('\\n');\n return sitemap.replace(/<\\/urlset>/i, `${block}\\n</urlset>`);\n}\n\n/** The site's own address, from the first `<loc>` of its sitemap. */\nexport function originFromSitemap(sitemap: string): string | null {\n const loc = /<loc>\\s*([^<\\s]+)\\s*<\\/loc>/i.exec(sitemap)?.[1];\n if (!loc) return null;\n try {\n return new URL(loc).origin;\n } catch {\n return null;\n }\n}\n","/// <reference types=\"node\" />\n/**\n * `settoBlog()` — the Vite plugin that brings a site's blog posts into its\n * build (R24).\n *\n * // vite.config.ts\n * import { settoBlog } from '@setto/client/vite';\n * export default defineConfig({ plugins: [react(), settoBlog()] });\n *\n * // main.tsx\n * import blog from 'virtual:setto-blog';\n * <SettoProvider config={{ ...settoConfig, blog, … }}>\n *\n * It reads `src/content/blog/*.mdx`, validates each file against the post\n * contract — a post that breaks it fails the build, so a broken file can never\n * deploy — and serves them as data: the list as `virtual:setto-blog`, each\n * body as its own chunk loaded when the post is opened. The browser gets\n * syntax trees, never the parser and never code.\n *\n * After a production build it writes the static files sharing and search\n * need (see `./static.ts`): `<blog>/<slug>/index.html` with each post's own\n * head, `<blog>/index.html`, `<blog>/rss.xml`, and the posts in\n * `sitemap.xml`. Only when the site's `pages.json` has a visible blog page.\n */\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';\nimport { BLOG_DIR, parseBlogPostPath, type BlogFrontmatter } from '../blog/contract';\nimport { validateBlogPost, type BlogIssue } from '../blog/validate';\nimport { postsForLanguage } from '../blog/data';\nimport { blogStrings } from '../blog/strings';\nimport { SETTO_MEDIA_HOST } from '../lib/media';\nimport { blogPage, normalisePath, parseManifest, PAGES_PATH } from '../pages/manifest';\nimport { absolute, indexHead, originFromSitemap, postHead, rssFeed, withHead, withSitemapEntries } from './static';\n\nexport interface SettoBlogPluginOptions {\n /**\n * The site's public address, for canonical links, `og:url`, RSS and the\n * sitemap. Otherwise `SETTO_SITE_URL`, then Vercel's\n * `VERCEL_PROJECT_PRODUCTION_URL`, then the first address in\n * `public/sitemap.xml`. Without any, pages still get their own head, only\n * with relative addresses, and no feed.\n */\n siteUrl?: string;\n /** Hosts a post's images may come from. Setto's media store by default. */\n imageHosts?: readonly string[];\n /**\n * Where the blog lives, for a site without a blog page in `pages.json`\n * (one that routes by hand and mounts `<SettoBlog basePath>`). A blog page\n * in the manifest wins.\n */\n blogPath?: string;\n}\n\nconst INDEX_ID = 'virtual:setto-blog';\nconst POST_PREFIX = 'virtual:setto-blog/post/';\nconst RESOLVED = '\\0';\n\ninterface LoadedPost {\n key: string;\n frontmatter: BlogFrontmatter;\n body: unknown;\n}\n\n/** Positions and estree data are for tools, not readers: dropped to keep each chunk small. */\nfunction stripForBrowser(node: unknown): unknown {\n if (Array.isArray(node)) return node.map(stripForBrowser);\n if (!node || typeof node !== 'object') return node;\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(node)) {\n if (key === 'position' || key === 'data') continue;\n out[key] = stripForBrowser(value);\n }\n return out;\n}\n\nfunction formatIssues(file: string, issues: readonly BlogIssue[]): string {\n return issues.map((i) => ` ${file}${i.line ? `:${i.line}` : ''} (${i.path}): ${i.message}`).join('\\n');\n}\n\nexport function loadPosts(root: string, imageHosts: readonly string[]): LoadedPost[] {\n const dir = path.join(root, BLOG_DIR);\n if (!existsSync(dir)) return [];\n const posts: LoadedPost[] = [];\n const problems: string[] = [];\n for (const name of readdirSync(dir).sort()) {\n if (!name.endsWith('.mdx')) continue;\n const repoPath = `${BLOG_DIR}/${name}`;\n if (!parseBlogPostPath(repoPath)) {\n problems.push(` ${repoPath}: not <slug>.<locale>.mdx`);\n continue;\n }\n const result = validateBlogPost(readFileSync(path.join(dir, name), 'utf8'), { path: repoPath, imageHosts });\n if (!result.ok) {\n problems.push(formatIssues(repoPath, result.issues));\n continue;\n }\n const { slug, locale } = result.post.frontmatter;\n posts.push({ key: `${slug}.${locale}`, frontmatter: result.post.frontmatter, body: stripForBrowser(result.post.body) });\n }\n if (problems.length) {\n throw new Error(`[setto-blog] ${problems.length === 1 ? 'a post breaks' : 'posts break'} the blog contract:\\n${problems.join('\\n')}`);\n }\n return posts;\n}\n\nfunction readJson(file: string): unknown {\n try {\n return JSON.parse(readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n}\n\nexport function settoBlog(options: SettoBlogPluginOptions = {}): Plugin {\n const imageHosts = options.imageHosts ?? [SETTO_MEDIA_HOST];\n let config: ResolvedConfig;\n let cache: LoadedPost[] | null = null;\n const posts = () => (cache ??= loadPosts(config.root, imageHosts));\n\n return {\n name: 'setto-blog',\n\n configResolved(resolved) {\n config = resolved;\n },\n\n resolveId(id) {\n if (id === INDEX_ID || id.startsWith(POST_PREFIX)) return RESOLVED + id;\n return null;\n },\n\n load(id) {\n if (!id.startsWith(RESOLVED)) return null;\n const bare = id.slice(RESOLVED.length);\n if (bare === INDEX_ID) {\n const list = posts();\n const loaders = list\n .map((p) => ` ${JSON.stringify(p.key)}: () => import(${JSON.stringify(POST_PREFIX + p.key)}),`)\n .join('\\n');\n return [\n `const posts = ${JSON.stringify(list.map((p) => p.frontmatter))};`,\n `const loaders = {\\n${loaders}\\n};`,\n 'export default {',\n ' posts,',\n ' load(slug, locale) {',\n \" const load = loaders[slug + '.' + locale];\",\n ' return load ? load().then((m) => m.default) : Promise.resolve(null);',\n ' },',\n '};',\n ].join('\\n');\n }\n if (bare.startsWith(POST_PREFIX)) {\n const post = posts().find((p) => p.key === bare.slice(POST_PREFIX.length));\n if (!post) return 'export default null;';\n return `export default ${JSON.stringify({ frontmatter: post.frontmatter, body: post.body })};`;\n }\n return null;\n },\n\n configureServer(server: ViteDevServer) {\n const dir = path.join(config.root, BLOG_DIR);\n server.watcher.add(dir);\n const changed = (file: string) => {\n if (!path.resolve(file).startsWith(path.resolve(dir))) return;\n cache = null;\n for (const [id, mod] of server.moduleGraph.idToModuleMap) {\n if (id.startsWith(RESOLVED + INDEX_ID)) server.moduleGraph.invalidateModule(mod);\n }\n server.ws.send({ type: 'full-reload' });\n };\n server.watcher.on('add', changed).on('change', changed).on('unlink', changed);\n },\n\n writeBundle: {\n sequential: true,\n handler() {\n if (config.command !== 'build' || config.build.ssr) return;\n writeStaticFiles(config.root, path.resolve(config.root, config.build.outDir), posts(), options);\n },\n },\n };\n}\n\n/** Exported for tests: everything `writeBundle` does, given a built `outDir`. */\nexport function writeStaticFiles(\n root: string,\n outDir: string,\n loaded: readonly LoadedPost[],\n options: SettoBlogPluginOptions = {},\n): { written: string[] } {\n const written: string[] = [];\n const manifestRaw = readJson(path.join(root, PAGES_PATH));\n const parsed = manifestRaw ? parseManifest(manifestRaw) : null;\n const blog = parsed?.ok ? blogPage(parsed.manifest) : null;\n if (blog && (blog.page.hidden || blog.section.hidden)) return { written };\n const blogPath = blog?.page.path ?? (options.blogPath ? normalisePath(options.blogPath) : null);\n if (!blogPath || blogPath === '/') return { written };\n\n const indexFile = path.join(outDir, 'index.html');\n if (!existsSync(indexFile)) return { written };\n const indexHtml = readFileSync(indexFile, 'utf8');\n\n const settoConfig = (readJson(path.join(root, 'setto.config.json')) ?? {}) as { displayName?: string; languages?: string[] };\n const languages = settoConfig.languages?.length ? settoConfig.languages : ['no'];\n const language = languages[0]!;\n const siteName = settoConfig.displayName;\n const sitemapFile = path.join(outDir, 'sitemap.xml');\n const sitemap = existsSync(sitemapFile) ? readFileSync(sitemapFile, 'utf8') : null;\n const vercel = process.env.VERCEL_PROJECT_PRODUCTION_URL;\n const siteUrl =\n options.siteUrl ??\n process.env.SETTO_SITE_URL ??\n (vercel ? `https://${vercel}` : null) ??\n (sitemap ? originFromSitemap(sitemap) : null);\n\n const posts = postsForLanguage(\n loaded.map((p) => p.frontmatter),\n language,\n language,\n );\n\n const write = (relative: string, content: string) => {\n const file = path.join(outDir, relative);\n mkdirSync(path.dirname(file), { recursive: true });\n writeFileSync(file, content);\n written.push(relative.split(path.sep).join('/'));\n };\n\n /*\n * Every language the site speaks gets the blog's pages, the second and\n * later ones under their prefix (`/en/blogg/…`), as the site serves them.\n * A post shown in a language it was not written in is the primary page's\n * duplicate, so its canonical address is the primary one; `hreflang`\n * names only the versions that exist.\n */\n const all = loaded.map((p) => p.frontmatter);\n const prefixOf = (lang: string) => (lang === language ? '' : `/${lang}`);\n const versionsOf = (slug: string) => all.filter((p) => p.slug === slug);\n for (const lang of languages) {\n const langBlogPath = `${prefixOf(lang)}${blogPath}`;\n const dir = langBlogPath.replace(/^\\//, '');\n const langStrings = blogStrings(lang);\n write(\n path.join(dir, 'index.html'),\n withHead(\n indexHtml,\n indexHead({\n title: langStrings.blog,\n description: siteName ? `${langStrings.latest} · ${siteName}` : langStrings.latest,\n blogPath: langBlogPath,\n siteUrl,\n siteName,\n locale: lang,\n }),\n ),\n );\n for (const post of postsForLanguage(all, lang, language)) {\n const meta = postHead({ post, blogPath: langBlogPath, siteUrl, siteName });\n const versions = versionsOf(post.slug);\n if (post.locale !== lang && siteUrl) meta.url = absolute(siteUrl, `${prefixOf(post.locale)}${blogPath}/${post.slug}`);\n if (siteUrl && versions.length > 1) {\n meta.alternates = [\n ...versions.map((v) => ({ hreflang: v.locale, href: absolute(siteUrl, `${prefixOf(v.locale)}${blogPath}/${v.slug}`) })),\n { hreflang: 'x-default', href: absolute(siteUrl, `${blogPath}/${post.slug}`) },\n ];\n }\n write(path.join(dir, post.slug, 'index.html'), withHead(indexHtml, meta));\n }\n if (siteUrl) {\n const feedTitle = siteName ? `${langStrings.blog} · ${siteName}` : langStrings.blog;\n write(\n path.join(dir, 'rss.xml'),\n rssFeed({\n posts: postsForLanguage(all, lang, language).slice(0, 50),\n blogPath: langBlogPath,\n siteUrl,\n title: feedTitle,\n description: siteName ? `${langStrings.latest} · ${siteName}` : langStrings.latest,\n locale: lang,\n }),\n );\n }\n }\n\n if (siteUrl) {\n if (sitemap) {\n const entries = [\n { loc: absolute(siteUrl, blogPath), lastmod: posts[0]?.date },\n ...posts.map((post) => ({ loc: absolute(siteUrl, `${blogPath}/${post.slug}`), lastmod: post.date })),\n // A version in another language is a page of its own.\n ...all\n .filter((post) => post.locale !== language && languages.includes(post.locale))\n .map((post) => ({ loc: absolute(siteUrl, `${prefixOf(post.locale)}${blogPath}/${post.slug}`), lastmod: post.date })),\n ];\n const next = withSitemapEntries(sitemap, entries);\n if (next !== sitemap) write('sitemap.xml', next);\n }\n }\n return { written };\n}\n"],"names":["path"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sevenfold/setto-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.7",
|
|
@@ -22,9 +22,16 @@
|
|
|
22
22
|
"./blog": {
|
|
23
23
|
"types": "./dist/blog/index.d.ts",
|
|
24
24
|
"import": "./dist/blog.js"
|
|
25
|
+
},
|
|
26
|
+
"./vite": {
|
|
27
|
+
"types": "./dist/vite/index.d.ts",
|
|
28
|
+
"import": "./dist/vite.js"
|
|
29
|
+
},
|
|
30
|
+
"./virtual": {
|
|
31
|
+
"types": "./virtual.d.ts"
|
|
25
32
|
}
|
|
26
33
|
},
|
|
27
|
-
"files": ["dist"],
|
|
34
|
+
"files": ["dist", "virtual.d.ts"],
|
|
28
35
|
"scripts": {
|
|
29
36
|
"build": "vite build && vite build -c vite.blog.config.ts && tsc -p tsconfig.build.json",
|
|
30
37
|
"dev": "vite build --watch",
|
|
@@ -45,6 +52,7 @@
|
|
|
45
52
|
"remark-gfm": "^4.0.1"
|
|
46
53
|
},
|
|
47
54
|
"devDependencies": {
|
|
55
|
+
"@types/node": "22",
|
|
48
56
|
"@types/react": "^19.2.5",
|
|
49
57
|
"@types/react-dom": "^19.2.3",
|
|
50
58
|
"@vitejs/plugin-react": "^5.1.1",
|
package/virtual.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module `settoBlog()` serves. Reference it from a site's
|
|
3
|
+
* `src/vite-env.d.ts`:
|
|
4
|
+
*
|
|
5
|
+
* /// <reference types="@setto/client/virtual" />
|
|
6
|
+
*/
|
|
7
|
+
declare module 'virtual:setto-blog' {
|
|
8
|
+
import type { SettoBlogData } from '@setto/client';
|
|
9
|
+
const blog: SettoBlogData;
|
|
10
|
+
export default blog;
|
|
11
|
+
}
|