@arpsw/astro-cms 0.3.1

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/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) ARP.software. All rights reserved.
2
+
3
+ This package is proprietary and intended for use within ARP.software projects.
4
+ Not licensed for redistribution.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # @arpsw/astro-cms
2
+
3
+ Astro integration for the **ARP (Laravel) CMS**. It packages the wiring every
4
+ ARP CMS site repeats — the API client, types, i18n + locale/path resolution, and
5
+ config — so a new site is *install → configure → design* instead of fork-and-sync.
6
+
7
+ Modelled on [`@storyblok/astro`](https://github.com/storyblok/monoblok/tree/main/packages/astro):
8
+ a single integration in `astro.config`, plus runtime helpers and a block
9
+ dispatcher. The CMS is multi-site; one deployment serves one site (`site` slug).
10
+
11
+ > **Status — `0.x` (pre-release).** Shipped: the `arpCms()` integration (i18n
12
+ > routing + the `virtual:arp-cms` config module), the CMS client, API types,
13
+ > i18n/path resolution, and `resolveRequest()`. Landing next: the `<CmsBlock>`
14
+ > dispatcher (you currently map block types in the site). See **Roadmap**.
15
+
16
+ ## Requirements
17
+
18
+ - Astro `^6`
19
+ - Node `>= 22.12.0`
20
+ - A reachable ARP CMS API (Laravel), e.g. `https://arp-agiledrop.test` in dev
21
+
22
+ ## Install
23
+
24
+ The package is published to the **public npm registry** under the `@arpsw`
25
+ scope — no registry config or auth token required:
26
+
27
+ ```bash
28
+ npm install @arpsw/astro-cms
29
+ ```
30
+
31
+ ## Configure
32
+
33
+ Register the integration in `astro.config.ts`. Wire the values from the site's
34
+ own `.env` — the integration runs in Node before Vite, so it can't read
35
+ `import.meta.env` itself:
36
+
37
+ ```ts
38
+ import { defineConfig } from 'astro/config';
39
+ import cloudflare from '@astrojs/cloudflare';
40
+ import arpCms from '@arpsw/astro-cms';
41
+
42
+ export default defineConfig({
43
+ adapter: cloudflare(),
44
+ integrations: [
45
+ arpCms({
46
+ baseUrl: process.env.CMS_API_BASE_URL ?? 'http://arp-agiledrop.test',
47
+ site: process.env.CMS_SITE ?? 'agiledrop',
48
+ locales: ['en', 'sl'], // first is the fallback default
49
+ defaultLocale: process.env.DEFAULT_LOCALE, // optional; must be in `locales`
50
+ menuSlug: process.env.CMS_MENU_SLUG ?? 'main',
51
+ previewToken: process.env.CMS_PREVIEW_TOKEN,
52
+ websiteUrls: {
53
+ en: process.env.WEBSITE_URL_EN,
54
+ sl: process.env.WEBSITE_URL_SL,
55
+ },
56
+ }),
57
+ ],
58
+ });
59
+ ```
60
+
61
+ The integration then, on `astro:config:setup`:
62
+
63
+ - sets Astro's `i18n: { locales, defaultLocale, routing: { prefixDefaultLocale: false } }`,
64
+ - applies the `publicDir` dev workaround (Astro/Vite trailing-slash bug),
65
+ - exposes the resolved config to runtime code as the **`virtual:arp-cms`** module.
66
+
67
+ You do **not** repeat the `i18n` block or the `publicDir` tweak in your own config.
68
+
69
+ ## Render content
70
+
71
+ Each site keeps a thin catch-all that wraps **its own** layout and maps block
72
+ types to **its own** components. Import runtime helpers from the `/runtime`
73
+ subpath (the `.` entry is the integration, kept free of runtime imports so it's
74
+ safe in `astro.config`):
75
+
76
+ ```astro
77
+ ---
78
+ // src/pages/[...slug].astro
79
+ import Base from '../layouts/Base.astro';
80
+ import { resolveRequest } from '@arpsw/astro-cms/runtime';
81
+ import HomeHero from '../components/blocks/HomeHero.astro';
82
+ import Features from '../components/blocks/Features.astro';
83
+
84
+ const blocks = { home_hero: HomeHero, features: Features };
85
+
86
+ const { locale, resolved, menu, redirect } = await resolveRequest(Astro);
87
+ if (redirect) return Astro.redirect(redirect.to, redirect.code);
88
+ // resolveRequest already set Astro.response.status + Cache-Control.
89
+ ---
90
+ <Base mainMenu={menu?.items ?? []}>
91
+ {resolved?.type === 'page' &&
92
+ resolved.data.blocks.map((block) => {
93
+ const Cmp = blocks[block.type];
94
+ return Cmp ? <Cmp {...block} {locale} /> : null;
95
+ })}
96
+ </Base>
97
+ ```
98
+
99
+ `resolveRequest()` encapsulates locale/path resolution → the CMS `resolve`
100
+ lookup → the nav menu fetch → the edge `Cache-Control` headers. The route stays
101
+ ~15 lines; the package owns the plumbing, the site owns the design.
102
+
103
+ ### Optional: `<CmsBlock>`
104
+
105
+ For sites whose blocks share a uniform prop signature, a generic dispatcher
106
+ saves the `block.type` switch:
107
+
108
+ ```astro
109
+ import CmsBlock from '@arpsw/astro-cms/CmsBlock.astro';
110
+ import Hero from '../components/blocks/Hero.astro';
111
+ const components = { hero: Hero, features: Features };
112
+ ...
113
+ {page.blocks.map((block) => <CmsBlock {block} {components} {locale} />)}
114
+ ```
115
+
116
+ It renders `components[block.type]` with the whole `block` (read `block.data`)
117
+ plus any extra props; unknown types render nothing. If your blocks need
118
+ **per-type props or per-block typed `data`** (e.g. only the first block gets
119
+ `isFirst`), hand-write a renderer with a `block.type` switch instead — that
120
+ stays fully type-safe.
121
+
122
+ ## Media & i18n helpers (`/runtime`)
123
+
124
+ **Media** — normalise the DAM picker shape (`MediaAsset | MediaAsset[] | null`):
125
+
126
+ ```ts
127
+ import { assetSrc, assetAlt, firstAsset, assetFocalPosition } from '@arpsw/astro-cms/runtime';
128
+ const src = assetSrc(block.image, 'large'); // best size, falls back to .url
129
+ const alt = assetAlt(block.image); // alt → title → ''
130
+ const pos = assetFocalPosition(block.image); // "50% 30%" for object-position, or undefined
131
+ ```
132
+
133
+ **Language switcher** — one entry per configured locale (labels from `localeMeta`):
134
+
135
+ ```ts
136
+ import { languageSwitchEntries, isRTL } from '@arpsw/astro-cms/runtime';
137
+ const entries = languageSwitchEntries(Astro.url); // [{ locale, code, native, href, isActive, hreflang }]
138
+ ```
139
+
140
+ **UI translations** — the package owns the *mechanism*, the site owns the
141
+ *content*. Define a per-locale dictionary and get a typed lookup:
142
+
143
+ ```ts
144
+ // site src/i18n.ts
145
+ import { makeTranslator } from '@arpsw/astro-cms/runtime';
146
+ export const t = makeTranslator({
147
+ en: { footer: { contact: 'Contact us' } },
148
+ sl: { footer: { contact: 'Kontaktirajte nas' } },
149
+ });
150
+ // component: const s = t(locale); s.footer.contact (falls back to default locale)
151
+ ```
152
+
153
+ ## Options
154
+
155
+ | Option | Required | Default | Notes |
156
+ | --- | --- | --- | --- |
157
+ | `baseUrl` | ✓ | — | CMS API base URL; trailing slashes trimmed |
158
+ | `site` | ✓ | — | Multi-site slug (or numeric id) |
159
+ | `locales` | ✓ | — | Locale codes; first is the fallback default |
160
+ | `defaultLocale` | | `locales[0]` | Must be one of `locales`, else ignored |
161
+ | `menuSlug` | | `"main"` | Nav menu slug |
162
+ | `previewToken` | | — | Bearer for `preview/*`; omit to disable preview |
163
+ | `cache` | | sensible defaults | `Cache-Control` overrides (`page`/`notFound`/`error`/`preview`) |
164
+ | `websiteUrls` | | `{}` | Per-locale canonical URLs; unset → path-prefix routing |
165
+ | `localeMeta` | | `{}` | Per-locale display data (`code`, `native`, `english?`, `dir?`) for the language switcher + RTL |
166
+
167
+ ## Local development of this package
168
+
169
+ No monorepo — link a local checkout into a site while iterating:
170
+
171
+ ```bash
172
+ # in this package
173
+ npm run build # or: npm run dev (tsup --watch)
174
+ npm link
175
+
176
+ # in the consuming site
177
+ npm link @arpsw/astro-cms
178
+ ```
179
+
180
+ `npm run check` type-checks; `npm run build` emits `dist/` (ESM + `.d.ts`).
181
+
182
+ ## Roadmap
183
+
184
+ - ✅ CMS client (`getPage`, `resolvePath`, `listPosts`, `getMenu`,
185
+ `getWebform`, …), API types, i18n/path resolution (`resolveRequest`,
186
+ `resolveLocaleAndPath`, `getLocaleUrl`, `localePath`, `linkHref`). First
187
+ consumer: `astro-website` (agiledrop).
188
+ - ✅ `<CmsBlock>` — optional generic dispatcher (`@arpsw/astro-cms/CmsBlock.astro`).
189
+ Per-block typed renderers remain the recommended pattern for varied props.
190
+ - later — optional `injectRoute` for the catch-all + preview routes, a
191
+ translation (UI-strings) system + `media` helper + per-locale display
192
+ metadata (needed before `arp-software-website` can adopt the package), and
193
+ `types` codegen from the Laravel API resources.
194
+
195
+ ## Publishing
196
+
197
+ Tag a release; CI (`.github/workflows/release.yml`) builds and publishes to
198
+ GitHub Packages:
199
+
200
+ ```bash
201
+ npm version patch # bumps package.json + creates the tag
202
+ git push --follow-tags
203
+ ```
@@ -0,0 +1,16 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { A as ArpCmsOptions } from './options-O5HIKY42.js';
3
+ export { C as CacheConfig, L as LocaleMeta, R as ResolvedArpCmsConfig } from './options-O5HIKY42.js';
4
+
5
+ /**
6
+ * The ARP CMS Astro integration.
7
+ *
8
+ * Wires the boilerplate every ARP CMS site repeats: it configures Astro's i18n
9
+ * routing from `locales`/`defaultLocale`, applies the `publicDir` dev workaround,
10
+ * and publishes the resolved connection/locale/cache config to runtime code
11
+ * through the `virtual:arp-cms` module (consumed by `@arpsw/astro-cms`'s client
12
+ * and i18n helpers).
13
+ */
14
+ declare function arpCms(options: ArpCmsOptions): AstroIntegration;
15
+
16
+ export { ArpCmsOptions, arpCms, arpCms as default };
package/dist/index.js ADDED
@@ -0,0 +1,98 @@
1
+ // src/integration.ts
2
+ import { fileURLToPath } from "url";
3
+
4
+ // src/options.ts
5
+ var DEFAULT_CACHE = {
6
+ page: "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",
7
+ notFound: "public, max-age=0, s-maxage=60",
8
+ error: "no-store",
9
+ preview: "no-store, no-cache, must-revalidate"
10
+ };
11
+ var trimTrailingSlashes = (value) => value.replace(/\/+$/, "");
12
+ function resolveOptions(options) {
13
+ if (!options.locales?.length) {
14
+ throw new Error("[@arpsw/astro-cms] `locales` must list at least one locale.");
15
+ }
16
+ const fallback = options.locales[0];
17
+ const defaultLocale = options.defaultLocale && options.locales.includes(options.defaultLocale) ? options.defaultLocale : fallback;
18
+ return {
19
+ cms: {
20
+ baseUrl: trimTrailingSlashes(options.baseUrl),
21
+ site: options.site.trim(),
22
+ previewToken: options.previewToken || void 0,
23
+ menuSlug: (options.menuSlug ?? "main").trim()
24
+ },
25
+ cache: { ...DEFAULT_CACHE, ...options.cache },
26
+ websiteUrls: options.websiteUrls ?? {},
27
+ contentTypePaths: options.contentTypePaths ?? {},
28
+ localeMeta: options.localeMeta ?? {},
29
+ locales: [...options.locales],
30
+ defaultLocale
31
+ };
32
+ }
33
+
34
+ // src/integration.ts
35
+ var VIRTUAL_ID = "virtual:arp-cms";
36
+ var RESOLVED_VIRTUAL_ID = "\0" + VIRTUAL_ID;
37
+ function arpCms(options) {
38
+ const resolved = resolveOptions(options);
39
+ return {
40
+ name: "@arpsw/astro-cms",
41
+ hooks: {
42
+ "astro:config:setup": ({ config, updateConfig, logger }) => {
43
+ updateConfig({
44
+ i18n: {
45
+ locales: [...resolved.locales],
46
+ defaultLocale: resolved.defaultLocale,
47
+ // Full object: `astro:config:setup`'s updateConfig uses the resolved
48
+ // (strict) config type, unlike the shorthand defineConfig accepts.
49
+ // Default-locale pages live at the root; `redirectToDefaultLocale`
50
+ // must be false when `prefixDefaultLocale` is false (Astro rejects
51
+ // true here — it would risk redirect loops).
52
+ routing: {
53
+ prefixDefaultLocale: false,
54
+ redirectToDefaultLocale: false,
55
+ fallbackType: "redirect"
56
+ }
57
+ },
58
+ vite: {
59
+ plugins: [virtualConfigPlugin(resolved)],
60
+ // Astro passes `publicDir` with a trailing slash, which makes Vite's
61
+ // initPublicFiles strip the leading slash from cached filenames and
62
+ // 404 every `public/` asset in dev. Pass it explicitly (no trailing
63
+ // slash) to bypass the cached path.
64
+ publicDir: fileURLToPath(config.publicDir)
65
+ }
66
+ });
67
+ logger.info(
68
+ `serving CMS site "${resolved.cms.site}" \xB7 locales [${resolved.locales.join(
69
+ ", "
70
+ )}] \xB7 default "${resolved.defaultLocale}"`
71
+ );
72
+ }
73
+ }
74
+ };
75
+ }
76
+ function virtualConfigPlugin(resolved) {
77
+ return {
78
+ name: "arp-cms:virtual-config",
79
+ resolveId(id) {
80
+ if (id === VIRTUAL_ID) {
81
+ return RESOLVED_VIRTUAL_ID;
82
+ }
83
+ return void 0;
84
+ },
85
+ load(id) {
86
+ if (id === RESOLVED_VIRTUAL_ID) {
87
+ return `export const config = ${JSON.stringify(resolved)};
88
+ export default config;`;
89
+ }
90
+ return void 0;
91
+ }
92
+ };
93
+ }
94
+ export {
95
+ arpCms,
96
+ arpCms as default
97
+ };
98
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/integration.ts","../src/options.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport type { AstroIntegration } from 'astro';\nimport type { Plugin } from 'vite';\nimport { resolveOptions, type ArpCmsOptions, type ResolvedArpCmsConfig } from './options';\n\nconst VIRTUAL_ID = 'virtual:arp-cms';\nconst RESOLVED_VIRTUAL_ID = '\\0' + VIRTUAL_ID;\n\n/**\n * The ARP CMS Astro integration.\n *\n * Wires the boilerplate every ARP CMS site repeats: it configures Astro's i18n\n * routing from `locales`/`defaultLocale`, applies the `publicDir` dev workaround,\n * and publishes the resolved connection/locale/cache config to runtime code\n * through the `virtual:arp-cms` module (consumed by `@arpsw/astro-cms`'s client\n * and i18n helpers).\n */\nexport function arpCms(options: ArpCmsOptions): AstroIntegration {\n const resolved = resolveOptions(options);\n\n return {\n name: '@arpsw/astro-cms',\n hooks: {\n 'astro:config:setup': ({ config, updateConfig, logger }) => {\n updateConfig({\n i18n: {\n locales: [...resolved.locales],\n defaultLocale: resolved.defaultLocale,\n // Full object: `astro:config:setup`'s updateConfig uses the resolved\n // (strict) config type, unlike the shorthand defineConfig accepts.\n // Default-locale pages live at the root; `redirectToDefaultLocale`\n // must be false when `prefixDefaultLocale` is false (Astro rejects\n // true here — it would risk redirect loops).\n routing: {\n prefixDefaultLocale: false,\n redirectToDefaultLocale: false,\n fallbackType: 'redirect',\n },\n },\n vite: {\n plugins: [virtualConfigPlugin(resolved)],\n // Astro passes `publicDir` with a trailing slash, which makes Vite's\n // initPublicFiles strip the leading slash from cached filenames and\n // 404 every `public/` asset in dev. Pass it explicitly (no trailing\n // slash) to bypass the cached path.\n publicDir: fileURLToPath(config.publicDir),\n },\n });\n\n logger.info(\n `serving CMS site \"${resolved.cms.site}\" · locales [${resolved.locales.join(\n ', ',\n )}] · default \"${resolved.defaultLocale}\"`,\n );\n },\n },\n };\n}\n\n/** Serves the resolved config as the `virtual:arp-cms` module to runtime code. */\nfunction virtualConfigPlugin(resolved: ResolvedArpCmsConfig): Plugin {\n return {\n name: 'arp-cms:virtual-config',\n resolveId(id) {\n if (id === VIRTUAL_ID) {\n return RESOLVED_VIRTUAL_ID;\n }\n return undefined;\n },\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) {\n return `export const config = ${JSON.stringify(resolved)};\\nexport default config;`;\n }\n return undefined;\n },\n };\n}\n","/**\n * Public options for the `arpCms()` integration, and the resolved, serializable\n * config shape that gets exposed to runtime code via `virtual:arp-cms`.\n *\n * The integration runs in the consumer's `astro.config` (Node, before Vite), so\n * the site passes config explicitly here — typically wired from its own `.env`.\n */\n\n/** Per-locale display metadata for the language switcher + `<html dir>`. */\nexport interface LocaleMeta {\n /** Short uppercase code shown in the picker (EN, SL). */\n code: string;\n /** Endonym — the language's name in its own language. */\n native: string;\n /** Exonym in English (optional). */\n english?: string;\n /** Text direction; defaults to 'ltr'. */\n dir?: 'ltr' | 'rtl';\n}\n\n/** Edge (Cloudflare) `Cache-Control` headers set by the SSR routes. */\nexport interface CacheConfig {\n /** Successful page/post responses. */\n page: string;\n /** 404 responses (shorter TTL so new CMS content becomes reachable quickly). */\n notFound: string;\n /** Upstream/CMS errors — never cache. */\n error: string;\n /** Preview routes — never cache, never index. */\n preview: string;\n}\n\nexport interface ArpCmsOptions {\n /** Base URL of the Laravel CMS API (trailing slashes are trimmed). */\n baseUrl: string;\n /** Multi-site CMS site this deployment serves (slug preferred, id accepted). */\n site: string;\n /** Locale codes this site publishes. The first is the fallback default. */\n locales: readonly string[];\n /** Effective default locale; must be one of `locales`. Defaults to `locales[0]`. */\n defaultLocale?: string;\n /** Navigation menu slug fetched for the site nav. Defaults to `\"main\"`. */\n menuSlug?: string;\n /** Bearer token for the `preview/*` endpoints; omit to disable preview. */\n previewToken?: string;\n /** Per-locale `Cache-Control` overrides; sensible defaults are applied. */\n cache?: Partial<CacheConfig>;\n /** Per-locale canonical site URLs (no trailing slash); unset → path-prefix routing. */\n websiteUrls?: Record<string, string | undefined>;\n /**\n * Per-content-type, per-locale URL prefixes (e.g. `{ post: { en: 'blog' } }`),\n * mirroring the CMS `/config` `content_type_paths`. Page has no prefix (it\n * lives at the site root). Consumed by {@link contentTypePath}.\n */\n contentTypePaths?: Record<string, Record<string, string | undefined>>;\n /** Per-locale display metadata for the language switcher + RTL handling. */\n localeMeta?: Record<string, LocaleMeta>;\n}\n\n/** Resolved config — serialized into the `virtual:arp-cms` module at build time. */\nexport interface ResolvedArpCmsConfig {\n cms: {\n baseUrl: string;\n site: string;\n previewToken?: string;\n menuSlug: string;\n };\n cache: CacheConfig;\n websiteUrls: Record<string, string | undefined>;\n contentTypePaths: Record<string, Record<string, string | undefined>>;\n localeMeta: Record<string, LocaleMeta>;\n locales: readonly string[];\n defaultLocale: string;\n}\n\nconst DEFAULT_CACHE: CacheConfig = {\n page: 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400',\n notFound: 'public, max-age=0, s-maxage=60',\n error: 'no-store',\n preview: 'no-store, no-cache, must-revalidate',\n};\n\nconst trimTrailingSlashes = (value: string): string => value.replace(/\\/+$/, '');\n\nexport function resolveOptions(options: ArpCmsOptions): ResolvedArpCmsConfig {\n if (!options.locales?.length) {\n throw new Error('[@arpsw/astro-cms] `locales` must list at least one locale.');\n }\n\n const fallback = options.locales[0]!;\n const defaultLocale =\n options.defaultLocale && options.locales.includes(options.defaultLocale)\n ? options.defaultLocale\n : fallback;\n\n return {\n cms: {\n baseUrl: trimTrailingSlashes(options.baseUrl),\n site: options.site.trim(),\n previewToken: options.previewToken || undefined,\n menuSlug: (options.menuSlug ?? 'main').trim(),\n },\n cache: { ...DEFAULT_CACHE, ...options.cache },\n websiteUrls: options.websiteUrls ?? {},\n contentTypePaths: options.contentTypePaths ?? {},\n localeMeta: options.localeMeta ?? {},\n locales: [...options.locales],\n defaultLocale,\n };\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;;;AC2E9B,IAAM,gBAA6B;AAAA,EACjC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,SAAS;AACX;AAEA,IAAM,sBAAsB,CAAC,UAA0B,MAAM,QAAQ,QAAQ,EAAE;AAExE,SAAS,eAAe,SAA8C;AAC3E,MAAI,CAAC,QAAQ,SAAS,QAAQ;AAC5B,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AAEA,QAAM,WAAW,QAAQ,QAAQ,CAAC;AAClC,QAAM,gBACJ,QAAQ,iBAAiB,QAAQ,QAAQ,SAAS,QAAQ,aAAa,IACnE,QAAQ,gBACR;AAEN,SAAO;AAAA,IACL,KAAK;AAAA,MACH,SAAS,oBAAoB,QAAQ,OAAO;AAAA,MAC5C,MAAM,QAAQ,KAAK,KAAK;AAAA,MACxB,cAAc,QAAQ,gBAAgB;AAAA,MACtC,WAAW,QAAQ,YAAY,QAAQ,KAAK;AAAA,IAC9C;AAAA,IACA,OAAO,EAAE,GAAG,eAAe,GAAG,QAAQ,MAAM;AAAA,IAC5C,aAAa,QAAQ,eAAe,CAAC;AAAA,IACrC,kBAAkB,QAAQ,oBAAoB,CAAC;AAAA,IAC/C,YAAY,QAAQ,cAAc,CAAC;AAAA,IACnC,SAAS,CAAC,GAAG,QAAQ,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;;;ADxGA,IAAM,aAAa;AACnB,IAAM,sBAAsB,OAAO;AAW5B,SAAS,OAAO,SAA0C;AAC/D,QAAM,WAAW,eAAe,OAAO;AAEvC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,sBAAsB,CAAC,EAAE,QAAQ,cAAc,OAAO,MAAM;AAC1D,qBAAa;AAAA,UACX,MAAM;AAAA,YACJ,SAAS,CAAC,GAAG,SAAS,OAAO;AAAA,YAC7B,eAAe,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMxB,SAAS;AAAA,cACP,qBAAqB;AAAA,cACrB,yBAAyB;AAAA,cACzB,cAAc;AAAA,YAChB;AAAA,UACF;AAAA,UACA,MAAM;AAAA,YACJ,SAAS,CAAC,oBAAoB,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,YAKvC,WAAW,cAAc,OAAO,SAAS;AAAA,UAC3C;AAAA,QACF,CAAC;AAED,eAAO;AAAA,UACL,qBAAqB,SAAS,IAAI,IAAI,mBAAgB,SAAS,QAAQ;AAAA,YACrE;AAAA,UACF,CAAC,mBAAgB,SAAS,aAAa;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAGA,SAAS,oBAAoB,UAAwC;AACnE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,IAAI;AACZ,UAAI,OAAO,YAAY;AACrB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK,IAAI;AACP,UAAI,OAAO,qBAAqB;AAC9B,eAAO,yBAAyB,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA,MAC1D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Public options for the `arpCms()` integration, and the resolved, serializable
3
+ * config shape that gets exposed to runtime code via `virtual:arp-cms`.
4
+ *
5
+ * The integration runs in the consumer's `astro.config` (Node, before Vite), so
6
+ * the site passes config explicitly here — typically wired from its own `.env`.
7
+ */
8
+ /** Per-locale display metadata for the language switcher + `<html dir>`. */
9
+ interface LocaleMeta {
10
+ /** Short uppercase code shown in the picker (EN, SL). */
11
+ code: string;
12
+ /** Endonym — the language's name in its own language. */
13
+ native: string;
14
+ /** Exonym in English (optional). */
15
+ english?: string;
16
+ /** Text direction; defaults to 'ltr'. */
17
+ dir?: 'ltr' | 'rtl';
18
+ }
19
+ /** Edge (Cloudflare) `Cache-Control` headers set by the SSR routes. */
20
+ interface CacheConfig {
21
+ /** Successful page/post responses. */
22
+ page: string;
23
+ /** 404 responses (shorter TTL so new CMS content becomes reachable quickly). */
24
+ notFound: string;
25
+ /** Upstream/CMS errors — never cache. */
26
+ error: string;
27
+ /** Preview routes — never cache, never index. */
28
+ preview: string;
29
+ }
30
+ interface ArpCmsOptions {
31
+ /** Base URL of the Laravel CMS API (trailing slashes are trimmed). */
32
+ baseUrl: string;
33
+ /** Multi-site CMS site this deployment serves (slug preferred, id accepted). */
34
+ site: string;
35
+ /** Locale codes this site publishes. The first is the fallback default. */
36
+ locales: readonly string[];
37
+ /** Effective default locale; must be one of `locales`. Defaults to `locales[0]`. */
38
+ defaultLocale?: string;
39
+ /** Navigation menu slug fetched for the site nav. Defaults to `"main"`. */
40
+ menuSlug?: string;
41
+ /** Bearer token for the `preview/*` endpoints; omit to disable preview. */
42
+ previewToken?: string;
43
+ /** Per-locale `Cache-Control` overrides; sensible defaults are applied. */
44
+ cache?: Partial<CacheConfig>;
45
+ /** Per-locale canonical site URLs (no trailing slash); unset → path-prefix routing. */
46
+ websiteUrls?: Record<string, string | undefined>;
47
+ /**
48
+ * Per-content-type, per-locale URL prefixes (e.g. `{ post: { en: 'blog' } }`),
49
+ * mirroring the CMS `/config` `content_type_paths`. Page has no prefix (it
50
+ * lives at the site root). Consumed by {@link contentTypePath}.
51
+ */
52
+ contentTypePaths?: Record<string, Record<string, string | undefined>>;
53
+ /** Per-locale display metadata for the language switcher + RTL handling. */
54
+ localeMeta?: Record<string, LocaleMeta>;
55
+ }
56
+ /** Resolved config — serialized into the `virtual:arp-cms` module at build time. */
57
+ interface ResolvedArpCmsConfig {
58
+ cms: {
59
+ baseUrl: string;
60
+ site: string;
61
+ previewToken?: string;
62
+ menuSlug: string;
63
+ };
64
+ cache: CacheConfig;
65
+ websiteUrls: Record<string, string | undefined>;
66
+ contentTypePaths: Record<string, Record<string, string | undefined>>;
67
+ localeMeta: Record<string, LocaleMeta>;
68
+ locales: readonly string[];
69
+ defaultLocale: string;
70
+ }
71
+
72
+ export type { ArpCmsOptions as A, CacheConfig as C, LocaleMeta as L, ResolvedArpCmsConfig as R };
@@ -0,0 +1,183 @@
1
+ import { Page, Locale, Menu, Post, Webform, PaginatedResponse, PageListItem, Resolved, MediaAsset } from './types.js';
2
+ export { Block, MenuItem, PageMeta, RedirectEnvelope, ResolvedLink, ResolvedNotFound, ResolvedPage, ResolvedPost, ResolvedRedirect, WebformElement } from './types.js';
3
+ import { R as ResolvedArpCmsConfig } from './options-O5HIKY42.js';
4
+
5
+ declare const config: ResolvedArpCmsConfig;
6
+
7
+ declare class CmsNotFoundError extends Error {
8
+ readonly endpoint: string;
9
+ constructor(endpoint: string);
10
+ }
11
+ declare class CmsApiError extends Error {
12
+ readonly endpoint: string;
13
+ readonly status: number;
14
+ constructor(endpoint: string, status: number, message: string);
15
+ }
16
+ declare function getHomepage(locale: Locale): Promise<Page>;
17
+ declare function resolvePath(path: string, locale: Locale): Promise<Resolved>;
18
+ declare function resolvePathPreview(path: string, locale: Locale): Promise<Resolved>;
19
+ declare function getHomepagePreview(locale: Locale): Promise<Page>;
20
+ declare function getPagePreview(path: string, locale: Locale): Promise<Page>;
21
+ declare function getPostPreview(slug: string, locale: Locale): Promise<Post>;
22
+ type PageOrRedirect = {
23
+ kind: 'page';
24
+ page: Page;
25
+ } | {
26
+ kind: 'redirect';
27
+ to: string;
28
+ type: number;
29
+ };
30
+ declare function getPage(path: string, locale: Locale): Promise<PageOrRedirect>;
31
+ declare function listPages(locale: Locale, perPage?: number): Promise<PaginatedResponse<PageListItem>>;
32
+ declare function listPosts(locale: Locale, perPage?: number): Promise<PaginatedResponse<Post>>;
33
+ declare function getPost(slug: string, locale: Locale): Promise<Post>;
34
+ declare function getMenu(slug: string, locale: Locale): Promise<Menu>;
35
+ declare function getWebform(slug: string, locale: Locale): Promise<Webform>;
36
+ interface WebformSubmitInput {
37
+ slug: string;
38
+ locale: Locale;
39
+ payload: Record<string, unknown>;
40
+ }
41
+ declare function submitWebform({ slug, locale, payload, }: WebformSubmitInput): Promise<unknown>;
42
+
43
+ declare function isLocale(value: string | undefined): value is Locale;
44
+ /**
45
+ * Local dev / single-domain mode: locale-prefixed path on the current origin.
46
+ * The effective default locale never gets a prefix.
47
+ */
48
+ declare function localePath(locale: Locale, path?: string): string;
49
+ /**
50
+ * Resolve a locale to a full origin URL when one is configured (WEBSITE_URL_*,
51
+ * surfaced as `config.websiteUrls`). Returns undefined when the locale has no
52
+ * URL set — caller should fall back to {@link localePath}.
53
+ */
54
+ declare function getLocaleSite(locale: Locale): URL | undefined;
55
+ /** Build the canonical URL for a locale + path (origin URL if set, else prefix). */
56
+ declare function getLocaleUrl(locale: Locale, path?: string): string;
57
+ /**
58
+ * URL prefix configured for a content type in a locale (e.g. `post` → `blog`),
59
+ * from `config.contentTypePaths` (Site settings → `/config` `content_type_paths`).
60
+ * Returns undefined when unset — Page has no prefix (it lives at the site root).
61
+ */
62
+ declare function contentTypePath(type: string, locale: Locale): string | undefined;
63
+ /**
64
+ * Resolve the href for a CMS link. Internal links carry `{locale, path}`, so we
65
+ * build the per-locale URL from the local website config; external/manual links
66
+ * (no locale/path) use their literal `url`.
67
+ */
68
+ declare function linkHref(link: {
69
+ url?: string | null;
70
+ locale?: Locale | null;
71
+ path?: string | null;
72
+ }): string;
73
+ /**
74
+ * Strip the locale prefix from a request pathname, returning the CMS-facing
75
+ * path. The CMS itself doesn't know about path prefixes — that's a routing
76
+ * concern. Default-locale paths are returned unprefixed.
77
+ */
78
+ declare function stripLocalePrefix(pathname: string): {
79
+ locale: Locale;
80
+ path: string;
81
+ };
82
+ /**
83
+ * Resolve both the active locale AND the CMS-facing path from the request.
84
+ *
85
+ * 1. Host match against `config.websiteUrls` (production: one origin/prefix
86
+ * per locale) — the host alone determines the locale; the prefix is then
87
+ * stripped to yield the logical path.
88
+ * 2. Path-prefix fallback (`/sl/about` → sl, `/about`) — only when no host
89
+ * matches, i.e. dev/test where there's no per-locale domain.
90
+ */
91
+ declare function resolveLocaleAndPath(url: URL): {
92
+ locale: Locale;
93
+ path: string;
94
+ };
95
+ /** Resolve just the active locale (same rules as {@link resolveLocaleAndPath}). */
96
+ declare function resolveLocale(url: URL): Locale;
97
+ /** True when the locale is configured (in `localeMeta`) as right-to-left. */
98
+ declare function isRTL(locale: Locale): boolean;
99
+ /**
100
+ * UI-strings translation mechanism. The package owns the *shape*; the site owns
101
+ * the *content*. Pass a per-locale dictionary; get a `(locale) => strings`
102
+ * lookup that falls back to the default locale then the first entry.
103
+ *
104
+ * // site src/i18n.ts
105
+ * export const t = makeTranslator({ en: { cta: 'Contact' }, sl: { cta: 'Kontakt' } });
106
+ * // component: const s = t(locale); s.cta
107
+ */
108
+ declare function makeTranslator<T>(dictionary: Partial<Record<string, T>>): (locale: Locale) => T;
109
+ interface LanguageSwitchEntry {
110
+ locale: Locale;
111
+ /** Uppercase code from localeMeta (EN), or the upper-cased locale. */
112
+ code: string;
113
+ /** Endonym from localeMeta, or the locale code. */
114
+ native: string;
115
+ href: string;
116
+ hreflang: string;
117
+ isActive: boolean;
118
+ }
119
+ /**
120
+ * One entry per configured locale, each linking to the equivalent path in that
121
+ * locale (host/prefix-aware). Labels come from `localeMeta` (passed to
122
+ * `arpCms({ localeMeta })`); missing metadata falls back to the locale code.
123
+ */
124
+ declare function languageSwitchEntries(currentUrl: URL): LanguageSwitchEntry[];
125
+
126
+ /**
127
+ * Helpers for the CMS media-asset shape. The DAM MediaAssetPicker returns
128
+ * `MediaAsset | MediaAsset[] | null` even for single-select, so these normalise
129
+ * it for block components. Pure functions — no config, importable anywhere.
130
+ */
131
+
132
+ type MaybeAsset = MediaAsset | MediaAsset[] | null | undefined;
133
+ /** First asset from the picker shape (array or single), or null. */
134
+ declare function firstAsset(m: MaybeAsset): MediaAsset | null;
135
+ /** Best URL for a size, falling back to the original `url`. */
136
+ declare function assetSrc(m: MaybeAsset, size?: 'large' | 'medium' | 'thumbnail' | 'preview'): string | undefined;
137
+ /** Alt text, falling back to the asset title then empty string. */
138
+ declare function assetAlt(m: MaybeAsset): string;
139
+ /**
140
+ * CSS `object-position` from the asset's focal point (DAM stores 0–1 floats),
141
+ * e.g. `"50% 30%"`. Returns undefined when no focal point is set.
142
+ */
143
+ declare function assetFocalPosition(m: MaybeAsset): string | undefined;
144
+
145
+ interface ResolveRequestOptions {
146
+ /** Hit the preview/draft endpoints (sets no-store + noindex). */
147
+ preview?: boolean;
148
+ }
149
+ interface ResolveRequestResult {
150
+ /** Active locale resolved from host/path. */
151
+ locale: Locale;
152
+ /** CMS-facing logical path (locale prefix stripped). */
153
+ path: string;
154
+ /** The resolve envelope, or null if the CMS call errored. */
155
+ resolved: Resolved | null;
156
+ /** The site nav menu (config.cms.menuSlug), or null if unavailable. */
157
+ menu: Menu | null;
158
+ /** Set when the CMS returned a redirect — the caller should `Astro.redirect`. */
159
+ redirect: {
160
+ to: string;
161
+ code: number;
162
+ } | null;
163
+ /** HTTP status that was set on the response. */
164
+ status: number;
165
+ /** Human-readable error message when the CMS call failed. */
166
+ error: string | null;
167
+ }
168
+ interface RequestContext {
169
+ url: URL;
170
+ response: {
171
+ status?: number;
172
+ headers: Headers;
173
+ };
174
+ }
175
+ /**
176
+ * Resolve an incoming request end-to-end: locale/path → CMS `resolve` lookup →
177
+ * nav menu → response status + edge `Cache-Control` headers. Returns the data
178
+ * for the route to render; on a CMS redirect it returns `redirect` for the
179
+ * caller to `return Astro.redirect(redirect.to, redirect.code)`.
180
+ */
181
+ declare function resolveRequest(ctx: RequestContext, options?: ResolveRequestOptions): Promise<ResolveRequestResult>;
182
+
183
+ export { CmsApiError, CmsNotFoundError, type LanguageSwitchEntry, Locale, MediaAsset, Menu, Page, PageListItem, type PageOrRedirect, PaginatedResponse, Post, type ResolveRequestOptions, type ResolveRequestResult, Resolved, Webform, type WebformSubmitInput, assetAlt, assetFocalPosition, assetSrc, config, contentTypePath, firstAsset, getHomepage, getHomepagePreview, getLocaleSite, getLocaleUrl, getMenu, getPage, getPagePreview, getPost, getPostPreview, getWebform, isLocale, isRTL, languageSwitchEntries, linkHref, listPages, listPosts, localePath, makeTranslator, resolveLocale, resolveLocaleAndPath, resolvePath, resolvePathPreview, resolveRequest, stripLocalePrefix, submitWebform };
@@ -0,0 +1,357 @@
1
+ // src/config.ts
2
+ import { config as virtualConfig } from "virtual:arp-cms";
3
+ var config = virtualConfig;
4
+
5
+ // src/client.ts
6
+ var API_PREFIX = `/api/cms/v1/sites/${encodeURIComponent(config.cms.site)}`;
7
+ var CmsNotFoundError = class extends Error {
8
+ constructor(endpoint) {
9
+ super(`CMS resource not found: ${endpoint}`);
10
+ this.endpoint = endpoint;
11
+ this.name = "CmsNotFoundError";
12
+ }
13
+ endpoint;
14
+ };
15
+ var CmsApiError = class extends Error {
16
+ constructor(endpoint, status, message) {
17
+ super(`CMS API error (${status}) on ${endpoint}: ${message}`);
18
+ this.endpoint = endpoint;
19
+ this.status = status;
20
+ this.name = "CmsApiError";
21
+ }
22
+ endpoint;
23
+ status;
24
+ };
25
+ async function fetchJson(path, options = {}) {
26
+ const url = new URL(`${API_PREFIX}${path}`, config.cms.baseUrl);
27
+ if (options.query) {
28
+ for (const [key, value] of Object.entries(options.query)) {
29
+ if (value !== void 0) {
30
+ url.searchParams.set(key, String(value));
31
+ }
32
+ }
33
+ }
34
+ const headers = {
35
+ Accept: "application/json"
36
+ };
37
+ if (options.locale) {
38
+ headers["Accept-Language"] = options.locale;
39
+ if (!url.searchParams.has("locale")) {
40
+ url.searchParams.set("locale", options.locale);
41
+ }
42
+ }
43
+ if (options.preview) {
44
+ const token = config.cms.previewToken;
45
+ if (!token) {
46
+ throw new Error("CMS preview token is not set (arpCms({ previewToken }))");
47
+ }
48
+ headers.Authorization = `Bearer ${token}`;
49
+ }
50
+ const response = await fetch(url.toString(), { headers });
51
+ if (response.status === 404) {
52
+ throw new CmsNotFoundError(url.pathname);
53
+ }
54
+ if (!response.ok) {
55
+ throw new CmsApiError(url.pathname, response.status, await response.text());
56
+ }
57
+ return response.json();
58
+ }
59
+ function unwrap(payload) {
60
+ if (payload && typeof payload === "object" && "data" in payload) {
61
+ return payload.data;
62
+ }
63
+ return payload;
64
+ }
65
+ async function getHomepage(locale) {
66
+ return unwrap(await fetchJson("/pages/_homepage", { locale }));
67
+ }
68
+ async function resolvePath(path, locale) {
69
+ try {
70
+ return await fetchJson("/resolve", { locale, query: { path } });
71
+ } catch (e) {
72
+ if (e instanceof CmsNotFoundError) {
73
+ return { type: "not_found" };
74
+ }
75
+ throw e;
76
+ }
77
+ }
78
+ async function resolvePathPreview(path, locale) {
79
+ try {
80
+ return await fetchJson("/preview/resolve", {
81
+ locale,
82
+ query: { path },
83
+ preview: true
84
+ });
85
+ } catch (e) {
86
+ if (e instanceof CmsNotFoundError) {
87
+ return { type: "not_found" };
88
+ }
89
+ throw e;
90
+ }
91
+ }
92
+ async function getHomepagePreview(locale) {
93
+ return unwrap(
94
+ await fetchJson("/preview/pages/_homepage", { locale, preview: true })
95
+ );
96
+ }
97
+ async function getPagePreview(path, locale) {
98
+ const normalized = path.replace(/^\/+/, "");
99
+ return unwrap(
100
+ await fetchJson(`/preview/pages/${normalized}`, { locale, preview: true })
101
+ );
102
+ }
103
+ async function getPostPreview(slug, locale) {
104
+ return unwrap(
105
+ await fetchJson(`/preview/posts/${slug}`, { locale, preview: true })
106
+ );
107
+ }
108
+ async function getPage(path, locale) {
109
+ const normalized = path.replace(/^\/+/, "");
110
+ const payload = await fetchJson(`/pages/${normalized}`, {
111
+ locale
112
+ });
113
+ if ("redirect" in payload) {
114
+ return { kind: "redirect", to: payload.redirect.to, type: payload.redirect.type };
115
+ }
116
+ return { kind: "page", page: unwrap(payload) };
117
+ }
118
+ async function listPages(locale, perPage = 100) {
119
+ return fetchJson("/pages", {
120
+ locale,
121
+ query: { per_page: perPage }
122
+ });
123
+ }
124
+ async function listPosts(locale, perPage = 25) {
125
+ return fetchJson("/posts", {
126
+ locale,
127
+ query: { per_page: perPage }
128
+ });
129
+ }
130
+ async function getPost(slug, locale) {
131
+ return unwrap(await fetchJson(`/posts/${slug}`, { locale }));
132
+ }
133
+ async function getMenu(slug, locale) {
134
+ return unwrap(await fetchJson(`/menus/${slug}`, { locale }));
135
+ }
136
+ async function getWebform(slug, locale) {
137
+ return unwrap(await fetchJson(`/webforms/${slug}`, { locale }));
138
+ }
139
+ async function submitWebform({
140
+ slug,
141
+ locale,
142
+ payload
143
+ }) {
144
+ const url = new URL(`${API_PREFIX}/webforms/${slug}/submit`, config.cms.baseUrl);
145
+ const response = await fetch(url.toString(), {
146
+ method: "POST",
147
+ headers: {
148
+ Accept: "application/json",
149
+ "Content-Type": "application/json",
150
+ "Accept-Language": locale
151
+ },
152
+ body: JSON.stringify(payload)
153
+ });
154
+ if (!response.ok) {
155
+ throw new CmsApiError(url.pathname, response.status, await response.text());
156
+ }
157
+ return response.json();
158
+ }
159
+
160
+ // src/i18n.ts
161
+ function isLocale(value) {
162
+ return !!value && config.locales.includes(value);
163
+ }
164
+ function localePath(locale, path = "/") {
165
+ const clean = path.startsWith("/") ? path : `/${path}`;
166
+ if (locale === config.defaultLocale) {
167
+ return clean;
168
+ }
169
+ return clean === "/" ? `/${locale}/` : `/${locale}${clean}`;
170
+ }
171
+ function getLocaleSite(locale) {
172
+ const raw = config.websiteUrls[locale];
173
+ if (!raw) return void 0;
174
+ try {
175
+ return new URL(raw);
176
+ } catch {
177
+ return void 0;
178
+ }
179
+ }
180
+ function getLocaleUrl(locale, path = "/") {
181
+ const site = getLocaleSite(locale);
182
+ if (!site) {
183
+ return localePath(locale, path);
184
+ }
185
+ const clean = path.startsWith("/") ? path : `/${path}`;
186
+ const sitePrefix = site.pathname.replace(/\/+$/, "");
187
+ return `${site.origin}${sitePrefix}${clean === "/" ? "" : clean}`;
188
+ }
189
+ function contentTypePath(type, locale) {
190
+ return config.contentTypePaths[type]?.[locale] ?? void 0;
191
+ }
192
+ function linkHref(link) {
193
+ if (link.locale && link.path != null && isLocale(link.locale)) {
194
+ return getLocaleUrl(link.locale, link.path);
195
+ }
196
+ return link.url ?? "#";
197
+ }
198
+ function stripLocalePrefix(pathname) {
199
+ const segments = pathname.split("/").filter(Boolean);
200
+ const first = segments[0];
201
+ if (first && first !== config.defaultLocale && config.locales.includes(first)) {
202
+ const rest = segments.slice(1).join("/");
203
+ return { locale: first, path: rest === "" ? "/" : `/${rest}` };
204
+ }
205
+ return { locale: config.defaultLocale, path: pathname === "" ? "/" : pathname };
206
+ }
207
+ function resolveLocaleAndPath(url) {
208
+ const requestHost = url.hostname.toLowerCase();
209
+ const requestPath = url.pathname || "/";
210
+ for (const code of config.locales) {
211
+ const site = getLocaleSite(code);
212
+ if (!site || site.hostname.toLowerCase() !== requestHost) {
213
+ continue;
214
+ }
215
+ const sitePrefix = site.pathname.replace(/\/+$/, "");
216
+ if (sitePrefix === "") {
217
+ return { locale: code, path: requestPath };
218
+ }
219
+ if (requestPath === sitePrefix || requestPath.startsWith(sitePrefix + "/")) {
220
+ return { locale: code, path: requestPath.slice(sitePrefix.length) || "/" };
221
+ }
222
+ }
223
+ return stripLocalePrefix(requestPath);
224
+ }
225
+ function resolveLocale(url) {
226
+ return resolveLocaleAndPath(url).locale;
227
+ }
228
+ function isRTL(locale) {
229
+ return (config.localeMeta[locale]?.dir ?? "ltr") === "rtl";
230
+ }
231
+ function makeTranslator(dictionary) {
232
+ return (locale) => dictionary[locale] ?? dictionary[config.defaultLocale] ?? Object.values(dictionary)[0];
233
+ }
234
+ function languageSwitchEntries(currentUrl) {
235
+ const { locale: currentLocale, path: logicalPath } = resolveLocaleAndPath(currentUrl);
236
+ return config.locales.map((l) => {
237
+ const site = getLocaleSite(l);
238
+ const href = site ? `${site.origin}${site.pathname.replace(/\/+$/, "")}${logicalPath === "/" ? "" : logicalPath}` : localePath(l, logicalPath);
239
+ const meta = config.localeMeta[l];
240
+ return {
241
+ locale: l,
242
+ code: meta?.code ?? l.toUpperCase(),
243
+ native: meta?.native ?? l,
244
+ href,
245
+ hreflang: l,
246
+ isActive: l === currentLocale
247
+ };
248
+ });
249
+ }
250
+
251
+ // src/media.ts
252
+ function firstAsset(m) {
253
+ if (!m) return null;
254
+ return Array.isArray(m) ? m[0] ?? null : m;
255
+ }
256
+ function assetSrc(m, size = "large") {
257
+ const a = firstAsset(m);
258
+ if (!a) return void 0;
259
+ return a[size] ?? a.url ?? void 0;
260
+ }
261
+ function assetAlt(m) {
262
+ const a = firstAsset(m);
263
+ return a?.alt ?? a?.title ?? "";
264
+ }
265
+ function assetFocalPosition(m) {
266
+ const focal = firstAsset(m)?.focal;
267
+ if (!focal || focal.x == null || focal.y == null) return void 0;
268
+ return `${(focal.x * 100).toFixed(2)}% ${(focal.y * 100).toFixed(2)}%`;
269
+ }
270
+
271
+ // src/runtime.ts
272
+ async function resolveRequest(ctx, options = {}) {
273
+ const { locale, path } = resolveLocaleAndPath(ctx.url);
274
+ let resolved = null;
275
+ let error = null;
276
+ let status = 200;
277
+ try {
278
+ resolved = options.preview ? await resolvePathPreview(path, locale) : await resolvePath(path, locale);
279
+ } catch (e) {
280
+ if (e instanceof CmsApiError) {
281
+ error = `CMS API returned ${e.status}.`;
282
+ status = 502;
283
+ } else {
284
+ error = e instanceof Error ? e.message : "Unknown error.";
285
+ status = 500;
286
+ }
287
+ }
288
+ if (resolved?.type === "redirect") {
289
+ ctx.response.headers.set("Cache-Control", config.cache.page);
290
+ return {
291
+ locale,
292
+ path,
293
+ resolved,
294
+ menu: null,
295
+ redirect: { to: resolved.to, code: resolved.code },
296
+ status,
297
+ error
298
+ };
299
+ }
300
+ let menu = null;
301
+ try {
302
+ menu = await getMenu(config.cms.menuSlug, locale);
303
+ } catch {
304
+ }
305
+ if (resolved?.type === "not_found") {
306
+ status = 404;
307
+ }
308
+ ctx.response.status = status;
309
+ ctx.response.headers.set("Cache-Control", cacheHeaderFor(resolved, error, options.preview));
310
+ if (options.preview) {
311
+ ctx.response.headers.set("X-Robots-Tag", "noindex, nofollow");
312
+ }
313
+ return { locale, path, resolved, menu, redirect: null, status, error };
314
+ }
315
+ function cacheHeaderFor(resolved, error, preview) {
316
+ if (preview) return config.cache.preview;
317
+ if (error || !resolved) return config.cache.error;
318
+ if (resolved.type === "page" || resolved.type === "post") return config.cache.page;
319
+ if (resolved.type === "not_found") return config.cache.notFound;
320
+ return config.cache.error;
321
+ }
322
+ export {
323
+ CmsApiError,
324
+ CmsNotFoundError,
325
+ assetAlt,
326
+ assetFocalPosition,
327
+ assetSrc,
328
+ config,
329
+ contentTypePath,
330
+ firstAsset,
331
+ getHomepage,
332
+ getHomepagePreview,
333
+ getLocaleSite,
334
+ getLocaleUrl,
335
+ getMenu,
336
+ getPage,
337
+ getPagePreview,
338
+ getPost,
339
+ getPostPreview,
340
+ getWebform,
341
+ isLocale,
342
+ isRTL,
343
+ languageSwitchEntries,
344
+ linkHref,
345
+ listPages,
346
+ listPosts,
347
+ localePath,
348
+ makeTranslator,
349
+ resolveLocale,
350
+ resolveLocaleAndPath,
351
+ resolvePath,
352
+ resolvePathPreview,
353
+ resolveRequest,
354
+ stripLocalePrefix,
355
+ submitWebform
356
+ };
357
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/client.ts","../src/i18n.ts","../src/media.ts","../src/runtime.ts"],"sourcesContent":["/**\n * The resolved runtime config, provided by the `arpCms()` integration through\n * the `virtual:arp-cms` module. Runtime code (client, i18n) reads it from here.\n *\n * The value is imported from the virtual module but re-exported with an explicit\n * type so the emitted `.d.ts` carries a concrete `ResolvedArpCmsConfig` — never a\n * `from 'virtual:arp-cms'` re-export, which consumers couldn't resolve.\n *\n * This module imports a virtual module, so it (and anything that re-exports it)\n * must NOT be pulled into `astro.config` — keep the package's `.` entry free of\n * it. Runtime consumers reach it via the `@arpsw/astro-cms/runtime` subpath.\n */\nimport { config as virtualConfig } from 'virtual:arp-cms';\nimport type { ResolvedArpCmsConfig } from './options';\n\nexport const config: ResolvedArpCmsConfig = virtualConfig;\n","/**\n * Typed client for the ARP CMS API. Connection + site come from the resolved\n * config (`virtual:arp-cms`); every content endpoint is scoped to the site via\n * `/api/cms/v1/sites/{site}`.\n */\nimport { config } from './config';\nimport type {\n Locale,\n Menu,\n Page,\n PageListItem,\n PaginatedResponse,\n Post,\n RedirectEnvelope,\n Resolved,\n Webform,\n} from './types';\n\nconst API_PREFIX = `/api/cms/v1/sites/${encodeURIComponent(config.cms.site)}`;\n\nexport class CmsNotFoundError extends Error {\n constructor(public readonly endpoint: string) {\n super(`CMS resource not found: ${endpoint}`);\n this.name = 'CmsNotFoundError';\n }\n}\n\nexport class CmsApiError extends Error {\n constructor(\n public readonly endpoint: string,\n public readonly status: number,\n message: string,\n ) {\n super(`CMS API error (${status}) on ${endpoint}: ${message}`);\n this.name = 'CmsApiError';\n }\n}\n\ninterface FetchOptions {\n locale?: Locale;\n query?: Record<string, string | number | undefined>;\n preview?: boolean;\n}\n\nasync function fetchJson<T>(path: string, options: FetchOptions = {}): Promise<T> {\n const url = new URL(`${API_PREFIX}${path}`, config.cms.baseUrl);\n\n if (options.query) {\n for (const [key, value] of Object.entries(options.query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n };\n\n if (options.locale) {\n headers['Accept-Language'] = options.locale;\n // Also send locale as a query param. The CMS's cached read routes are\n // wrapped in Spatie ResponseCache, which hashes by URL+method but not by\n // headers — relying on Accept-Language alone causes cross-locale cache\n // poisoning. ?locale=… makes the cache key vary by locale, and the API's\n // ResolvesApiLocale concern already honours the query param explicitly.\n if (!url.searchParams.has('locale')) {\n url.searchParams.set('locale', options.locale);\n }\n }\n\n if (options.preview) {\n const token = config.cms.previewToken;\n if (!token) {\n throw new Error('CMS preview token is not set (arpCms({ previewToken }))');\n }\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(url.toString(), { headers });\n\n if (response.status === 404) {\n throw new CmsNotFoundError(url.pathname);\n }\n\n if (!response.ok) {\n throw new CmsApiError(url.pathname, response.status, await response.text());\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction unwrap<T>(payload: { data: T } | T): T {\n if (payload && typeof payload === 'object' && 'data' in payload) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n}\n\n// --- Pages -----------------------------------------------------------------\n\nexport async function getHomepage(locale: Locale): Promise<Page> {\n return unwrap(await fetchJson<{ data: Page }>('/pages/_homepage', { locale }));\n}\n\n// --- Resolver --------------------------------------------------------------\n//\n// One call per incoming URL. Laravel decides whether the path is a redirect, a\n// page, a post, or 404. The catch-all switches on the envelope's .type.\n\nexport async function resolvePath(path: string, locale: Locale): Promise<Resolved> {\n try {\n return await fetchJson<Resolved>('/resolve', { locale, query: { path } });\n } catch (e) {\n if (e instanceof CmsNotFoundError) {\n return { type: 'not_found' };\n }\n throw e;\n }\n}\n\nexport async function resolvePathPreview(path: string, locale: Locale): Promise<Resolved> {\n try {\n return await fetchJson<Resolved>('/preview/resolve', {\n locale,\n query: { path },\n preview: true,\n });\n } catch (e) {\n if (e instanceof CmsNotFoundError) {\n return { type: 'not_found' };\n }\n throw e;\n }\n}\n\n// --- Preview (draft) -------------------------------------------------------\n\nexport async function getHomepagePreview(locale: Locale): Promise<Page> {\n return unwrap(\n await fetchJson<{ data: Page }>('/preview/pages/_homepage', { locale, preview: true }),\n );\n}\n\nexport async function getPagePreview(path: string, locale: Locale): Promise<Page> {\n const normalized = path.replace(/^\\/+/, '');\n return unwrap(\n await fetchJson<{ data: Page }>(`/preview/pages/${normalized}`, { locale, preview: true }),\n );\n}\n\nexport async function getPostPreview(slug: string, locale: Locale): Promise<Post> {\n return unwrap(\n await fetchJson<{ data: Post }>(`/preview/posts/${slug}`, { locale, preview: true }),\n );\n}\n\nexport type PageOrRedirect =\n | { kind: 'page'; page: Page }\n | { kind: 'redirect'; to: string; type: number };\n\nexport async function getPage(path: string, locale: Locale): Promise<PageOrRedirect> {\n const normalized = path.replace(/^\\/+/, '');\n const payload = await fetchJson<{ data: Page } | RedirectEnvelope>(`/pages/${normalized}`, {\n locale,\n });\n\n if ('redirect' in payload) {\n return { kind: 'redirect', to: payload.redirect.to, type: payload.redirect.type };\n }\n\n return { kind: 'page', page: unwrap(payload) };\n}\n\nexport async function listPages(\n locale: Locale,\n perPage = 100,\n): Promise<PaginatedResponse<PageListItem>> {\n return fetchJson<PaginatedResponse<PageListItem>>('/pages', {\n locale,\n query: { per_page: perPage },\n });\n}\n\n// --- Posts -----------------------------------------------------------------\n\nexport async function listPosts(locale: Locale, perPage = 25): Promise<PaginatedResponse<Post>> {\n return fetchJson<PaginatedResponse<Post>>('/posts', {\n locale,\n query: { per_page: perPage },\n });\n}\n\nexport async function getPost(slug: string, locale: Locale): Promise<Post> {\n return unwrap(await fetchJson<{ data: Post }>(`/posts/${slug}`, { locale }));\n}\n\n// --- Menus -----------------------------------------------------------------\n\nexport async function getMenu(slug: string, locale: Locale): Promise<Menu> {\n return unwrap(await fetchJson<{ data: Menu }>(`/menus/${slug}`, { locale }));\n}\n\n// --- Webforms --------------------------------------------------------------\n\nexport async function getWebform(slug: string, locale: Locale): Promise<Webform> {\n return unwrap(await fetchJson<{ data: Webform }>(`/webforms/${slug}`, { locale }));\n}\n\nexport interface WebformSubmitInput {\n slug: string;\n locale: Locale;\n payload: Record<string, unknown>;\n}\n\nexport async function submitWebform({\n slug,\n locale,\n payload,\n}: WebformSubmitInput): Promise<unknown> {\n const url = new URL(`${API_PREFIX}/webforms/${slug}/submit`, config.cms.baseUrl);\n const response = await fetch(url.toString(), {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'Accept-Language': locale,\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n throw new CmsApiError(url.pathname, response.status, await response.text());\n }\n\n return response.json();\n}\n","/**\n * Locale + path resolution and per-locale URL building, all driven by the\n * resolved config (locales, defaultLocale, websiteUrls).\n *\n * Locale display metadata (native names, RTL `dir`) is intentionally NOT here —\n * that's site UI data, owned by the consuming site's language switcher.\n */\nimport { config } from './config';\nimport type { Locale } from './types';\n\nexport function isLocale(value: string | undefined): value is Locale {\n return !!value && config.locales.includes(value);\n}\n\n/**\n * Local dev / single-domain mode: locale-prefixed path on the current origin.\n * The effective default locale never gets a prefix.\n */\nexport function localePath(locale: Locale, path: string = '/'): string {\n const clean = path.startsWith('/') ? path : `/${path}`;\n if (locale === config.defaultLocale) {\n return clean;\n }\n return clean === '/' ? `/${locale}/` : `/${locale}${clean}`;\n}\n\n/**\n * Resolve a locale to a full origin URL when one is configured (WEBSITE_URL_*,\n * surfaced as `config.websiteUrls`). Returns undefined when the locale has no\n * URL set — caller should fall back to {@link localePath}.\n */\nexport function getLocaleSite(locale: Locale): URL | undefined {\n const raw = config.websiteUrls[locale];\n if (!raw) return undefined;\n try {\n return new URL(raw);\n } catch {\n return undefined;\n }\n}\n\n/** Build the canonical URL for a locale + path (origin URL if set, else prefix). */\nexport function getLocaleUrl(locale: Locale, path: string = '/'): string {\n const site = getLocaleSite(locale);\n if (!site) {\n return localePath(locale, path);\n }\n const clean = path.startsWith('/') ? path : `/${path}`;\n const sitePrefix = site.pathname.replace(/\\/+$/, '');\n return `${site.origin}${sitePrefix}${clean === '/' ? '' : clean}`;\n}\n\n/**\n * URL prefix configured for a content type in a locale (e.g. `post` → `blog`),\n * from `config.contentTypePaths` (Site settings → `/config` `content_type_paths`).\n * Returns undefined when unset — Page has no prefix (it lives at the site root).\n */\nexport function contentTypePath(type: string, locale: Locale): string | undefined {\n return config.contentTypePaths[type]?.[locale] ?? undefined;\n}\n\n/**\n * Resolve the href for a CMS link. Internal links carry `{locale, path}`, so we\n * build the per-locale URL from the local website config; external/manual links\n * (no locale/path) use their literal `url`.\n */\nexport function linkHref(link: {\n url?: string | null;\n locale?: Locale | null;\n path?: string | null;\n}): string {\n if (link.locale && link.path != null && isLocale(link.locale)) {\n return getLocaleUrl(link.locale, link.path);\n }\n return link.url ?? '#';\n}\n\n/**\n * Strip the locale prefix from a request pathname, returning the CMS-facing\n * path. The CMS itself doesn't know about path prefixes — that's a routing\n * concern. Default-locale paths are returned unprefixed.\n */\nexport function stripLocalePrefix(pathname: string): { locale: Locale; path: string } {\n const segments = pathname.split('/').filter(Boolean);\n const first = segments[0];\n\n if (first && first !== config.defaultLocale && config.locales.includes(first)) {\n const rest = segments.slice(1).join('/');\n return { locale: first, path: rest === '' ? '/' : `/${rest}` };\n }\n\n return { locale: config.defaultLocale, path: pathname === '' ? '/' : pathname };\n}\n\n/**\n * Resolve both the active locale AND the CMS-facing path from the request.\n *\n * 1. Host match against `config.websiteUrls` (production: one origin/prefix\n * per locale) — the host alone determines the locale; the prefix is then\n * stripped to yield the logical path.\n * 2. Path-prefix fallback (`/sl/about` → sl, `/about`) — only when no host\n * matches, i.e. dev/test where there's no per-locale domain.\n */\nexport function resolveLocaleAndPath(url: URL): { locale: Locale; path: string } {\n const requestHost = url.hostname.toLowerCase();\n const requestPath = url.pathname || '/';\n\n for (const code of config.locales) {\n const site = getLocaleSite(code);\n if (!site || site.hostname.toLowerCase() !== requestHost) {\n continue;\n }\n\n const sitePrefix = site.pathname.replace(/\\/+$/, '');\n if (sitePrefix === '') {\n return { locale: code, path: requestPath };\n }\n if (requestPath === sitePrefix || requestPath.startsWith(sitePrefix + '/')) {\n return { locale: code, path: requestPath.slice(sitePrefix.length) || '/' };\n }\n }\n\n return stripLocalePrefix(requestPath);\n}\n\n/** Resolve just the active locale (same rules as {@link resolveLocaleAndPath}). */\nexport function resolveLocale(url: URL): Locale {\n return resolveLocaleAndPath(url).locale;\n}\n\n/** True when the locale is configured (in `localeMeta`) as right-to-left. */\nexport function isRTL(locale: Locale): boolean {\n return (config.localeMeta[locale]?.dir ?? 'ltr') === 'rtl';\n}\n\n/**\n * UI-strings translation mechanism. The package owns the *shape*; the site owns\n * the *content*. Pass a per-locale dictionary; get a `(locale) => strings`\n * lookup that falls back to the default locale then the first entry.\n *\n * // site src/i18n.ts\n * export const t = makeTranslator({ en: { cta: 'Contact' }, sl: { cta: 'Kontakt' } });\n * // component: const s = t(locale); s.cta\n */\nexport function makeTranslator<T>(dictionary: Partial<Record<string, T>>): (locale: Locale) => T {\n return (locale: Locale): T =>\n dictionary[locale] ?? dictionary[config.defaultLocale] ?? Object.values(dictionary)[0]!;\n}\n\nexport interface LanguageSwitchEntry {\n locale: Locale;\n /** Uppercase code from localeMeta (EN), or the upper-cased locale. */\n code: string;\n /** Endonym from localeMeta, or the locale code. */\n native: string;\n href: string;\n hreflang: string;\n isActive: boolean;\n}\n\n/**\n * One entry per configured locale, each linking to the equivalent path in that\n * locale (host/prefix-aware). Labels come from `localeMeta` (passed to\n * `arpCms({ localeMeta })`); missing metadata falls back to the locale code.\n */\nexport function languageSwitchEntries(currentUrl: URL): LanguageSwitchEntry[] {\n const { locale: currentLocale, path: logicalPath } = resolveLocaleAndPath(currentUrl);\n\n return config.locales.map((l) => {\n const site = getLocaleSite(l);\n const href = site\n ? `${site.origin}${site.pathname.replace(/\\/+$/, '')}${logicalPath === '/' ? '' : logicalPath}`\n : localePath(l, logicalPath);\n const meta = config.localeMeta[l];\n\n return {\n locale: l,\n code: meta?.code ?? l.toUpperCase(),\n native: meta?.native ?? l,\n href,\n hreflang: l,\n isActive: l === currentLocale,\n };\n });\n}\n","/**\n * Helpers for the CMS media-asset shape. The DAM MediaAssetPicker returns\n * `MediaAsset | MediaAsset[] | null` even for single-select, so these normalise\n * it for block components. Pure functions — no config, importable anywhere.\n */\nimport type { MediaAsset } from './types';\n\ntype MaybeAsset = MediaAsset | MediaAsset[] | null | undefined;\n\n/** First asset from the picker shape (array or single), or null. */\nexport function firstAsset(m: MaybeAsset): MediaAsset | null {\n if (!m) return null;\n return Array.isArray(m) ? (m[0] ?? null) : m;\n}\n\n/** Best URL for a size, falling back to the original `url`. */\nexport function assetSrc(\n m: MaybeAsset,\n size: 'large' | 'medium' | 'thumbnail' | 'preview' = 'large',\n): string | undefined {\n const a = firstAsset(m);\n if (!a) return undefined;\n return (a[size] as string | null | undefined) ?? a.url ?? undefined;\n}\n\n/** Alt text, falling back to the asset title then empty string. */\nexport function assetAlt(m: MaybeAsset): string {\n const a = firstAsset(m);\n return a?.alt ?? a?.title ?? '';\n}\n\n/**\n * CSS `object-position` from the asset's focal point (DAM stores 0–1 floats),\n * e.g. `\"50% 30%\"`. Returns undefined when no focal point is set.\n */\nexport function assetFocalPosition(m: MaybeAsset): string | undefined {\n const focal = firstAsset(m)?.focal;\n if (!focal || focal.x == null || focal.y == null) return undefined;\n return `${(focal.x * 100).toFixed(2)}% ${(focal.y * 100).toFixed(2)}%`;\n}\n","/**\n * Runtime entry (`@arpsw/astro-cms/runtime`) — import from pages/components.\n *\n * Re-exports the CMS client + i18n helpers, and adds `resolveRequest()`: the\n * one-call request handler a site's catch-all route uses. This module reads the\n * resolved config from `virtual:arp-cms`, so it must NOT be imported from\n * `astro.config` (use the `.` entry — the integration — there).\n */\nimport { config } from './config';\nimport { CmsApiError, getMenu, resolvePath, resolvePathPreview } from './client';\nimport { resolveLocaleAndPath } from './i18n';\nimport type { Locale, Menu, Resolved } from './types';\n\nexport interface ResolveRequestOptions {\n /** Hit the preview/draft endpoints (sets no-store + noindex). */\n preview?: boolean;\n}\n\nexport interface ResolveRequestResult {\n /** Active locale resolved from host/path. */\n locale: Locale;\n /** CMS-facing logical path (locale prefix stripped). */\n path: string;\n /** The resolve envelope, or null if the CMS call errored. */\n resolved: Resolved | null;\n /** The site nav menu (config.cms.menuSlug), or null if unavailable. */\n menu: Menu | null;\n /** Set when the CMS returned a redirect — the caller should `Astro.redirect`. */\n redirect: { to: string; code: number } | null;\n /** HTTP status that was set on the response. */\n status: number;\n /** Human-readable error message when the CMS call failed. */\n error: string | null;\n}\n\ninterface RequestContext {\n url: URL;\n // `status` is optional to match Astro's `Astro.response` (number | undefined).\n response: { status?: number; headers: Headers };\n}\n\n/**\n * Resolve an incoming request end-to-end: locale/path → CMS `resolve` lookup →\n * nav menu → response status + edge `Cache-Control` headers. Returns the data\n * for the route to render; on a CMS redirect it returns `redirect` for the\n * caller to `return Astro.redirect(redirect.to, redirect.code)`.\n */\nexport async function resolveRequest(\n ctx: RequestContext,\n options: ResolveRequestOptions = {},\n): Promise<ResolveRequestResult> {\n const { locale, path } = resolveLocaleAndPath(ctx.url);\n\n let resolved: Resolved | null = null;\n let error: string | null = null;\n let status = 200;\n\n try {\n resolved = options.preview\n ? await resolvePathPreview(path, locale)\n : await resolvePath(path, locale);\n } catch (e) {\n if (e instanceof CmsApiError) {\n error = `CMS API returned ${e.status}.`;\n status = 502;\n } else {\n error = e instanceof Error ? e.message : 'Unknown error.';\n status = 500;\n }\n }\n\n if (resolved?.type === 'redirect') {\n ctx.response.headers.set('Cache-Control', config.cache.page);\n return {\n locale,\n path,\n resolved,\n menu: null,\n redirect: { to: resolved.to, code: resolved.code },\n status,\n error,\n };\n }\n\n let menu: Menu | null = null;\n try {\n menu = await getMenu(config.cms.menuSlug, locale);\n } catch {\n // Non-fatal — render without a nav menu.\n }\n\n if (resolved?.type === 'not_found') {\n status = 404;\n }\n ctx.response.status = status;\n\n ctx.response.headers.set('Cache-Control', cacheHeaderFor(resolved, error, options.preview));\n if (options.preview) {\n ctx.response.headers.set('X-Robots-Tag', 'noindex, nofollow');\n }\n\n return { locale, path, resolved, menu, redirect: null, status, error };\n}\n\nfunction cacheHeaderFor(\n resolved: Resolved | null,\n error: string | null,\n preview?: boolean,\n): string {\n if (preview) return config.cache.preview;\n if (error || !resolved) return config.cache.error;\n if (resolved.type === 'page' || resolved.type === 'post') return config.cache.page;\n if (resolved.type === 'not_found') return config.cache.notFound;\n return config.cache.error;\n}\n\nexport { config } from './config';\nexport * from './client';\nexport * from './i18n';\nexport * from './media';\nexport type * from './types';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,168 @@
1
+ type Locale = string;
2
+ interface MediaAsset {
3
+ id: number;
4
+ type?: string | null;
5
+ title?: string | null;
6
+ caption?: string | null;
7
+ /** Per-use alt (from media_asset_options.alt_text), falls back to title. */
8
+ alt?: string | null;
9
+ url: string | null;
10
+ thumbnail?: string | null;
11
+ medium?: string | null;
12
+ large?: string | null;
13
+ preview?: string | null;
14
+ file_name?: string | null;
15
+ mime_type?: string | null;
16
+ focal?: {
17
+ x: number | null;
18
+ y: number | null;
19
+ } | null;
20
+ [key: string]: unknown;
21
+ }
22
+ interface ResolvedLink {
23
+ url: string | null;
24
+ /** Target locale for internal links; null for external/manual links. */
25
+ locale: Locale | null;
26
+ /** Locale-relative logical path for internal links (the frontend builds the
27
+ * real href from {locale, path}); null for external/manual links. */
28
+ path: string | null;
29
+ open_in_new_tab: boolean;
30
+ linkable_type: string | null;
31
+ linkable_id: number | null;
32
+ }
33
+ interface Block<T = Record<string, unknown>> {
34
+ type: string;
35
+ data: T;
36
+ }
37
+ interface PageMeta {
38
+ title: string | null;
39
+ description: string | null;
40
+ /**
41
+ * After BlockSerializer resolves the DAM picker shape, og_image is an array
42
+ * (the picker is array-based even for single-select). We accept the legacy
43
+ * single-object shape too for forward/backward compatibility.
44
+ */
45
+ og_image: MediaAsset | MediaAsset[] | null;
46
+ }
47
+ interface Page {
48
+ id: number;
49
+ slug: string;
50
+ locale: Locale;
51
+ title: string;
52
+ is_homepage: boolean;
53
+ meta: PageMeta;
54
+ blocks: Block[];
55
+ updated_at: string | null;
56
+ }
57
+ interface PageListItem {
58
+ id: number;
59
+ slug: string;
60
+ locale: Locale;
61
+ title: string;
62
+ is_homepage: boolean;
63
+ updated_at: string | null;
64
+ }
65
+ interface RedirectEnvelope {
66
+ redirect: {
67
+ to: string;
68
+ type: number;
69
+ };
70
+ }
71
+ interface Post {
72
+ id: number;
73
+ slug: string;
74
+ locale: Locale;
75
+ title: string;
76
+ excerpt: string | null;
77
+ body: string | null;
78
+ status: string;
79
+ published_at: string | null;
80
+ meta: {
81
+ title: string | null;
82
+ description: string | null;
83
+ og_image: MediaAsset | null;
84
+ };
85
+ featured_image: MediaAsset | null;
86
+ author?: {
87
+ name: string | null;
88
+ slug: string | null;
89
+ };
90
+ category?: {
91
+ name: string | null;
92
+ slug: string | null;
93
+ };
94
+ }
95
+ interface MenuItem {
96
+ id: number;
97
+ title: string;
98
+ type: string | null;
99
+ url: string | null;
100
+ /** Target locale for internal links; null for external/manual links. */
101
+ locale: Locale | null;
102
+ /** Locale-relative logical path for internal links; null otherwise. */
103
+ path: string | null;
104
+ open_in_new_tab: boolean;
105
+ is_megamenu: boolean;
106
+ megamenu_section: string | null;
107
+ cta_style: string | null;
108
+ children: MenuItem[];
109
+ }
110
+ interface Menu {
111
+ slug: string;
112
+ name: string;
113
+ locale: Locale;
114
+ items: MenuItem[];
115
+ }
116
+ interface WebformElement {
117
+ key: string | null;
118
+ type: string | null;
119
+ label: string | null;
120
+ help_text: string | null;
121
+ placeholder: string | null;
122
+ required: boolean;
123
+ max_length: number | null;
124
+ options: {
125
+ key: string;
126
+ label: string;
127
+ }[] | null;
128
+ content: string | null;
129
+ semantic_role: string | null;
130
+ elements?: WebformElement[];
131
+ }
132
+ interface Webform {
133
+ slug: string;
134
+ title: string;
135
+ locale: Locale;
136
+ elements: WebformElement[];
137
+ confirmation: string | null;
138
+ }
139
+ interface PaginatedResponse<T> {
140
+ data: T[];
141
+ links: Record<string, string | null>;
142
+ meta: {
143
+ current_page: number;
144
+ last_page: number;
145
+ per_page: number;
146
+ total: number;
147
+ [key: string]: unknown;
148
+ };
149
+ }
150
+ type ResolvedRedirect = {
151
+ type: 'redirect';
152
+ to: string;
153
+ code: number;
154
+ };
155
+ type ResolvedPage = {
156
+ type: 'page';
157
+ data: Page;
158
+ };
159
+ type ResolvedPost = {
160
+ type: 'post';
161
+ data: Post;
162
+ };
163
+ type ResolvedNotFound = {
164
+ type: 'not_found';
165
+ };
166
+ type Resolved = ResolvedRedirect | ResolvedPage | ResolvedPost | ResolvedNotFound;
167
+
168
+ export type { Block, Locale, MediaAsset, Menu, MenuItem, Page, PageListItem, PageMeta, PaginatedResponse, Post, RedirectEnvelope, Resolved, ResolvedLink, ResolvedNotFound, ResolvedPage, ResolvedPost, ResolvedRedirect, Webform, WebformElement };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@arpsw/astro-cms",
3
+ "version": "0.3.1",
4
+ "description": "Astro integration for the ARP (Laravel) CMS — shared API client, i18n/path resolution, and config wiring for ARP CMS sites.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "author": "ARP.software",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/arpsw/astro-cms.git"
11
+ },
12
+ "keywords": [
13
+ "astro",
14
+ "astro-integration",
15
+ "arp",
16
+ "cms"
17
+ ],
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org",
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src/components",
25
+ "README.md"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./runtime": {
33
+ "types": "./dist/runtime.d.ts",
34
+ "import": "./dist/runtime.js"
35
+ },
36
+ "./types": {
37
+ "types": "./dist/types.d.ts",
38
+ "import": "./dist/types.js"
39
+ },
40
+ "./CmsBlock.astro": "./src/components/CmsBlock.astro",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "check": "tsc --noEmit",
47
+ "format": "prettier --write .",
48
+ "format:check": "prettier --check .",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "peerDependencies": {
52
+ "astro": "^6.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.19.19",
56
+ "astro": "^6.3.7",
57
+ "prettier": "^3.4.2",
58
+ "prettier-plugin-astro": "^0.14.1",
59
+ "tsup": "^8.3.5",
60
+ "typescript": "^5.9.2"
61
+ }
62
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ /**
3
+ * Optional generic block dispatcher. Looks up a component for `block.type` in
4
+ * the `components` map and renders it, passing the whole `block` (so the
5
+ * component reads `block.data`) plus any extra props through. Unknown types
6
+ * render nothing.
7
+ *
8
+ * import CmsBlock from '@arpsw/astro-cms/CmsBlock.astro';
9
+ * import Hero from '../components/blocks/Hero.astro';
10
+ * const components = { hero: Hero };
11
+ * {page.blocks.map((block) => <CmsBlock {block} {components} {locale} />)}
12
+ *
13
+ * Best for sites where blocks share a uniform prop signature. Sites that want
14
+ * per-block typed props (or extra per-type props) should hand-write a renderer
15
+ * with a `block.type` switch instead.
16
+ */
17
+ import type { Block } from '../types';
18
+
19
+ interface Props {
20
+ block: Block;
21
+ components: Record<string, unknown>;
22
+ }
23
+
24
+ const { block, components, ...rest } = Astro.props as Props & Record<string, unknown>;
25
+ const Component = components[block.type] as
26
+ | ((props: Record<string, unknown>) => unknown)
27
+ | undefined;
28
+ ---
29
+
30
+ {Component && <Component block={block} {...rest} />}