@lightnet/sveltia-admin 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # @lightnet/sveltia-admin
2
+
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - The `imagesFolder` option was removed from `@lightnet/sveltia-admin`, and image fields now always use the content-adjacent `images` directory.
8
+
9
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - The experimental Decap-based admin integration was replaced by `@lightnet/sveltia-admin`, which no longer accepts a `languages` option.
10
+
11
+ ### Minor Changes
12
+
13
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - Add an experimental `useLanguagesCollection` option to `sveltiaAdmin(...)` so sites can edit LightNet languages through a root-level `languages.json` file.
14
+ To use it, move the `languages` array out of `lightnet(...)` into `languages.json`, import that file back into `astro.config.*`, and enable `experimental.useLanguagesCollection`.
15
+
16
+ ### Patch Changes
17
+
18
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - `commonId` is now optional for media items in both `lightnet` schema validation and `@lightnet/sveltia-admin`.
19
+
20
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - Export `pathWithBase` from `lightnet/utils` and use that public entrypoint inside `@lightnet/sveltia-admin` so published installs resolve the media edit button controller correctly.
21
+
22
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - Normalize `sveltiaAdmin({ path })` values before composing admin routes and config URLs so documented paths like `/admin` resolve correctly.
23
+
24
+ - [#361](https://github.com/LightNetDev/LightNet/pull/361) [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce) Thanks [@smn-cds](https://github.com/smn-cds)! - Respect Astro `base` paths for Sveltia Admin URLs so the CMS config request and media edit-button links work correctly when a site is deployed under a subpath such as `/docs`.
25
+
26
+ - Updated dependencies [[`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce), [`65b3eec`](https://github.com/LightNetDev/LightNet/commit/65b3eec5b68565237b46c6423d21257ad4747dce)]:
27
+ - lightnet@4.0.0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @lightnet/sveltia-admin
2
+
3
+ **Experimental** Administration UI for LightNet sites. This is built as an [Astro Integration](https://docs.astro.build/en/guides/integrations-guide/) that uses [Sveltia CMS](https://sveltiacms.app/) to administrate LightNet´s content folders.
4
+
5
+ ## Documentation
6
+
7
+ [Read the LightNet Administration UI docs](https://docs.lightnet.community/content/administration-ui/) on how to use this.
8
+
9
+ ## License
10
+
11
+ MIT
12
+
13
+ Copyright (c) 2024–present [LightNet contributors](https://github.com/LightNetDev/LightNet/graphs/contributors)
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@lightnet/sveltia-admin",
3
+ "description": "Admin UI for LightNet based on Sveltia CMS.",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "version": "4.0.0",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/LightNetDev/lightnet",
13
+ "directory": "packages/sveltia-admin"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/LightNetDev/lightnet/issues"
17
+ },
18
+ "homepage": "https://lightnet.community",
19
+ "keywords": [
20
+ "astro-integration"
21
+ ],
22
+ "files": [
23
+ "src",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "scripts": {
28
+ "test": "vitest",
29
+ "e2e": "playwright test"
30
+ },
31
+ "exports": {
32
+ ".": "./src/astro-integration/integration.ts",
33
+ "./Admin.astro": "./src/Admin.astro",
34
+ "./config.json.ts": "./src/config.json.ts"
35
+ },
36
+ "peerDependencies": {
37
+ "astro": "^6.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@sveltia/cms": "0.151.4",
41
+ "lightnet": "^4.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@internal/e2e-test-utils": "workspace:^0.0.1",
45
+ "@playwright/test": "^1.58.2",
46
+ "astro": "^6.1.1",
47
+ "typescript": "^5.9.3",
48
+ "vitest": "^4.1.2"
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
52
+ }
53
+ }
@@ -0,0 +1,25 @@
1
+ ---
2
+ import config from "virtual:lightnet/sveltiaAdminConfig"
3
+
4
+ import { pathWithBase } from "./utils/paths"
5
+ ---
6
+
7
+ <!doctype html>
8
+ <html>
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="robots" content="noindex" />
12
+ <title>LightNet Administration</title>
13
+ <link
14
+ href={pathWithBase(`/${config.path}/config.json`)}
15
+ type="application/json"
16
+ rel="cms-config-url"
17
+ />
18
+ </head>
19
+ <body>
20
+ <script>
21
+ import { init } from "@sveltia/cms"
22
+ init({})
23
+ </script>
24
+ </body>
25
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="250" height="250" fill="none"><path fill="url(#a)" d="M127.177 237.945c-31.458-8.449-57.358-27.9-58.64-61.948-1.282-34.047 25.656-62.629 65.654-59.025v-11.337c.016-5.824 7.193-7.94 11.592-4.231l42.11 34.295c3.114 2.632 3.107 7.457-.028 10.232l-42.082 34.907c-4.421 3.931-11.607.984-11.592-4.841v-11.569c-4.214-.655-7.424-.837-9.694-.655-36.43 2.914-33.36 64.459 29.562 61.263 27.034-1.375 71.032-31.406 72.833-78.024 0-70.268-87.295-93.122-65.334-141.292 2.21-4.848-6.687-7.035-12.286-4.905-22.507 6.2-47.336 19.82-61.462 60.259-1.808 5.175-4.081 17.397-6.023 22.485C79.99 88.665 77.4 100.1 70.23 99.493c-9.396-.794-6.122-16.14-4.069-22.75 1.73-5.568-5.982-10.273-13.048-2.995-11.996 12.356-23.989 28.879-28.621 54.665-1.82 10.118-1.92 24.43.146 36.433 8.965 52.294 54.493 85.67 98.792 85.148 10.956-.13 11.254-10.033 3.747-12.049"/><defs><linearGradient id="a" x1="125" x2="125" y1="-3.432" y2="252.061" gradientUnits="userSpaceOnUse"><stop stop-color="#d64fc0"/><stop offset=".466" stop-color="#ff1878"/><stop offset=".611" stop-color="#ff1878"/><stop offset=".981" stop-color="#ff6c0b"/></linearGradient></defs></svg>
@@ -0,0 +1,109 @@
1
+ import { z } from "astro/zod"
2
+
3
+ const normalizeAdminPath = (path: string) => path.replace(/^\/+|\/+$/g, "")
4
+
5
+ /**
6
+ * @see https://sveltiacms.app/en/docs/backends/gitlab
7
+ */
8
+ const gitlabSchema = z
9
+ .object({
10
+ name: z.literal("gitlab"),
11
+ repo: z.string(),
12
+ appId: z.string().optional(),
13
+ branch: z.string().default("main"),
14
+ authType: z.literal("pkce").default("pkce"),
15
+ })
16
+ .transform((gitlabConfig) => ({
17
+ ...gitlabConfig,
18
+ app_id: gitlabConfig.appId,
19
+ auth_type: gitlabConfig.authType,
20
+ }))
21
+
22
+ /**
23
+ * @see https://sveltiacms.app/en/docs/backends/github
24
+ */
25
+ const githubSchema = z
26
+ .object({
27
+ name: z.literal("github"),
28
+ repo: z.string(),
29
+ baseUrl: z.string().optional(),
30
+ branch: z.string().default("main"),
31
+ })
32
+ .transform((githubConfig) => ({
33
+ ...githubConfig,
34
+ base_url: githubConfig.baseUrl,
35
+ }))
36
+
37
+ // Internal testing backend used by the Playwright harness.
38
+ const testRepoSchema = z.object({
39
+ name: z.literal("test-repo"),
40
+ })
41
+
42
+ export const adminConfigSchema = z.object({
43
+ /**
44
+ * Path for the admin page.
45
+ *
46
+ * Default is /admin
47
+ */
48
+ path: z.string().default("admin").transform(normalizeAdminPath),
49
+ /**
50
+ * Maximum upload file size in megabytes.
51
+ *
52
+ * Default is 25 (this aligns with Cloudflare's max file size).
53
+ */
54
+ maxFileSize: z.number().default(25),
55
+ /**
56
+ * Connected Git Host.
57
+ */
58
+ backend: gitlabSchema.or(githubSchema).or(testRepoSchema).optional(),
59
+
60
+ /**
61
+ * Path from the repository root to the LightNet site root.
62
+ * Set this when the site lives in a subdirectory (for example, in a monorepo).
63
+ * Leave empty when the site is at the repository root.
64
+ */
65
+ siteRootInRepo: z.string().default(""),
66
+
67
+ /**
68
+ * Experimental config options are opt-in and might change with any release.
69
+ */
70
+ experimental: z
71
+ .object({
72
+ /**
73
+ * Enable editing LightNet languages through the admin UI.
74
+ *
75
+ * This expects a `languages.json` file at the LightNet site root
76
+ * (for example `/languages.json` inside your Astro site directory,
77
+ * not inside this package). That file should contain the full
78
+ * LightNet `languages` array, including each language's `code`,
79
+ * translated `label`, and any site-language flags like
80
+ * `isDefaultSiteLanguage` or `isSiteLanguage`.
81
+ *
82
+ * Your site's `astro.config.*` should import that file and pass it to
83
+ * `lightnet({ languages })`.
84
+ *
85
+ * @example
86
+ * import languages from "./languages.json" assert { type: "json" }
87
+ *
88
+ * export default defineConfig({
89
+ * integrations: [
90
+ * lightnet({ languages }),
91
+ * lightnetSveltiaAdmin({
92
+ * experimental: {
93
+ * useLanguagesCollection: true,
94
+ * },
95
+ * }),
96
+ * ],
97
+ * })
98
+ *
99
+ * This option is experimental and may change without a stable
100
+ * migration path.
101
+ */
102
+ useLanguagesCollection: z.boolean().default(false),
103
+ })
104
+ .optional(),
105
+ })
106
+
107
+ export type SveltiaAdminConfig = z.input<typeof adminConfigSchema>
108
+
109
+ export type ExtendedSveltiaAdminConfig = z.output<typeof adminConfigSchema>
@@ -0,0 +1,90 @@
1
+ import { fileURLToPath } from "node:url"
2
+
3
+ import type { AstroIntegration, ViteUserConfig } from "astro"
4
+ import { AstroError } from "astro/errors"
5
+
6
+ import {
7
+ adminConfigSchema,
8
+ type ExtendedSveltiaAdminConfig,
9
+ type SveltiaAdminConfig,
10
+ } from "./config"
11
+ import { verifySchema } from "./verify-schema"
12
+
13
+ export default function lightnetSveltiaAdmin(
14
+ config: SveltiaAdminConfig,
15
+ ): AstroIntegration {
16
+ return {
17
+ name: "@lightnet/sveltia-admin",
18
+ hooks: {
19
+ "astro:config:setup": ({ injectRoute, updateConfig }) => {
20
+ if (Object.hasOwn(config, "imagesFolder")) {
21
+ throw new AstroError(
22
+ "Invalid LightNet Administration UI configuration",
23
+ "Fix these errors on the sveltiaAdmin configuration inside astro.config.mjs:\n\n- imagesFolder: `imagesFolder` was removed. Remove this option from `sveltiaAdmin(...)`. Image paths now always resolve from the content-adjacent `images` folder.",
24
+ )
25
+ }
26
+
27
+ const preparedConfig = verifySchema(
28
+ adminConfigSchema,
29
+ config,
30
+ "Invalid LightNet Administration UI configuration",
31
+ "Fix these errors on the sveltiaAdmin configuration inside astro.config.mjs:",
32
+ )
33
+
34
+ injectRoute({
35
+ pattern: preparedConfig.path,
36
+ entrypoint: "@lightnet/sveltia-admin/Admin.astro",
37
+ prerender: true,
38
+ })
39
+ injectRoute({
40
+ pattern: `${preparedConfig.path}/config.json`,
41
+ entrypoint: "@lightnet/sveltia-admin/config.json.ts",
42
+ prerender: true,
43
+ })
44
+
45
+ updateConfig({
46
+ vite: {
47
+ plugins: [vitePluginSveltiaAdminConfig(preparedConfig)],
48
+ },
49
+ })
50
+ },
51
+ "astro:server:start": ({ address }) => {
52
+ process.env.LIGHTNET_DEV_SITE_URL = `http://localhost:${address.port}`
53
+ },
54
+ },
55
+ }
56
+ }
57
+
58
+ const CONFIG = "virtual:lightnet/sveltiaAdminConfig"
59
+ const MEDIA_ITEM_EDIT_BUTTON_CONTROLLER =
60
+ "virtual:lightnet/components/media-item-edit-button-controller"
61
+ const MEDIA_ITEM_EDIT_BUTTON_CONTROLLER_PATH = JSON.stringify(
62
+ fileURLToPath(
63
+ new URL("./media-item-edit-button-controller.ts", import.meta.url),
64
+ ),
65
+ )
66
+ const VIRTUAL_MODULES = [CONFIG, MEDIA_ITEM_EDIT_BUTTON_CONTROLLER] as const
67
+
68
+ function vitePluginSveltiaAdminConfig(
69
+ userConfig: ExtendedSveltiaAdminConfig,
70
+ ): NonNullable<ViteUserConfig["plugins"]>[number] {
71
+ return {
72
+ name: "vite-plugin-lightnet-sveltia-admin-config",
73
+ enforce: "pre",
74
+ resolveId(id): string | undefined {
75
+ const module = VIRTUAL_MODULES.find((m) => m === id)
76
+ if (module) return `\0${module}`
77
+ },
78
+ load(id): string | undefined {
79
+ const module = VIRTUAL_MODULES.find((m) => id === `\0${m}`)
80
+ switch (module) {
81
+ case CONFIG:
82
+ return `export default ${JSON.stringify(userConfig)};`
83
+ case MEDIA_ITEM_EDIT_BUTTON_CONTROLLER:
84
+ return userConfig.path === "admin"
85
+ ? `export { default } from ${MEDIA_ITEM_EDIT_BUTTON_CONTROLLER_PATH};`
86
+ : "export default undefined;"
87
+ }
88
+ },
89
+ }
90
+ }
@@ -0,0 +1,27 @@
1
+ import { pathWithBase } from "../utils/paths"
2
+
3
+ const parseCachedUser = () => {
4
+ try {
5
+ const cachedUser = localStorage.getItem("sveltia-cms.user")
6
+ return cachedUser ? JSON.parse(cachedUser) : undefined
7
+ } catch {
8
+ return undefined
9
+ }
10
+ }
11
+
12
+ export default {
13
+ shouldShow: () => {
14
+ if (import.meta.env.DEV) {
15
+ return true
16
+ }
17
+
18
+ const cachedUser = parseCachedUser()
19
+ return (
20
+ typeof cachedUser === "object" &&
21
+ cachedUser !== null &&
22
+ typeof cachedUser.backendName === "string"
23
+ )
24
+ },
25
+ createHref: (mediaId: string) =>
26
+ `${pathWithBase("/admin")}#/collections/media/entries/${encodeURIComponent(mediaId)}`,
27
+ }
@@ -0,0 +1,32 @@
1
+ import { AstroError } from "astro/errors"
2
+ import { z } from "astro/zod"
3
+
4
+ export function verifySchema<T extends z.Schema>(
5
+ schema: T,
6
+ toVerify: unknown,
7
+ errorMessage: string | ((id: string | undefined) => string),
8
+ hint: string | ((id: string | undefined) => string),
9
+ ): z.output<T> {
10
+ const parsed = schema.safeParse(toVerify, {})
11
+ if (parsed.success) {
12
+ return parsed.data
13
+ }
14
+
15
+ throwParseError(toVerify, errorMessage, hint, parsed)
16
+ }
17
+
18
+ function throwParseError(
19
+ toVerify: unknown,
20
+ errorMessage: string | ((id: string | undefined) => string),
21
+ hint: string | ((id: string | undefined) => string),
22
+ parsed: z.ZodSafeParseError<unknown>,
23
+ ): never {
24
+ const id = z.object({ id: z.string() }).safeParse(toVerify).data?.id
25
+ const message =
26
+ typeof errorMessage === "string" ? errorMessage : errorMessage(id)
27
+ const hintFinal = typeof hint === "string" ? hint : hint(id)
28
+ const issues = parsed.error.issues
29
+ .map((issue) => `- ${issue.path.join(".")}: ${issue.message}`)
30
+ .join("\n")
31
+ throw new AstroError(message, `${hintFinal}\n\n${issues}`)
32
+ }
@@ -0,0 +1,4 @@
1
+ declare module "virtual:lightnet/sveltiaAdminConfig" {
2
+ const config: import("./config").ExtendedSveltiaAdminConfig
3
+ export default config
4
+ }
@@ -0,0 +1,7 @@
1
+ import type { APIRoute } from "astro"
2
+
3
+ import { getConfig } from "./sveltia/sveltia.config"
4
+
5
+ export const GET: APIRoute = () => {
6
+ return new Response(JSON.stringify(getConfig()))
7
+ }
@@ -0,0 +1,31 @@
1
+ import type { Collection } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { inlineTranslation } from "../../utils/inline-translation"
5
+ import { projectPath } from "../../utils/path"
6
+
7
+ export const categoriesCollection: Collection = {
8
+ name: "categories",
9
+ label: "Categories",
10
+ description:
11
+ "Organize and filter media items by topic. Examples: discipleship, youth, prayer. [Read documentation](https://docs.lightnet.community/content/categories/)",
12
+ label_singular: "Category",
13
+ folder: projectPath("src/content/categories"),
14
+ create: true,
15
+ format: "json",
16
+ slug: "{{fields._slug}}",
17
+ summary: `{{label.${config.defaultLocale}}} ({{slug}})`,
18
+ fields: [
19
+ inlineTranslation({ name: "label", label: "Name" }),
20
+ {
21
+ name: "image",
22
+ label: "Image",
23
+ required: false,
24
+ choose_url: false,
25
+ widget: "image",
26
+ media_folder: "./images",
27
+ accept: "image/png, image/jpeg, image/webp",
28
+ hint: "When you upload an image, it is automatically resized (up to 2048 pixels) and saved in a web-friendly format.",
29
+ },
30
+ ],
31
+ }
@@ -0,0 +1,12 @@
1
+ import { categoriesCollection } from "./categories"
2
+ import { mediaCollectionCollection } from "./media-collections"
3
+ import { mediaItemCollection } from "./media-items"
4
+ import { mediaTypeCollection } from "./media-types"
5
+
6
+ export const contentCollections = [
7
+ mediaItemCollection,
8
+ { divider: true },
9
+ categoriesCollection,
10
+ mediaCollectionCollection,
11
+ mediaTypeCollection,
12
+ ]
@@ -0,0 +1,73 @@
1
+ import type { CollectionFile } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+ import sveltiaAdminConfig from "virtual:lightnet/sveltiaAdminConfig"
4
+
5
+ import { inlineTranslation } from "../../utils/inline-translation"
6
+ import { projectPath } from "../../utils/path"
7
+
8
+ export const languagesSelect = () => {
9
+ if (sveltiaAdminConfig.experimental?.useLanguagesCollection) {
10
+ return {
11
+ name: "language",
12
+ label: "Language",
13
+ widget: "relation",
14
+ collection: "_singletons",
15
+ file: "languages",
16
+ value_field: "{{languages.*.code}}",
17
+ display_fields: [
18
+ `{{languages.*.label.${config.defaultLocale}}} ({{languages.*.code}})`,
19
+ ],
20
+ }
21
+ } else {
22
+ return {
23
+ name: "language",
24
+ label: "Language",
25
+ widget: "select",
26
+ options: config.languages.map(({ code, label }) => {
27
+ return {
28
+ label: `${label[config.defaultLocale]} (${code})`,
29
+ value: code,
30
+ }
31
+ }),
32
+ }
33
+ }
34
+ }
35
+
36
+ export const defineLanguagesCollection = () => {
37
+ if (!sveltiaAdminConfig.experimental?.useLanguagesCollection) {
38
+ return
39
+ }
40
+ return languagesCollection
41
+ }
42
+
43
+ const languagesCollection: CollectionFile = {
44
+ name: "languages",
45
+ label: "Languages",
46
+ file: projectPath("languages.json"),
47
+ format: "json",
48
+ icon: "language",
49
+ fields: [
50
+ {
51
+ widget: "list",
52
+ name: "languages",
53
+ label: "Languages",
54
+ label_singular: "Language",
55
+ allow_duplicate: false,
56
+ root: true,
57
+ collapsed: "auto",
58
+ summary: `{{label.${config.defaultLocale}}} ({{code}})`,
59
+ fields: [
60
+ {
61
+ name: "code",
62
+ label: "Language Code",
63
+ hint: "Enter a valid IETF BCP 47 language tag (for example, en, en-US, ar). Use this [tool](https://r12a.github.io/app-subtags/) to find the right code.",
64
+ pattern: [
65
+ "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4}|[A-Za-z]{5,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\\d{3}))?(?:-(?:[A-Za-z0-9]{5,8}|\\d[A-Za-z0-9]{3}))*(?:-[0-9A-WY-Za-wy-z](?:-[A-Za-z0-9]{2,8})+)*(?:-x(?:-[A-Za-z0-9]{1,8})+)?|x(?:-[A-Za-z0-9]{1,8})+)$",
66
+ "Enter a valid BCP 47 language tag (e.g. en, en-US, zh-Hans-CN).",
67
+ ],
68
+ },
69
+ inlineTranslation({ name: "label", label: "Name" }),
70
+ ],
71
+ },
72
+ ],
73
+ }
@@ -0,0 +1,43 @@
1
+ import type { Collection } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { inlineTranslation } from "../../utils/inline-translation"
5
+ import { projectPath } from "../../utils/path"
6
+
7
+ export const mediaCollectionCollection: Collection = {
8
+ name: "media-collections",
9
+ label: "Media Collections",
10
+ description:
11
+ "Group related media items in a specific order. Examples: a course, a series, a study path. [Read documentation](https://docs.lightnet.community/content/media-collections/)",
12
+ label_singular: "Media Collection",
13
+ create: true,
14
+ folder: projectPath("src/content/media-collections"),
15
+ format: "json",
16
+ slug: "{{fields._slug}}",
17
+ summary: `{{label.${config.defaultLocale}}} ({{slug}})`,
18
+ fields: [
19
+ inlineTranslation({
20
+ name: "label",
21
+ label: "Name",
22
+ }),
23
+ {
24
+ name: "mediaItems",
25
+ label: "Media Items",
26
+ label_singular: "Media Item",
27
+ widget: "list",
28
+ hint: "The list order defines the item order in this collection.",
29
+ summary: "{{fields.mediaItem}}",
30
+ collapsed: true,
31
+ field: {
32
+ name: "mediaItem",
33
+ label: "Media Item",
34
+ widget: "relation",
35
+ collection: "media",
36
+ value_field: "{{slug}}",
37
+ display_fields: ["{{title}} ({{slug}})"],
38
+ search_fields: ["{{title}}", "{{slug}}"],
39
+ dropdown_threshold: 1,
40
+ },
41
+ },
42
+ ],
43
+ }
@@ -0,0 +1,164 @@
1
+ import type { Collection } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+ import sveltiaAdminConfig from "virtual:lightnet/sveltiaAdminConfig"
4
+
5
+ import { inlineTranslation } from "../../utils/inline-translation"
6
+ import { projectPath } from "../../utils/path"
7
+ import { languagesSelect } from "./languages"
8
+
9
+ export const mediaItemCollection: Collection = {
10
+ name: "media",
11
+ label: "Media Items",
12
+ description:
13
+ "Add content entries to the media library. Examples: a book PDF, a YouTube link. [Read documentation](https://docs.lightnet.community/content/media-items/)",
14
+ label_singular: "Media Item",
15
+ folder: projectPath("src/content/media"),
16
+ create: true,
17
+ preview_path: `${config.defaultLocale}/media/{{filename}}`,
18
+ format: "json",
19
+ slug: "{{fields._slug}}",
20
+ sortable_fields: ["slug", "dateCreated", "language"],
21
+ summary: "{{title}} ({{slug}})",
22
+ view_groups: [
23
+ { label: "Language", field: "language", pattern: ".*" },
24
+ { label: "Type", field: "type", pattern: ".*" },
25
+ ],
26
+ fields: [
27
+ { name: "title", label: "Title", widget: "string" },
28
+ {
29
+ name: "type",
30
+ label: "Type",
31
+ widget: "relation",
32
+ collection: "media-types",
33
+ value_field: "{{slug}}",
34
+ display_fields: [`{{label.${config.defaultLocale}}} ({{slug}})`],
35
+ },
36
+ languagesSelect(),
37
+ {
38
+ name: "image",
39
+ label: "Image",
40
+ widget: "image",
41
+ choose_url: false,
42
+ media_folder: "./images",
43
+ accept: "image/png, image/jpeg, image/webp",
44
+ hint: "When you upload an image, it is automatically resized (up to 2048 pixels) and saved in a web-friendly format.",
45
+ },
46
+ {
47
+ name: "content",
48
+ label: "Content",
49
+ widget: "list",
50
+ hint: "Add files or weblinks. First item in the list is the main content.",
51
+ min: 1,
52
+ summary: "{{types.url}}",
53
+ types: [
54
+ {
55
+ name: "upload",
56
+ label: "File Upload",
57
+ fields: [
58
+ {
59
+ name: "url",
60
+ label: "File",
61
+ widget: "file",
62
+ choose_url: false,
63
+ media_folder: projectPath("public/files"),
64
+ public_folder: "/files",
65
+ hint: `Maximum file size is ${sveltiaAdminConfig.maxFileSize} MB.`,
66
+ media_library: {
67
+ config: {
68
+ max_file_size: sveltiaAdminConfig.maxFileSize * 1000000,
69
+ },
70
+ },
71
+ },
72
+ inlineTranslation({
73
+ name: "label",
74
+ label: "Label",
75
+ hint: "Optional. Defaults to the file name, for example 'bible' from 'bible.pdf'.",
76
+ required: false,
77
+ collapsed: "auto",
78
+ }),
79
+ ],
80
+ },
81
+ {
82
+ name: "link",
83
+ label: "Link",
84
+ summary: "{{url}}",
85
+ fields: [
86
+ {
87
+ name: "url",
88
+ label: "Url",
89
+ widget: "string",
90
+ type: "url",
91
+ pattern: ["^https?://", "Link must start with http(s)://"],
92
+ },
93
+ inlineTranslation({
94
+ name: "label",
95
+ label: "Label",
96
+ required: false,
97
+ hint: "Optional. Defaults to the file name or link domain, for example 'youtube.com'.",
98
+ collapsed: "auto",
99
+ }),
100
+ ],
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ name: "dateCreated",
106
+ label: "Date Created",
107
+ widget: "datetime",
108
+ time_format: false,
109
+ required: true,
110
+ default: "{{now}}",
111
+ picker_utc: true,
112
+ hint: "The date this item was added to this media library.",
113
+ },
114
+ {
115
+ name: "authors",
116
+ label: "Authors",
117
+ label_singular: "Author",
118
+ required: false,
119
+ collapsed: true,
120
+ default: [],
121
+ widget: "list",
122
+ summary: "{{fields.name}}",
123
+ field: { label: "Name", name: "name", widget: "string" },
124
+ },
125
+ {
126
+ name: "commonId",
127
+ label: "Common ID",
128
+ widget: "string",
129
+ required: false,
130
+ hint: "Optional: Use a shared Common ID to link translated versions of a media item.",
131
+ },
132
+ {
133
+ name: "categories",
134
+ label: "Categories",
135
+ required: false,
136
+ widget: "relation",
137
+ multiple: true,
138
+ collection: "categories",
139
+ display_fields: ["{{slug}}"],
140
+ search_fields: ["{{slug}}"],
141
+ },
142
+ {
143
+ name: "description",
144
+ label: "Description",
145
+ widget: "markdown",
146
+ required: false,
147
+ editor_components: [],
148
+ buttons: [
149
+ "heading-one",
150
+ "heading-two",
151
+ "heading-three",
152
+ "heading-four",
153
+ "heading-five",
154
+ "heading-six",
155
+ "bold",
156
+ "italic",
157
+ "bulleted-list",
158
+ "numbered-list",
159
+ "quote",
160
+ "link",
161
+ ],
162
+ },
163
+ ],
164
+ }
@@ -0,0 +1,79 @@
1
+ import type { Collection } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { inlineTranslation } from "../../utils/inline-translation"
5
+ import { projectPath } from "../../utils/path"
6
+
7
+ export const mediaTypeCollection: Collection = {
8
+ name: "media-types",
9
+ label: "Media Types",
10
+ description:
11
+ "Define different content formats. Examples: books, videos, audio. [Read documentation](https://docs.lightnet.community/content/media-types/)",
12
+ label_singular: "Media Type",
13
+ folder: projectPath("src/content/media-types"),
14
+ format: "json",
15
+ slug: "{{fields._slug}}",
16
+ summary: `{{label.${config.defaultLocale}}} ({{slug}})`,
17
+ fields: [
18
+ inlineTranslation({ name: "label", label: "Name" }),
19
+ {
20
+ name: "icon",
21
+ label: "Icon",
22
+ pattern: [
23
+ "(?:mdi|lucide)--.+",
24
+ "Icon name must start with mdi-- or lucide--",
25
+ ],
26
+ widget: "string",
27
+ hint: "Browse Lucide icons at https://lucide.dev/icons/ and enter the icon name with the 'lucide--' prefix, for example 'lucide--book-open'.",
28
+ },
29
+ {
30
+ name: "coverImageStyle",
31
+ label: "Cover Image Style",
32
+ widget: "select",
33
+ default: "default",
34
+ options: ["default", "book", "video"],
35
+ },
36
+ {
37
+ name: "detailsPage",
38
+ required: false,
39
+ typeKey: "layout",
40
+ label: "Details Page Configuration",
41
+ default: {},
42
+ widget: "object",
43
+ types: [
44
+ {
45
+ name: "default",
46
+ label: "Default",
47
+ fields: [
48
+ inlineTranslation({
49
+ name: "openActionLabel",
50
+ label: "Open Action Label",
51
+ required: false,
52
+ collapsed: "auto",
53
+ }),
54
+ ],
55
+ },
56
+ {
57
+ name: "custom",
58
+ label: "Custom",
59
+ fields: [
60
+ {
61
+ name: "customComponent",
62
+ required: true,
63
+ label: "Custom Component",
64
+ widget: "string",
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ name: "video",
70
+ label: "Video",
71
+ },
72
+ {
73
+ name: "audio",
74
+ label: "Audio",
75
+ },
76
+ ],
77
+ },
78
+ ],
79
+ }
@@ -0,0 +1,74 @@
1
+ import type { CmsConfig } from "@sveltia/cms"
2
+ import { site } from "astro:config/server"
3
+ import sveltiaAdminConfig from "virtual:lightnet/sveltiaAdminConfig"
4
+
5
+ import lightnetLogo from "../assets/lightnet-logo.svg?url"
6
+ import { contentCollections } from "./collections/content"
7
+ import { defineLanguagesCollection } from "./collections/content/languages"
8
+ import { projectPath } from "./utils/path"
9
+
10
+ export function getConfig(
11
+ siteUrl = process.env.LIGHTNET_DEV_SITE_URL ?? site,
12
+ ): CmsConfig {
13
+ return {
14
+ backend: sveltiaAdminConfig.backend ?? {
15
+ name: "github",
16
+ repo: createLocalRepoPath(),
17
+ },
18
+ media_folder: projectPath("src/assets"),
19
+ public_folder: "/src/assets",
20
+ app_title: "LightNet Admin",
21
+ logo: {
22
+ src: lightnetLogo,
23
+ },
24
+ media_libraries: {
25
+ stock_assets: {
26
+ providers: [],
27
+ },
28
+ default: {
29
+ config: {
30
+ slugify_filename: true,
31
+ max_file_size: sveltiaAdminConfig.maxFileSize * 1024 * 1024,
32
+ transformations: {
33
+ raster_image: {
34
+ format: "webp",
35
+ quality: 85,
36
+ width: 2048,
37
+ height: 2048,
38
+ },
39
+ svg: {
40
+ optimize: true,
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ editor: { preview: false },
47
+ site_url: siteUrl,
48
+ output: {
49
+ omit_empty_optional_fields: true,
50
+ },
51
+ slug: {
52
+ clean_accents: true,
53
+ maxlength: 60,
54
+ },
55
+ collections: [...contentCollections],
56
+ singletons: [defineLanguagesCollection()].filter((c) => !!c),
57
+ }
58
+ }
59
+
60
+ // Sveltia CMS uses repo as unique site identifier for IndexedDB
61
+ // https://github.com/sveltia/sveltia-cms/issues/630
62
+ // Also it expects repo in format <org>/<repo>
63
+ // We do not want to require setting a path for local only settings so we generate
64
+ // our path from site url.
65
+ function createLocalRepoPath() {
66
+ return (
67
+ (site ?? "")
68
+ .replace(/^https?:\/\//, "")
69
+ .replaceAll("/", "-")
70
+ .replaceAll(".", "-") + "/local-repository"
71
+ )
72
+ }
73
+
74
+ export const config = getConfig()
@@ -0,0 +1,25 @@
1
+ import type { Field, ObjectField } from "@sveltia/cms"
2
+ import config from "virtual:lightnet/config"
3
+
4
+ type Options = Partial<ObjectField> & {
5
+ name: string
6
+ }
7
+
8
+ const locales = [
9
+ config.defaultLocale,
10
+ ...config.locales.filter((l) => l !== config.defaultLocale),
11
+ ].map((locale) => ({
12
+ name: locale,
13
+ label: locale,
14
+ }))
15
+
16
+ export const inlineTranslation = (options: Options): Field => ({
17
+ summary: `{{${config.defaultLocale}}}`,
18
+ ...options,
19
+ widget: "object",
20
+ fields: locales.map((locale) => ({
21
+ ...locale,
22
+ widget: "string",
23
+ required: locale.name === config.defaultLocale,
24
+ })),
25
+ })
@@ -0,0 +1,6 @@
1
+ import sveltiaAdminConfig from "virtual:lightnet/sveltiaAdminConfig"
2
+
3
+ const basePath: string = sveltiaAdminConfig.siteRootInRepo
4
+
5
+ export const projectPath = (path: string) =>
6
+ `${basePath}${basePath.endsWith("/") || path.startsWith("/") ? "" : "/"}${path}`
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Prefix a site-internal path with Astro's configured base path.
3
+ *
4
+ * This helper trims any trailing slash from `BASE_URL`, ensures the input
5
+ * path starts with a leading slash, and concatenates the two values.
6
+ * Absolute URLs are out of scope for this helper.
7
+ *
8
+ * @param path internal path such as "/en/media", "/api/internal/search.json", or "/"
9
+ * @returns base-aware internal path
10
+ */
11
+ export function pathWithBase(path: string) {
12
+ const normalizedBase = import.meta.env.BASE_URL.replace(/\/+$/, "")
13
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`
14
+ return `${normalizedBase}${normalizedPath}`
15
+ }