includio-cms 0.37.1 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/API.md +54 -4
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +19 -0
  5. package/dist/admin/remote/booking.remote.js +1 -1
  6. package/dist/booking/payment-amount.d.ts +23 -0
  7. package/dist/booking/payment-amount.js +37 -0
  8. package/dist/booking/server/http/portal-handler.js +2 -1
  9. package/dist/booking/server/payments.d.ts +14 -4
  10. package/dist/booking/server/payments.js +11 -5
  11. package/dist/booking/server/portal.d.ts +5 -2
  12. package/dist/booking/server/portal.js +10 -4
  13. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  14. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  15. package/dist/core/cms.d.ts +4 -1
  16. package/dist/core/cms.js +9 -2
  17. package/dist/core/fields/jsonLd/builders.d.ts +74 -0
  18. package/dist/core/fields/jsonLd/builders.js +88 -0
  19. package/dist/core/fields/jsonLd/graph.d.ts +15 -0
  20. package/dist/core/fields/jsonLd/graph.js +43 -0
  21. package/dist/core/fields/jsonLd/index.d.ts +2 -0
  22. package/dist/core/fields/jsonLd/index.js +2 -0
  23. package/dist/core/fields/resolveSeo.d.ts +1 -1
  24. package/dist/core/fields/resolveSeo.js +1 -1
  25. package/dist/core/fields/slugPath.d.ts +1 -1
  26. package/dist/core/fields/slugPath.js +3 -2
  27. package/dist/core/fields/slugifyFilename.d.ts +7 -0
  28. package/dist/core/fields/slugifyFilename.js +14 -0
  29. package/dist/core/i18n/localeUrl.d.ts +20 -0
  30. package/dist/core/i18n/localeUrl.js +36 -0
  31. package/dist/core/index.d.ts +2 -0
  32. package/dist/core/index.js +3 -0
  33. package/dist/core/server/entries/operations/get.js +1 -1
  34. package/dist/core/server/entries/operations/resolveEntry.js +1 -1
  35. package/dist/core/server/fields/resolveRelationFields.js +1 -1
  36. package/dist/core/server/fields/resolveRichtextLinks.js +1 -1
  37. package/dist/core/server/fields/resolveUrlFields.js +1 -1
  38. package/dist/core/server/fields/slugResolver.d.ts +2 -2
  39. package/dist/core/server/fields/slugResolver.js +12 -4
  40. package/dist/core/server/fields/utils/fixOrphans.d.ts +15 -3
  41. package/dist/core/server/fields/utils/fixOrphans.js +30 -7
  42. package/dist/core/server/generator/generator.js +60 -1
  43. package/dist/files-local/index.js +5 -5
  44. package/dist/paraglide/messages/_index.d.ts +3 -36
  45. package/dist/paraglide/messages/_index.js +3 -71
  46. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  47. package/dist/paraglide/messages/hello_world.js +33 -0
  48. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  49. package/dist/paraglide/messages/login_hello.js +34 -0
  50. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  51. package/dist/paraglide/messages/login_please_login.js +34 -0
  52. package/dist/sveltekit/components/json-ld.svelte +12 -0
  53. package/dist/sveltekit/components/json-ld.svelte.d.ts +6 -0
  54. package/dist/sveltekit/components/seo.svelte +62 -11
  55. package/dist/sveltekit/components/seo.svelte.d.ts +10 -0
  56. package/dist/sveltekit/index.d.ts +2 -0
  57. package/dist/sveltekit/index.js +2 -0
  58. package/dist/sveltekit/server/handle.js +5 -5
  59. package/dist/sveltekit/server/index.d.ts +1 -0
  60. package/dist/sveltekit/server/index.js +2 -0
  61. package/dist/sveltekit/server/sitemap.d.ts +19 -0
  62. package/dist/sveltekit/server/sitemap.js +43 -0
  63. package/dist/types/cms.d.ts +3 -2
  64. package/dist/types/cms.schema.d.ts +10 -0
  65. package/dist/types/cms.schema.js +7 -1
  66. package/dist/types/languages.d.ts +13 -0
  67. package/dist/types/languages.js +2 -1
  68. package/dist/updates/0.37.2/index.d.ts +2 -0
  69. package/dist/updates/0.37.2/index.js +20 -0
  70. package/dist/updates/index.js +3 -1
  71. package/package.json +1 -1
  72. package/dist/paraglide/messages/en.d.ts +0 -5
  73. package/dist/paraglide/messages/en.js +0 -14
  74. package/dist/paraglide/messages/pl.d.ts +0 -5
  75. package/dist/paraglide/messages/pl.js +0 -14
package/API.md CHANGED
@@ -1,8 +1,8 @@
1
- # includio-cms — Public API v0.37.1
1
+ # includio-cms — Public API v0.37.3
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
5
- Entry points: **23** · Stable: **665** · Experimental: **4**
5
+ Entry points: **23** · Stable: **715** · Experimental: **4**
6
6
 
7
7
  Tags:
8
8
  - `@public` — frozen for v1.0; semver-protected.
@@ -13,17 +13,59 @@ Tags:
13
13
 
14
14
  ### `includio-cms`
15
15
 
16
+ - `article(input: ArticleInput): JsonLdNode`
17
+ - `interface ArticleInput`
18
+ - `blogPosting(input: ArticleInput): JsonLdNode`
19
+ - `breadcrumbList(items: Array<{ name: string; url: string }>): JsonLdNode`
16
20
  - `interface EmailAdapter` — Contract for email adapters. Used by `better-auth` reset-password flow and form submission
21
+ - `faqPage(items: Array<{ question: string; answer: string }>): JsonLdNode`
22
+ - `interface GeoInput`
23
+ - `imageNode(img: string | MediaFile): string | JsonLdNode` — Normalize an image to a URL string or an ImageObject with dimensions.
24
+ - `jsonLdGraph(nodes: JsonLdNode | JsonLdNode[]): { '@context': 'https://schema.org'; '@graph': JsonLdNode[]; }` — Wrap node(s) into a schema.org
25
+ - `type JsonLdNode = Record<string, unknown> & { '@type': string | string[] }`
26
+ - `localBusiness(input: LocalBusinessInput): JsonLdNode`
27
+ - `interface LocalBusinessInput`
28
+ - `organization(input: OrganizationInput): JsonLdNode`
29
+ - `interface OrganizationInput`
30
+ - `interface PostalAddressInput`
31
+ - `pruneEmpty(value: T): T` — Recursively drop undefined/null/'' and empty arrays/objects.
17
32
  - `type ResolvedSeo = { [K in keyof SeoFieldData]: K extends 'ogImage' ? string | MediaFile | undefined : string | unde...` — Normalized SEO data — localized values flattened by `language`. A stable
18
33
  - `resolveSeo(entry: { seo?: Record<string, unknown> } | Record<string,..., language?: string): ResolvedSeo` — Resolve `entry.seo` into flat fields, flattening localized objects by
19
34
  - `interface SendMailOptions`
35
+ - `slugifyFilename(base: string): string` — Slugify a media file base name (extension handled by the caller).
36
+ - `thing(type: string | string[], props: Record<string, unknown> = {}): JsonLdNode` — Build a schema.org node of `type` from `props`, pruning empty values.
37
+ - `webPage(input: WebPageInput): JsonLdNode`
38
+ - `interface WebPageInput`
39
+ - `website(input: WebSiteInput): JsonLdNode`
40
+ - `interface WebSiteInput`
20
41
 
21
42
  ### `includio-cms/core`
22
43
 
44
+ - `article(input: ArticleInput): JsonLdNode`
45
+ - `interface ArticleInput`
46
+ - `blogPosting(input: ArticleInput): JsonLdNode`
47
+ - `breadcrumbList(items: Array<{ name: string; url: string }>): JsonLdNode`
23
48
  - `interface EmailAdapter` — Contract for email adapters. Used by `better-auth` reset-password flow and form submission
49
+ - `faqPage(items: Array<{ question: string; answer: string }>): JsonLdNode`
50
+ - `interface GeoInput`
51
+ - `imageNode(img: string | MediaFile): string | JsonLdNode` — Normalize an image to a URL string or an ImageObject with dimensions.
52
+ - `jsonLdGraph(nodes: JsonLdNode | JsonLdNode[]): { '@context': 'https://schema.org'; '@graph': JsonLdNode[]; }` — Wrap node(s) into a schema.org
53
+ - `type JsonLdNode = Record<string, unknown> & { '@type': string | string[] }`
54
+ - `localBusiness(input: LocalBusinessInput): JsonLdNode`
55
+ - `interface LocalBusinessInput`
56
+ - `organization(input: OrganizationInput): JsonLdNode`
57
+ - `interface OrganizationInput`
58
+ - `interface PostalAddressInput`
59
+ - `pruneEmpty(value: T): T` — Recursively drop undefined/null/'' and empty arrays/objects.
24
60
  - `type ResolvedSeo = { [K in keyof SeoFieldData]: K extends 'ogImage' ? string | MediaFile | undefined : string | unde...` — Normalized SEO data — localized values flattened by `language`. A stable
25
61
  - `resolveSeo(entry: { seo?: Record<string, unknown> } | Record<string,..., language?: string): ResolvedSeo` — Resolve `entry.seo` into flat fields, flattening localized objects by
26
62
  - `interface SendMailOptions`
63
+ - `slugifyFilename(base: string): string` — Slugify a media file base name (extension handled by the caller).
64
+ - `thing(type: string | string[], props: Record<string, unknown> = {}): JsonLdNode` — Build a schema.org node of `type` from `props`, pruning empty values.
65
+ - `webPage(input: WebPageInput): JsonLdNode`
66
+ - `interface WebPageInput`
67
+ - `website(input: WebSiteInput): JsonLdNode`
68
+ - `interface WebSiteInput`
27
69
 
28
70
  ### `includio-cms/core/server`
29
71
 
@@ -332,8 +374,10 @@ Tags:
332
374
  - `const Image: LegacyComponentType`
333
375
  - `isImageFieldData(media: MediaFieldData): media is ImageFieldData`
334
376
  - `isVideoFieldData(media: MediaFieldData): media is VideoFieldData`
377
+ - `const JsonLd: LegacyComponentType`
335
378
  - `const Media: LegacyComponentType`
336
379
  - `const Preview: LegacyComponentType`
380
+ - `const Seo: LegacyComponentType`
337
381
  - `setCustomFields(defs: Map<string, CustomFieldDefinition>): <inferred>`
338
382
  - `setPreferMp4(value: boolean): <inferred>`
339
383
  - `setRemotes(data: typeof remotes): <inferred>`
@@ -343,6 +387,8 @@ Tags:
343
387
 
344
388
  ### `includio-cms/sveltekit/server`
345
389
 
390
+ - `buildRobots(options: RobotsOptions = {}): string` — Build a robots.txt document. Defaults to allow-all + optional sitemap link.
391
+ - `buildSitemap(entries: SitemapEntry[]): string` — Build a sitemap.xml document from a list of URL entries.
346
392
  - `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
347
393
  - `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without
348
394
  - `type CountEntriesOptions = Omit< ResolveEntriesOptions, 'limit' | 'offset' | 'populate' | 'orderBy' | 'dataOrderBy' >`
@@ -365,7 +411,10 @@ Tags:
365
411
  - `resolveEntry(opts: ResolveEntryOptions): Promise<Entry | null>` — Fetch a single populated Entry.
366
412
  - `interface ResolveEntryOptions`
367
413
  - `type ResolveStatus = 'published' | 'draft' | 'scheduled'` — Status filter for `resolveEntry`/`resolveEntries`/`countEntries`.
414
+ - `interface RobotsOptions`
415
+ - `interface RobotsRule`
368
416
  - `sendMail(options: SendMailOptions): Promise<void>` — Send an email through the configured CMS email adapter. Thin convenience
417
+ - `interface SitemapEntry`
369
418
 
370
419
  ### `includio-cms/db-postgres`
371
420
 
@@ -691,18 +740,19 @@ Tags:
691
740
  - `createBooking(input: CreateBookingInput, opts: { now?: Date; actor?: PriceActor } = {}): Promise<{ booking: typeof bookingsTable.$inferSelect; accessToken: string }>` — Create a booking (public self-serve or staff). Validates catalog capacity +
692
741
  - `createBookingHandler(): { POST: RequestHandler }` — Public self-serve booking creation. Mount at `POST /api/booking`.
693
742
  - `createDepositPayment(bookingId: string): Promise<{ redirectUrl: string | null; status: 'redirect' | 'manual' | 'error'...` — Create the deposit payment for a freshly-created booking: ask the first
694
- - `createInstallmentPayment(bookingId: string, amount: number): Promise<{ paymentLink: string | null }>` — Generate an installment/balance payment link for an arbitrary amount (staff or
743
+ - `createInstallmentPayment(bookingId: string, opts: InstallmentPaymentOptions): Promise<{ paymentLink: string | null }>` — Generate an installment/balance payment link. Records a pending ledger row keyed
695
744
  - `createMaintenanceHandler(): { POST: RequestHandler }` — Secured external trigger for booking maintenance — the swap-in for the
696
745
  - `createPortalHandler(): { GET: RequestHandler; POST: RequestHandler }` — Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
697
746
  - `createWebhookHandler(): { POST: RequestHandler }` — Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
698
747
  - `editBooking(bookingId: string, changes: BookingEditChanges, opts: { actor: 'staff' | 'portal'; now?: Date }): Promise<typeof bookingsTable.$inferSelect>` — Edit a booking (staff or portal). Recomputes totals per-participant + derived
699
748
  - `getBookingDb(): <inferred>` — Returns the Drizzle handle, asserting the postgres adapter is in use.
700
749
  - `getBookingMaintenanceStatus(): { running: boolean; lastRun: Date | null }`
750
+ - `interface InstallmentPaymentOptions`
701
751
  - `listBookingAdjustments(bookingId: string): Promise<BookingAdjustment[]>` — List all adjustments (active + voided) for a booking, newest first.
702
752
  - `pickEmailTemplate(kind: BookingEmailKind): string` — Template file name for a kind (1:1 — kept as a function for future indirection).
703
753
  - `portalAddTrips(token: string, additions: Array<{ tripRef: string; assignments: string[] }>): <inferred>` — Self-service dobieranie wycieczki: doczytaj obecny stan, scal z `additions`
704
754
  - `portalEdit(token: string, changes: BookingEditChanges): <inferred>` — Portal self-service edit (actor:'portal' guards). Throws → handler maps to 409/400.
705
- - `portalPay(token: string, amount: number): <inferred>` — Portal: create an installment/balance payment link.
755
+ - `portalPay(token: string): <inferred>` — Portal: create a balance payment link. The amount is NOT taken from the request —
706
756
  - `portalUpdateParticipant(token: string, assignmentId: string, data: Record<string, unknown>): <inferred>` — Portal self-service: customer fills their own participant data. Verifies the
707
757
  - `portalView(token: string): <inferred>` — Portal view: booking + items + participants + ledger (no staff data).
708
758
  - `priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig): Promise<ParticipantPriceResult>` — Price a booking from its CURRENTLY persisted people/items/assignments (no
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.37.2 — 2026-07-16
7
+
8
+ Powierzchnia SEO/AEO rdzenia — reużywalne helpery, które raz naprawione w rdzeniu działają u każdego klienta (wyszły z wdrożenia SEO u pod-zwrotnikami). W tym samym patchu domyka się też łatka bezpieczeństwa bookingu (kwota płatności).
9
+
10
+ ### Added
11
+ - **Buildery JSON-LD (schema.org) w `includio-cms/core`.** Typowane, czyste funkcje: `organization`, `localBusiness`, `website`, `webPage`, `breadcrumbList`, `article`, `blogPosting`, `faqPage` + generyk `thing` i `jsonLdGraph`. Opt-in — nic nie emitują domyślnie. `undefined`/puste wartości wypadają z outputu, więc częściowe dane dają poprawny minimalny JSON-LD. Bespoke typy (np. `TouristTrip + Offer`, `Product`) składasz po stronie projektu przez `thing("TouristTrip", { ... })` — rdzeń zostaje generyczny.
12
+ - **Komponent `<JsonLd>` w `includio-cms/sveltekit`.** Renderuje `<script type="application/ld+json">` w `<svelte:head>` (SSR), escapuje `<` → breakout ze `<script>` jest niemożliwy. Przyjmuje obiekt albo tablicę (np. wynik `jsonLdGraph`).
13
+ - **`buildSitemap` / `buildRobots` w `includio-cms/sveltekit/server`.** Czyste buildery: `buildSitemap(entries)` → string `sitemap.xml` (URL-e XML-escaped, `lastmod` `Date`→ISO lub string, opcjonalne `changefreq`/`priority`), `buildRobots(options)` → `robots.txt` (domyślnie allow-all + link do sitemapy, konfigurowalne reguły). Projekt posiada route i wpina helper: `export const GET = async () => new Response(buildSitemap(entries), { headers: { "content-type": "application/xml" } });`.
14
+
15
+ ### Fixed
16
+ - **Slugifikacja nazw plików mediów.** Upload i zmiana nazwy przepuszczają basename przez `slugifyFilename` (nowy helper w `includio-cms/core`): `"IMG-1234 (1).jpeg"` → `img-1234-1.jpeg`, polskie znaki transliterowane, spacje/wielkość/nawiasy usunięte. Tylko nowe pliki — istniejące URL-e w bazie nietknięte (bez migracji). Cap 200 znaków, path-traversal niemożliwy (slug nie zawiera `/` ani `.`).
17
+ - **`<Seo>` dopełniony i wyeksportowany z `includio-cms/sveltekit`.** Wcześniej komponent istniał, ale nie był eksportowany (martwy). Bugfix: `og:url` i `og:type` używają `property=` zamiast `name=` (wymóg Open Graph). Nowe propsy: `ogType` (domyślnie `website`; `article` dokłada `article:published_time`/`modified_time`/`author`/`section`/`tag`), `noindex` (→ `<meta name="robots" content="noindex">`), `siteName` (→ `og:site_name`). `ogImage` typu `MediaFile` emituje `og:image:width`/`height`/`alt`.
18
+ - **Typografia — łamanie sierot przyimkowych.** `fixOrphans` wstawia NBSP nie tylko po jednoliterowych spójnikach (`i/w/z/o/u/a/e`), ale też po krótkich przyimkach (`do na od po we za ze ku nad pod bez dla przy`). Lista konfigurowalna parametrem `words` + eksport `DEFAULT_ORPHAN_WORDS`. Domyślnie konserwatywnie (bez spójników typu `oraz/lub`), żeby nie NBSP-ować za dużo.
19
+ - **Bezpieczeństwo (booking): kwota dopłaty przestaje pochodzić z żądania HTTP.** Domknięcie łuku 0.37.1 (cena szła z katalogu, ale kwota wpłaty — nie): `portalPay` podawał `amount` z ciała żądania prosto do `createInstallmentPayment`, bez konfrontacji z saldem — przeglądarka dyktowała kwotę (nadpłata do ręcznego zwrotu albo link na grosz). Bramka kwotowa oparta na aktorze (bliźniak bramki cenowej): `portal` NIE dyktuje (serwer obciąża `totals.balance`, fail-closed gdy brak salda), `staff` może. Portalowa wpłata zapisuje się teraz z `createdBy` aktora — nie zahardkodowanym `staff` (koniec kłamstwa w audycie).
20
+ - **Booking: `ledger.balance` przestaje gubić rabaty.** `portalView` liczył `ledger` z `totals.gross` → `balance = gross − paid`, pomijając `discount`/`surcharge`/`manualDiscount`; portal pokazywał dwa różne salda i przycisk płatności brał gorzej policzone. Teraz `toPortalView` karmi `ledgerSummary` wartością netto (`netOf(totals)`), więc `ledger.balance === totals.balance`.
21
+
22
+ ### Breaking
23
+ - **`createInstallmentPayment(bookingId, amount)` → `createInstallmentPayment(bookingId, { actor, amount? })`.** `actor: "portal" | "staff"` jest wymagany. Dla `portal` kwota jest ignorowana (serwer obciąża saldo); dla `staff` podajesz `amount`. `portalPay(token, amount)` → `portalPay(token)` (kwota nie jest już parametrem). Dotyczy tylko konsumentów wołających te funkcje bezpośrednio — ścieżka przez handler portalu/remote admina zaktualizowana w rdzeniu.
24
+
6
25
  ## 0.37.1 — 2026-07-14
7
26
 
8
27
  Dwie łatki cenowe w bookingu. (1) Cena rezerwacji przestaje pochodzić z żądania HTTP — do 0.37.0 publiczne endpointy przyjmowały `unitPrice` prosto z ciała żądania i nikt nie konfrontował go z katalogiem, więc wyprawę dało się zarezerwować za grosz. (2) Data wyceny zostaje zamrożona na rezerwacji — dotąd każde jej dotknięcie (edycja, ale też zaksięgowanie korekty) przeliczało reguły zależne od czasu wg DZISIEJSZEJ daty, przez co early-bird sprzed miesięcy cicho znikał i klient robił się winien pieniądze.
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.37.1)
1
+ # Includio CMS Documentation (v0.37.3)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/ROADMAP.md CHANGED
@@ -40,6 +40,23 @@
40
40
  - [x] `[fix]` `[breaking]` `[P0]` **Data wyceny zamrożona na rezerwacji** (`bookings.priced_at`). Dotąd cena bazowa była zamrożona, ale data — nie: `editBooking` i `priceFromExistingRows` (ścieżka **korekt**) przeliczały reguły zależne od czasu wg `new Date()`. Obsługa księgowała dopłatę ustaloną telefonicznie tydzień przed wylotem → `daysUntil(anchor, now)` spadało poniżej progu → **early-bird sprzed miesięcy cicho znikał, a klient robił się winien pieniądze**. Bomba tykała sama, bez żadnej zmiany cennika. Rezerwacje sprzed 0.37.1: `priced_at = NULL` → fallback na `created_at` (zero migracji danych). Pełny snapshot **wartości** polityki (`pricing_snapshot`) zostaje w 0.38.0 §4.4 — wymaga świadomej zmiany cennika przez klientkę, więc da się ją uprzedzić.
41
41
  - [x] `[fix]` `[breaking]` `[P0]` **Cena rezerwacji przestaje pochodzić z żądania HTTP.** Do 0.37.0 trzy publiczne drogi wpuszczały `unitPrice` z ciała żądania i nikt nie konfrontował go z katalogiem (`create-handler.ts:37`, portalowe `add-trips`, portalowe `edit` → `editBooking`) — wyprawę dało się zarezerwować za grosz. Bramka stoi teraz w rdzeniu (`createBooking`/`editBooking`), oparta na aktorze: `public`/`portal` nie dyktują ceny (nowa pozycja → szew `resolvePrice` z katalogu, istniejąca → jej `unitPriceSnapshot`), `staff` nadal może korygować ręcznie. Domyślny aktor to `public`, więc przyszły publiczny endpoint jest bezpieczny bez pamiętania o niczym. Fail-closed: brak `resolvePrice` = odmowa wyceny.
42
42
 
43
+ ## 0.37.2 — SEO/AEO surface + kwota płatności
44
+
45
+ > Domknięcie tego samego łuku co 0.37.1: **cena** przestała pochodzić z żądania HTTP, ale **kwota płatności** wciąż pochodzi.
46
+ > Zgłoszone z pod-zwrotnikami (2026-07-14) — klient z rabatem pakietowym płacił w portalu za dużo. Konsument załatał się u siebie
47
+ > (`totals.balance` zamiast `ledger.balance`), ale przyczyna siedzi w rdzeniu i każdy kolejny konsument wdepnie w to samo.
48
+
49
+ - [x] `[fix]` `[P0]` **Kwota dopłaty przestaje pochodzić z żądania HTTP.** `portalPay(token, amount)` podaje `amount` prosto do `createInstallmentPayment(bookingId, amount)` (`payments.ts:149`), a ta wkłada go do `totalGross` adaptera i insertuje `booking_payments` — **bez żadnej konfrontacji z `totals.balance`**. Przeglądarka dyktuje, ile zapłaci: może za dużo (nadpłata do zwrotu ręcznego), może grosz (link płatności na 0,01 zł). Bramka z 0.37.1 stanęła na `unitPrice`, ale nie na kwocie wpłaty. Fix: kwotę wyznacza/weryfikuje serwer względem `totals.balance` (aktor `portal` nie dyktuje kwoty, `staff` może — symetrycznie do bramki cenowej), fail-closed przy rozjeździe. Przy okazji: portalowa wpłata zapisuje się z `createdBy: 'staff'` (`payments.ts:187`), co kłamie w audycie. <!-- files: src/lib/booking/server/portal.ts, src/lib/booking/server/payments.ts -->
50
+ - [x] `[fix]` `[P0]` **`ledger.balance` gubi rabaty — i kusi nazwą.** `portalView` woła `ledgerSummary(booking.totals.gross, payments)` (`server/portal.ts:48`), a `ledgerSummary` liczy `balance = gross - paid` (`ledger.ts:87`), z pominięciem `discount` / `surcharge` / `manualDiscount`. Autorytatywne jest `totals.balance` (`netOf(base) - paid`, `totals.ts:73`) i portalView zwraca je obok — więc widok podaje konsumentowi **dwa różne salda, a to gorzej policzone nosi bardziej oczywistą nazwę**. Efekt u konsumenta: portal pokazywał „Pozostało 532 zł" i „Do dopłaty 610 zł" na jednym ekranie, a przycisk płatności brał tę drugą. Fix: `ledgerSummary` liczy saldo z netto (albo `portalView` przestaje zwracać `ledger.balance` i zostawia sam `overpaid`). Test regresyjny: rezerwacja z rabatem → `ledger.balance === totals.balance`. <!-- files: src/lib/booking/ledger.ts, src/lib/booking/server/portal.ts, src/lib/booking/totals.ts -->
51
+
52
+ > **SEO/AEO surface** (bundlowane w tym samym patchu, 2026-07-16). Reużywalne w rdzeniu — raz naprawione, działa u każdego klienta. Spec: `docs/superpowers/specs/2026-07-15-seo-aeo-core-surface-design.md`, plan: `docs/superpowers/plans/2026-07-16-seo-aeo-core-surface.md`.
53
+
54
+ - [x] `[feature]` `[P1]` **Buildery JSON-LD + `<JsonLd>`** — typowane, czyste buildery schema.org w `includio-cms/core` (`organization`/`localBusiness`/`website`/`webPage`/`breadcrumbList`/`article`/`blogPosting`/`faqPage` + generyk `thing`/`jsonLdGraph`) i komponent `<JsonLd>` (SSR w `<svelte:head>`, escape `<`). Opt-in. <!-- files: src/lib/core/fields/jsonLd/, src/lib/sveltekit/components/json-ld.svelte -->
55
+ - [x] `[fix]` `[P1]` **Slugifikacja nazw plików mediów** — `slugifyFilename` w core; `files-local` slugifikuje basename przy uploadzie/rename (tylko nowe pliki, non-breaking). <!-- files: src/lib/core/fields/slugifyFilename.ts, src/lib/files-local/index.ts -->
56
+ - [x] `[fix]` `[P1]` **`<Seo>` dopełniony + wyeksportowany** — export z `sveltekit`; bugfix `og:url`/`og:type` `name→property`; `ogType`/`noindex`/`siteName`/`article` props; og:image width/height/alt z `MediaFile`. <!-- files: src/lib/sveltekit/components/seo.svelte, src/lib/sveltekit/index.ts -->
57
+ - [x] `[fix]` `[P2]` **fixOrphans: przyimki wieloliterowe** — domyślny zestaw rozszerzony o 2–3-literowe przyimki, konfigurowalny `words` param + `DEFAULT_ORPHAN_WORDS`. <!-- files: src/lib/core/server/fields/utils/fixOrphans.ts -->
58
+ - [x] `[feature]` `[P1]` **`buildSitemap`/`buildRobots`** — czyste buildery w `sveltekit/server`; projekt posiada route. <!-- files: src/lib/sveltekit/server/sitemap.ts -->
59
+
43
60
  ## 0.38.0 — Moduły: kontrakt vs polityka
44
61
 
45
62
  > **WSTRZYMANE (2026-07-14).** Łuk jest dojrzały, ale klientka nie zobaczy z niego nic, a produktyzacja
@@ -86,6 +103,7 @@ Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`.
86
103
 
87
104
  ## Backlog
88
105
 
106
+ - [ ] `[feature]` `[P2]` **Panel SEO w sidebarze admina (Yoast-like)** — osobny brainstorm/spec. Guided, niestechniczny panel (odwrotność przytłaczającego Yoasta, on-brand): globalna konfiguracja SEO, per-wpisowy box (snippet preview, noindex toggle, focus keyword → wtedy świadome rozszerzenie pola `seo`), pobranie sitemapy (nakładka na `buildSitemap`). **Przekierowania (redirects)** prawdopodobnie własny spec (tabela DB + hook 301/302 + CRUD). Szwy pod to = SEO surface 0.37.2.
89
107
  - [ ] `[fix]` `[P1]` Wyciek puli połączeń DB w dev (HMR) — pojedynczy proces `vite dev` konsumenta trzymał **100 połączeń** do Postgresa (cały `max_connections`), po czym baza odbijała wszystko: `FATAL: sorry, too many clients already` (zaobserwowane 2026-07-13 na pod-zwrotnikami). Hipoteza: każde przeładowanie modułu przez HMR tworzy nowy klient `postgres.js` (pula domyślnie 10), a stary nie jest zamykany — 10 przeładowań wyczerpuje limit. Sprawdzić zamykanie klienta w `import.meta.hot.dispose` / singleton na `globalThis` w adapterze. Dotyka każdego, kto dłużej pracuje w dev. <!-- files: src/lib/db-postgres/index.ts -->
90
108
  - [ ] `[chore]` `[P1]` CMS doctor — `validateLayout()` jest napisany i przetestowany, ale nie jest wołany z żadnego miejsca produkcyjnego: literówka w nazwie pola w tabie przechodzi po cichu (pole ląduje w orphanach). Podpiąć go do `defineConfig` obok `validatePreviews`, dołożyć `pnpm cms check` w CLI i zestaw reguł zdrowia configu (martwe pola, `pathTemplate` bez `previewUrl`/`previews`, itp.). <!-- files: src/lib/core/fields/layoutUtils.ts, src/lib/sveltekit/config.ts, src/lib/core/config/, src/lib/cli/index.ts -->
91
109
  - [ ] `[feature]` `[P2]` Date/datetime field — przebudowa na shadcn-svelte (bits-ui Calendar/DatePicker) zamiast natywnego inputu. Zgłoszone w QA Etap 5a (4/5); funkcjonalnie OK, odłożone jako osobny redesign pola daty. <!-- files: src/lib/admin/components/fields/date-field.svelte, datetime-field.svelte -->
@@ -99,6 +117,7 @@ Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`.
99
117
  - [ ] `[feature]` `[P1]` Ban/unban UI — reason + expiry, `banUser`/`unbanUser` <!-- files: src/lib/admin/client/users/ban-user-dialog.svelte -->
100
118
  - [ ] `[feature]` `[P1]` Impersonation UI — impersonate/stop bar <!-- files: src/lib/admin/client/users/impersonation-bar.svelte -->
101
119
  - [ ] `[chore]` `[P2]` Caching/performance layer (scope TBD)
120
+ - [ ] `[perf]` Opt-out populacji media per-pole (`populate.fields` obejmujące media, nie tylko relacje) — dziś `resolveMediaFields` wołane bez `ctx`, galeria ciągnięta na marne w widokach list. Diagnoza + opcje: `docs/architecture/2026-07-16-media-populate-opt-out.md`
102
121
  - [ ] `[feature]` `[P2]` API/CLI for configuration (setup DX for less technical users)
103
122
  - [ ] `[feature]` `[P0]` Plugin hooks in CRUD ops (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
104
123
  - [ ] `[feature]` `[P0]` Plugin registration API — public surface for external plugins
@@ -209,7 +209,7 @@ export const editBookingStaff = command(z.object({ bookingId: z.string().uuid(),
209
209
  /** Staff: generate an installment/balance payment link for any amount. @public */
210
210
  export const addPaymentLink = command(z.object({ bookingId: z.string().uuid(), amount: z.number().int().positive() }), async ({ bookingId, amount }) => {
211
211
  requireAuth();
212
- const result = await createInstallmentPayment(bookingId, amount);
212
+ const result = await createInstallmentPayment(bookingId, { actor: 'staff', amount });
213
213
  if (result.paymentLink) {
214
214
  await sendBookingEmail(bookingId, 'payment-link', {
215
215
  amount,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Pure amount gate for installment/balance payment links — the payment-side twin
3
+ * of the 0.37.1 price gate. Keeps the *amount* from being dictated by an untrusted
4
+ * HTTP request. All amounts are in **minor units** (grosze).
5
+ */
6
+ export type PaymentActor = 'portal' | 'staff';
7
+ /** Thrown when a requested payment amount is not allowed for the actor. */
8
+ export declare class PaymentAmountError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /**
12
+ * Resolve the amount to charge on an installment/balance link.
13
+ *
14
+ * - `portal` (untrusted): the server dictates the amount = current outstanding
15
+ * `balance`; any `requestedAmount` from the browser is ignored. Fail-closed
16
+ * when nothing is due (`balance <= 0`).
17
+ * - `staff` (trusted): honors `requestedAmount` (positive integer minor units) —
18
+ * staff may bill a custom amount, e.g. a partial installment or a prepayment.
19
+ *
20
+ * @throws {PaymentAmountError} portal with nothing due, or staff with a
21
+ * non-positive / fractional / missing amount.
22
+ */
23
+ export declare function resolveInstallmentAmount(balance: number, actor: PaymentActor, requestedAmount?: number): number;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pure amount gate for installment/balance payment links — the payment-side twin
3
+ * of the 0.37.1 price gate. Keeps the *amount* from being dictated by an untrusted
4
+ * HTTP request. All amounts are in **minor units** (grosze).
5
+ */
6
+ /** Thrown when a requested payment amount is not allowed for the actor. */
7
+ export class PaymentAmountError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'PaymentAmountError';
11
+ }
12
+ }
13
+ /**
14
+ * Resolve the amount to charge on an installment/balance link.
15
+ *
16
+ * - `portal` (untrusted): the server dictates the amount = current outstanding
17
+ * `balance`; any `requestedAmount` from the browser is ignored. Fail-closed
18
+ * when nothing is due (`balance <= 0`).
19
+ * - `staff` (trusted): honors `requestedAmount` (positive integer minor units) —
20
+ * staff may bill a custom amount, e.g. a partial installment or a prepayment.
21
+ *
22
+ * @throws {PaymentAmountError} portal with nothing due, or staff with a
23
+ * non-positive / fractional / missing amount.
24
+ */
25
+ export function resolveInstallmentAmount(balance, actor, requestedAmount) {
26
+ if (actor === 'staff') {
27
+ if (!Number.isInteger(requestedAmount) || requestedAmount <= 0) {
28
+ throw new PaymentAmountError('Kwota wpłaty musi być dodatnią liczbą całkowitą (grosze).');
29
+ }
30
+ return requestedAmount;
31
+ }
32
+ // portal: browser does not dictate the amount — the server charges the balance.
33
+ if (balance <= 0) {
34
+ throw new PaymentAmountError('Brak salda do zapłaty.');
35
+ }
36
+ return balance;
37
+ }
@@ -65,7 +65,8 @@ export function createPortalHandler() {
65
65
  }
66
66
  try {
67
67
  if (action === 'pay') {
68
- const out = await portalPay(token, Number(body.amount) || 0);
68
+ // Kwota NIE pochodzi z żądania — serwer obciąża saldo (bramka kwotowa w rdzeniu).
69
+ const out = await portalPay(token);
69
70
  return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
70
71
  }
71
72
  if (action === 'edit') {
@@ -1,5 +1,6 @@
1
1
  import type { PaymentEvent } from '../../shop/types.js';
2
2
  import type { PaymentKind } from '../ledger.js';
3
+ import { type PaymentActor } from '../payment-amount.js';
3
4
  /**
4
5
  * Create the deposit payment for a freshly-created booking: ask the first
5
6
  * configured adapter for a payment (charging only the deposit amount via an
@@ -22,12 +23,21 @@ export declare function applyWebhookPayment(event: PaymentEvent): Promise<{
22
23
  bookingId: string | null;
23
24
  kind: PaymentKind | null;
24
25
  }>;
26
+ export interface InstallmentPaymentOptions {
27
+ /** Trust level of the caller. `portal` cannot dictate the amount; `staff` can. */
28
+ actor: PaymentActor;
29
+ /** Required for `staff` (positive minor units). Ignored for `portal` (server charges the balance). */
30
+ amount?: number;
31
+ }
25
32
  /**
26
- * Generate an installment/balance payment link for an arbitrary amount (staff or
27
- * portal). Records a pending ledger row keyed by the provider ref; the webhook
28
- * settles it later.
33
+ * Generate an installment/balance payment link. Records a pending ledger row keyed
34
+ * by the provider ref; the webhook settles it later.
35
+ *
36
+ * The charged amount is gated by {@link resolveInstallmentAmount}: for `actor:'portal'`
37
+ * the browser cannot dictate it — the server charges the outstanding `totals.balance`
38
+ * (fail-closed when nothing is due); `actor:'staff'` may bill a custom `amount`.
29
39
  */
30
- export declare function createInstallmentPayment(bookingId: string, amount: number): Promise<{
40
+ export declare function createInstallmentPayment(bookingId: string, opts: InstallmentPaymentOptions): Promise<{
31
41
  paymentLink: string | null;
32
42
  }>;
33
43
  /** Record a manual cash payment (staff). Settles immediately and recomputes status. */
@@ -6,6 +6,7 @@ import { applyPaymentEvent } from '../webhook-logic.js';
6
6
  import { deriveStatus } from '../state-machine.js';
7
7
  import { netOf } from '../totals.js';
8
8
  import { buildPaymentContext } from '../payment-context.js';
9
+ import { resolveInstallmentAmount } from '../payment-amount.js';
9
10
  /**
10
11
  * Create the deposit payment for a freshly-created booking: ask the first
11
12
  * configured adapter for a payment (charging only the deposit amount via an
@@ -127,11 +128,14 @@ export async function applyWebhookPayment(event) {
127
128
  });
128
129
  }
129
130
  /**
130
- * Generate an installment/balance payment link for an arbitrary amount (staff or
131
- * portal). Records a pending ledger row keyed by the provider ref; the webhook
132
- * settles it later.
131
+ * Generate an installment/balance payment link. Records a pending ledger row keyed
132
+ * by the provider ref; the webhook settles it later.
133
+ *
134
+ * The charged amount is gated by {@link resolveInstallmentAmount}: for `actor:'portal'`
135
+ * the browser cannot dictate it — the server charges the outstanding `totals.balance`
136
+ * (fail-closed when nothing is due); `actor:'staff'` may bill a custom `amount`.
133
137
  */
134
- export async function createInstallmentPayment(bookingId, amount) {
138
+ export async function createInstallmentPayment(bookingId, opts) {
135
139
  const config = requireBookingConfig();
136
140
  const db = getBookingDb();
137
141
  const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
@@ -140,6 +144,8 @@ export async function createInstallmentPayment(bookingId, amount) {
140
144
  const adapter = config.payment[0];
141
145
  if (!adapter)
142
146
  throw new Error('No payment adapter configured');
147
+ // Amount gate (payment-side twin of the 0.37.1 price gate).
148
+ const amount = resolveInstallmentAmount(booking.totals.balance, opts.actor, opts.amount);
143
149
  const ctx = {
144
150
  ...buildPaymentContext({
145
151
  accessToken: booking.accessToken,
@@ -165,7 +171,7 @@ export async function createInstallmentPayment(bookingId, amount) {
165
171
  status: 'pending',
166
172
  providerRef: result.providerRef ?? null,
167
173
  paymentLink: result.redirectUrl ?? null,
168
- createdBy: 'staff'
174
+ createdBy: opts.actor
169
175
  });
170
176
  return { paymentLink: result.redirectUrl ?? null };
171
177
  }
@@ -123,8 +123,11 @@ export declare function portalEdit(token: string, changes: BookingEditChanges):
123
123
  }[];
124
124
  ledger: import("../ledger.js").LedgerSummary;
125
125
  } | null>;
126
- /** Portal: create an installment/balance payment link. */
127
- export declare function portalPay(token: string, amount: number): Promise<{
126
+ /**
127
+ * Portal: create a balance payment link. The amount is NOT taken from the request —
128
+ * the server charges the outstanding `totals.balance` (see {@link createInstallmentPayment}).
129
+ */
130
+ export declare function portalPay(token: string): Promise<{
128
131
  paymentLink: string | null;
129
132
  } | null>;
130
133
  /**
@@ -2,6 +2,7 @@ import { and, eq, inArray, isNull } from 'drizzle-orm';
2
2
  import { getBookingDb } from './db.js';
3
3
  import { bookingsTable, bookingItemsTable, bookingPersonsTable, bookingItemPersonsTable, bookingPaymentsTable } from '../../db-postgres/schema/booking/index.js';
4
4
  import { ledgerSummary } from '../ledger.js';
5
+ import { netOf } from '../totals.js';
5
6
  import { editBooking } from './bookings.js';
6
7
  import { createInstallmentPayment } from './payments.js';
7
8
  import { resolveItemSchema, saveParticipantData } from './participants.js';
@@ -29,7 +30,9 @@ export function toPortalView(booking, items, participants, payments, schemaByIte
29
30
  kind: p.kind,
30
31
  paidAt: p.paidAt ?? null
31
32
  })),
32
- ledger: ledgerSummary(booking.totals.gross, payments)
33
+ // Ledger balance must fold in discounts/surcharges — feed NET (like `totals.balance`),
34
+ // not the pre-discount `gross`, so `ledger.balance === totals.balance`.
35
+ ledger: ledgerSummary(netOf(booking.totals), payments)
33
36
  };
34
37
  }
35
38
  /** Resolve a booking by its unguessable portal access token (active only). */
@@ -138,12 +141,15 @@ export async function portalEdit(token, changes) {
138
141
  await editBooking(booking.id, changes, { actor: 'portal' });
139
142
  return portalView(token);
140
143
  }
141
- /** Portal: create an installment/balance payment link. */
142
- export async function portalPay(token, amount) {
144
+ /**
145
+ * Portal: create a balance payment link. The amount is NOT taken from the request —
146
+ * the server charges the outstanding `totals.balance` (see {@link createInstallmentPayment}).
147
+ */
148
+ export async function portalPay(token) {
143
149
  const booking = await resolveByToken(token);
144
150
  if (!booking)
145
151
  return null;
146
- return createInstallmentPayment(booking.id, amount);
152
+ return createInstallmentPayment(booking.id, { actor: 'portal' });
147
153
  }
148
154
  /**
149
155
  * Portal self-service: customer fills their own participant data. Verifies the
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "image" | "url" | "text" | "date" | "search" | "radio" | "hidden" | "email" | "password" | (string & {}) | "reset" | "color" | "button" | "checkbox" | "tel" | "time" | "month" | "submit" | "datetime-local" | "range" | "week";
5
+ type?: "number" | "image" | "url" | "text" | "date" | "search" | "radio" | "email" | "hidden" | "password" | (string & {}) | "reset" | "color" | "button" | "checkbox" | "tel" | "time" | "month" | "submit" | "datetime-local" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -2,7 +2,7 @@ declare const SidebarInput: import("svelte").Component<(Omit<import("svelte/elem
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "image" | "url" | "text" | "date" | "search" | "radio" | "hidden" | "email" | "password" | (string & {}) | "reset" | "color" | "button" | "checkbox" | "tel" | "time" | "month" | "submit" | "datetime-local" | "range" | "week";
5
+ type?: "number" | "image" | "url" | "text" | "date" | "search" | "radio" | "email" | "hidden" | "password" | (string & {}) | "reset" | "color" | "button" | "checkbox" | "tel" | "time" | "month" | "submit" | "datetime-local" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -2,9 +2,10 @@ import type { DatabaseAdapter } from '../types/adapters/db.js';
2
2
  import type { FilesAdapter } from '../types/adapters/files.js';
3
3
  import type { AdminConfig, ApiKeyConfig, AuthConfig, CMSConfig, ICMS, MediaConfig, TypographyConfig } from '../types/cms.js';
4
4
  import type { CollectionConfigWithType } from '../types/collections.js';
5
- import type { Language } from '../types/languages.js';
5
+ import { type Language } from '../types/languages.js';
6
6
  import type { SingleConfigWithType } from '../types/singles.js';
7
7
  import type { CustomFieldDefinition, IconSetPlugin, Plugin } from '../types/plugins.js';
8
+ import { type LocaleUrl } from './i18n/localeUrl.js';
8
9
  import type { FormConfig } from '../types/forms.js';
9
10
  import type { AIAdapter } from '../types/adapters/ai.js';
10
11
  import type { EmailAdapter, SendMailOptions } from '../types/adapters/email.js';
@@ -24,6 +25,8 @@ export declare class CMS implements ICMS {
24
25
  singles: Record<string, SingleConfigWithType>;
25
26
  forms: Record<string, FormConfig>;
26
27
  languages: Language[];
28
+ /** Znormalizowana mapa locale→URL (prefiks/host, default). Jeden punkt prawdy i18n. */
29
+ localeUrl: LocaleUrl;
27
30
  mediaConfig: MediaConfig;
28
31
  typographyConfig: TypographyConfig;
29
32
  sidebarHelp: boolean;
package/dist/core/cms.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { languageCode } from '../types/languages.js';
1
2
  import { isIconSetPlugin } from '../types/plugins.js';
2
3
  import { setSchemaGetCMS } from './fields/fieldSchemaToTs.js';
4
+ import { buildLocaleUrl } from './i18n/localeUrl.js';
3
5
  import { betterAuth } from 'better-auth';
4
6
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
5
7
  import { admin } from 'better-auth/plugins';
@@ -17,6 +19,8 @@ export class CMS {
17
19
  singles;
18
20
  forms;
19
21
  languages;
22
+ /** Znormalizowana mapa locale→URL (prefiks/host, default). Jeden punkt prawdy i18n. */
23
+ localeUrl;
20
24
  mediaConfig;
21
25
  typographyConfig;
22
26
  sidebarHelp;
@@ -67,10 +71,13 @@ export class CMS {
67
71
  ...form
68
72
  };
69
73
  });
70
- this.languages = config.languages || [];
71
- if (this.languages.length === 0) {
74
+ const rawLanguages = config.languages || [];
75
+ if (rawLanguages.length === 0) {
72
76
  throw new Error('CMS config must include at least one language.');
73
77
  }
78
+ // `languages` znormalizowane do kodów (reszta rdzenia zakłada string[]); url/default niesie localeUrl.
79
+ this.languages = rawLanguages.map(languageCode);
80
+ this.localeUrl = buildLocaleUrl(rawLanguages);
74
81
  this.apiKeys = config.apiKeys || [];
75
82
  if (config.plugins) {
76
83
  this.plugins = config.plugins;
@@ -0,0 +1,74 @@
1
+ import type { MediaFile } from '../../../types/media.js';
2
+ import { type JsonLdNode } from './graph.js';
3
+ export interface OrganizationInput {
4
+ name: string;
5
+ url?: string;
6
+ logo?: string | MediaFile;
7
+ sameAs?: string[];
8
+ description?: string;
9
+ }
10
+ export declare function organization(input: OrganizationInput): JsonLdNode;
11
+ export interface PostalAddressInput {
12
+ streetAddress?: string;
13
+ addressLocality?: string;
14
+ postalCode?: string;
15
+ addressRegion?: string;
16
+ addressCountry?: string;
17
+ }
18
+ export interface GeoInput {
19
+ latitude?: number;
20
+ longitude?: number;
21
+ }
22
+ export interface LocalBusinessInput extends OrganizationInput {
23
+ telephone?: string;
24
+ email?: string;
25
+ image?: string | MediaFile;
26
+ address?: PostalAddressInput;
27
+ geo?: GeoInput;
28
+ openingHours?: string | string[];
29
+ priceRange?: string;
30
+ }
31
+ export declare function localBusiness(input: LocalBusinessInput): JsonLdNode;
32
+ export interface WebSiteInput {
33
+ name: string;
34
+ url: string;
35
+ description?: string;
36
+ inLanguage?: string;
37
+ /** e.g. https://site/search?q={search_term_string} */
38
+ searchUrlTemplate?: string;
39
+ }
40
+ export declare function website(input: WebSiteInput): JsonLdNode;
41
+ export interface WebPageInput {
42
+ name: string;
43
+ url: string;
44
+ description?: string;
45
+ inLanguage?: string;
46
+ isPartOf?: string;
47
+ }
48
+ export declare function webPage(input: WebPageInput): JsonLdNode;
49
+ export declare function breadcrumbList(items: Array<{
50
+ name: string;
51
+ url: string;
52
+ }>): JsonLdNode;
53
+ export interface ArticleInput {
54
+ headline: string;
55
+ url?: string;
56
+ image?: string | MediaFile;
57
+ description?: string;
58
+ datePublished?: string;
59
+ dateModified?: string;
60
+ author?: string | {
61
+ name: string;
62
+ url?: string;
63
+ };
64
+ publisher?: OrganizationInput;
65
+ inLanguage?: string;
66
+ section?: string;
67
+ keywords?: string;
68
+ }
69
+ export declare function article(input: ArticleInput): JsonLdNode;
70
+ export declare function blogPosting(input: ArticleInput): JsonLdNode;
71
+ export declare function faqPage(items: Array<{
72
+ question: string;
73
+ answer: string;
74
+ }>): JsonLdNode;