@deepsel/cms-utils 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Deepsel Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @deepsel/cms-utils
2
+
3
+ A collection of light-weight helper functions for building DeepCMS features in JavaScript/TypeScript apps.
4
+
5
+ `@deepsel/cms-utils` is designed to be:
6
+
7
+ - **Framework-agnostic** – works with Node, React, Astro, Next.js, etc.
8
+ - **TypeScript-friendly** – fully typed helpers.
9
+ - **CMS-oriented** – utilities for menus, slugs, URLs, localization, content trees, etc.
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @deepsel/cms-utils
17
+ ```
18
+
19
+ ## Basic Usage
20
+
21
+ Example (TypeScript / ESM):
22
+
23
+ ```typescript
24
+ import { getMenusForCurrentLang } from '@deepsel/cms-utils';
25
+
26
+ const menus = getMenusForCurrentLang();
27
+
28
+ console.log(menus);
29
+ ```
30
+
31
+ ## Local Development
32
+
33
+ This section explains how to develop `@deepsel/cms-utils` and use new, unpublished features in your app locally, using a local file dependency.
34
+
35
+ ### 1. Folder Layout
36
+
37
+ Clone this repo in your workspace:
38
+
39
+ ```
40
+ git clone git@github.com:DeepselSystems/cms-utils.git
41
+ ```
42
+
43
+ Put your package and your app side by side:
44
+
45
+ ```
46
+ workspace/
47
+ ├─ cms-utils/ # this repo (@deepsel/cms-utils)
48
+ └─ my-app/ # your actual app that consumes it
49
+ ```
50
+
51
+ - `cms-utils/` → the npm package repo (this one).
52
+ - `my-app/` → any app (React, Vue, Next.js, Astro, etc.) that will import `@deepsel/cms-utils`.
53
+
54
+ ### 2. Configure your App to Use the local package
55
+
56
+ In `my-app/package.json`, point the dependency to the local folder:
57
+
58
+ ```json
59
+ {
60
+ "dependencies": {
61
+ "@deepsel/cms-utils": "file:../cms-utils"
62
+ }
63
+ }
64
+ ```
65
+
66
+ Then, in `my-app`, run
67
+
68
+ ```bash
69
+ npm install
70
+ ```
71
+
72
+ What this does:
73
+
74
+ - Creates a symlink in `my-app/node_modules/@deepsel/cms-utils` that points to `../cms-utils`.
75
+ - The app will now use the local source rather than a published version from npm.
76
+
77
+ ### 3. Build the Package in Watch Mode
78
+
79
+ In `cms-utils`, run
80
+
81
+ ```bash
82
+ npm install # first time only
83
+ npm run dev # tsc --watch, keeps dist/ in sync with src/
84
+ ```
85
+
86
+ ### 4. Use the Package in the App During Development
87
+
88
+ In your app (`my-app`), import from `@deepsel/cms-utils` as if it were a regular npm package:
89
+
90
+ ```typescript
91
+ // inside my-app
92
+ import { getMenusForCurrentLang } from '@deepsel/cms-utils';
93
+
94
+ const menus = getMenusForCurrentLang();
95
+ ```
96
+
97
+ ### 5. Build the Package for Production
98
+
99
+ Once you’re happy with a set of changes, open a PR to merge them into the main branch.
100
+ Then you can update your app to use the published version:
101
+
102
+ ```json
103
+ {
104
+ "dependencies": {
105
+ "@deepsel/cms-utils": "^1.0.0"
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT – feel free to use in your own projects.
@@ -0,0 +1,4 @@
1
+ export * from './page';
2
+ export * from './language';
3
+ export * from './menus';
4
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './page';
2
+ export * from './language';
3
+ export * from './menus';
4
+ export * from './types';
@@ -0,0 +1 @@
1
+ export * from './isValidLanguageCode';
@@ -0,0 +1 @@
1
+ export * from './isValidLanguageCode';
@@ -0,0 +1,5 @@
1
+ export declare const validLanguageCodes: string[];
2
+ /**
3
+ * Checks if a string is a valid language code
4
+ */
5
+ export declare function isValidLanguageCode(lang: string): boolean;
@@ -0,0 +1,98 @@
1
+ export const validLanguageCodes = [
2
+ 'am_ET',
3
+ 'ar',
4
+ 'ar_SY',
5
+ 'az',
6
+ 'bg',
7
+ 'bn_IN',
8
+ 'bs',
9
+ 'ca_ES',
10
+ 'cs_CZ',
11
+ 'da_DK',
12
+ 'de',
13
+ 'de_CH',
14
+ 'el_GR',
15
+ 'en',
16
+ 'en_AU',
17
+ 'en_CA',
18
+ 'en_GB',
19
+ 'en_IN',
20
+ 'en_NZ',
21
+ 'er',
22
+ 'es',
23
+ 'es_419',
24
+ 'es_AR',
25
+ 'es_BO',
26
+ 'es_CL',
27
+ 'es_CO',
28
+ 'es_CR',
29
+ 'es_DO',
30
+ 'es_EC',
31
+ 'es_GT',
32
+ 'es_MX',
33
+ 'es_PA',
34
+ 'es_PE',
35
+ 'es_PY',
36
+ 'es_UY',
37
+ 'es_VE',
38
+ 'et',
39
+ 'eu_ES',
40
+ 'fa',
41
+ 'fi',
42
+ 'fr',
43
+ 'fr_BE',
44
+ 'fr_CA',
45
+ 'fr_CH',
46
+ 'gl',
47
+ 'gu',
48
+ 'hi',
49
+ 'hr',
50
+ 'hu',
51
+ 'id',
52
+ 'it',
53
+ 'ja',
54
+ 'ka',
55
+ 'kab',
56
+ 'km',
57
+ 'ko_KP',
58
+ 'ko_KR',
59
+ 'lb',
60
+ 'lo',
61
+ 'lt',
62
+ 'lv',
63
+ 'mk',
64
+ 'ml',
65
+ 'mn',
66
+ 'ms',
67
+ 'my',
68
+ 'nb_NO',
69
+ 'nl',
70
+ 'nl_BE',
71
+ 'pl',
72
+ 'pt',
73
+ 'pt_AO',
74
+ 'pt_BR',
75
+ 'ro',
76
+ 'ru',
77
+ 'sk',
78
+ 'sl',
79
+ 'sq',
80
+ 'sr@latin',
81
+ 'sr_RS',
82
+ 'sv',
83
+ 'te',
84
+ 'th',
85
+ 'tl',
86
+ 'tr',
87
+ 'uk',
88
+ 'vi',
89
+ 'zh_CN',
90
+ 'zh_HK',
91
+ 'zh_TW',
92
+ ];
93
+ /**
94
+ * Checks if a string is a valid language code
95
+ */
96
+ export function isValidLanguageCode(lang) {
97
+ return validLanguageCodes.includes(lang);
98
+ }
@@ -0,0 +1,3 @@
1
+ import { ProcessedMenuItem } from './types';
2
+ import type { SiteSettings } from '../types';
3
+ export declare const getCurrentLangMenus: (settings: SiteSettings) => ProcessedMenuItem[] | null;
@@ -0,0 +1,41 @@
1
+ /*
2
+ Get menus based on the current selected language
3
+ */
4
+ export const getCurrentLangMenus = (settings) => {
5
+ if (!settings || !settings.menus || !settings.menus.length)
6
+ return null;
7
+ let currentLang = localStorage.getItem('i18nextLng');
8
+ // If no language is selected, use the default language
9
+ if (!currentLang) {
10
+ currentLang = settings.default_language.iso_code;
11
+ }
12
+ return settings.menus
13
+ .map((menu) => processMenuItem(menu, currentLang))
14
+ .filter((item) => item !== null);
15
+ };
16
+ /*
17
+ Process a menu item and its children recursively:
18
+ - Remove null items
19
+ - Only keep fields that are needed from the translation for the current language
20
+ - Remove other translations
21
+ */
22
+ function processMenuItem(menuItem, currentLang) {
23
+ const translation = menuItem.translations[currentLang];
24
+ if (!translation) {
25
+ return null;
26
+ }
27
+ // Process children recursively and filter out null items
28
+ const children = menuItem.children && menuItem.children.length > 0
29
+ ? menuItem.children
30
+ .map((child) => processMenuItem(child, currentLang))
31
+ .filter((item) => item !== null)
32
+ : [];
33
+ return {
34
+ id: menuItem.id,
35
+ position: menuItem.position,
36
+ title: translation.title,
37
+ url: translation.url,
38
+ open_in_new_tab: translation.open_in_new_tab,
39
+ children,
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ export * from './getCurrentLangMenus';
@@ -0,0 +1 @@
1
+ export * from './getCurrentLangMenus';
@@ -0,0 +1,22 @@
1
+ export interface MenuItem {
2
+ id: number;
3
+ parent_id: number | null;
4
+ position: number;
5
+ translations: Record<string, {
6
+ open_in_new_tab: boolean;
7
+ page_content_id: number;
8
+ title: string;
9
+ url: string | null;
10
+ use_custom_url: boolean;
11
+ use_page_title: boolean;
12
+ }>;
13
+ children: MenuItem[];
14
+ }
15
+ export interface ProcessedMenuItem {
16
+ id: number;
17
+ position: number;
18
+ title: string;
19
+ url: string | null;
20
+ open_in_new_tab: boolean;
21
+ children: ProcessedMenuItem[];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,5 @@
1
+ import type { ApiResponse } from './types';
2
+ /**
3
+ * Fetches blog list data from the backend by language
4
+ */
5
+ export declare function fetchBlogListData(lang: string, authToken?: string | null, astroRequest?: Request | null, backendHost?: string): Promise<ApiResponse>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Fetches blog list data from the backend by language
3
+ */
4
+ export async function fetchBlogListData(lang, authToken = null, astroRequest = null, backendHost = 'http://localhost:8000') {
5
+ try {
6
+ // Determine the URL based on whether a language is provided
7
+ const url = lang && lang !== 'default'
8
+ ? `${backendHost}/blog_post/website/${lang}`
9
+ : `${backendHost}/blog_post/website/default`;
10
+ // Prepare fetch options
11
+ const fetchOptions = {
12
+ method: 'GET',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ };
17
+ // Send the current hostname to the backend for proper domain detection
18
+ let hostname = null;
19
+ // Server-side: Extract hostname from Astro request
20
+ if (astroRequest) {
21
+ const url = new URL(astroRequest.url);
22
+ hostname = url.hostname;
23
+ }
24
+ // Client-side: Extract hostname from window
25
+ else if (typeof window !== 'undefined') {
26
+ hostname = window.location.hostname;
27
+ }
28
+ if (hostname) {
29
+ fetchOptions.headers['X-Original-Host'] = hostname;
30
+ fetchOptions.headers['X-Frontend-Host'] = hostname;
31
+ // Note: Cannot override Host header due to browser security restrictions
32
+ }
33
+ // Add authentication headers if token exists (for both preview and protected content)
34
+ let token = authToken;
35
+ // If no token provided and we're in browser environment, try Capacitor Preferences
36
+ if (!token && typeof window !== 'undefined') {
37
+ try {
38
+ const { Preferences } = await import('@capacitor/preferences');
39
+ const tokenResult = await Preferences.get({ key: 'token' });
40
+ token = tokenResult.value;
41
+ }
42
+ catch (e) {
43
+ console.warn('Could not get token from Preferences:', e);
44
+ }
45
+ }
46
+ if (token) {
47
+ fetchOptions.headers['Authorization'] = `Bearer ${token}`;
48
+ }
49
+ // Fetch the blog list data from the backend
50
+ const response = await fetch(url, fetchOptions);
51
+ // Handle authentication errors
52
+ if (response.status === 401) {
53
+ return { error: true, status: 401, message: 'Authentication required' };
54
+ }
55
+ // Only treat actual 404 as not found
56
+ if (response.status === 404) {
57
+ const { detail } = await response.json();
58
+ console.warn('404', url, { detail });
59
+ return { notFound: true, status: 404, detail };
60
+ }
61
+ try {
62
+ // Parse the JSON
63
+ const posts = await response.json();
64
+ return { posts };
65
+ }
66
+ catch (parseError) {
67
+ console.error(`Failed to parse response: ${parseError.message}`);
68
+ return { error: true, parseError: parseError.message };
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error('Error fetching blog list data:', error);
73
+ return { error: true, message: error.message };
74
+ }
75
+ }
@@ -0,0 +1,5 @@
1
+ import type { ApiResponse } from './types';
2
+ /**
3
+ * Fetches Blog-Post-Content data from the backend by language and slug
4
+ */
5
+ export declare function fetchBlogPostData(lang: string, slug: string, authToken?: string | null, astroRequest?: Request | null, backendHost?: string): Promise<ApiResponse>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Fetches Blog-Post-Content data from the backend by language and slug
3
+ */
4
+ export async function fetchBlogPostData(lang, slug, authToken = null, astroRequest = null, backendHost = 'http://localhost:8000') {
5
+ try {
6
+ // Format the slug properly
7
+ const formattedSlug = slug.replace(/^\/blog\//, '');
8
+ // Determine the URL based on whether a language is provided
9
+ const url = lang && lang !== 'default'
10
+ ? `${backendHost}/blog_post/website/${lang}/${formattedSlug}`
11
+ : `${backendHost}/blog_post/website/default/${formattedSlug}`;
12
+ // Prepare fetch options
13
+ const fetchOptions = {
14
+ method: 'GET',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ };
19
+ // Send the current hostname to the backend for proper domain detection
20
+ let hostname = null;
21
+ // Server-side: Extract hostname from Astro request
22
+ if (astroRequest) {
23
+ const url = new URL(astroRequest.url);
24
+ hostname = url.hostname;
25
+ }
26
+ // Client-side: Extract hostname from window
27
+ else if (typeof window !== 'undefined') {
28
+ hostname = window.location.hostname;
29
+ }
30
+ if (hostname) {
31
+ fetchOptions.headers['X-Original-Host'] = hostname;
32
+ fetchOptions.headers['X-Frontend-Host'] = hostname;
33
+ // Note: Cannot override Host header due to browser security restrictions
34
+ }
35
+ // Add authentication headers if token exists (for both preview and protected content)
36
+ let token = authToken;
37
+ // If no token provided and we're in browser environment, try Capacitor Preferences
38
+ if (!token && typeof window !== 'undefined') {
39
+ try {
40
+ const { Preferences } = await import('@capacitor/preferences');
41
+ const tokenResult = await Preferences.get({ key: 'token' });
42
+ token = tokenResult.value;
43
+ }
44
+ catch (e) {
45
+ console.warn('Could not get token from Preferences:', e);
46
+ }
47
+ }
48
+ if (token) {
49
+ fetchOptions.headers['Authorization'] = `Bearer ${token}`;
50
+ }
51
+ // Fetch the page data from the backend
52
+ const response = await fetch(url, fetchOptions);
53
+ // Handle authentication errors
54
+ if (response.status === 401) {
55
+ return { error: true, status: 401, message: 'Authentication required' };
56
+ }
57
+ // Only treat actual 404 as not found
58
+ if (response.status === 404) {
59
+ const { detail } = await response.json();
60
+ console.warn('404', url, { detail });
61
+ return { notFound: true, status: 404, detail };
62
+ }
63
+ try {
64
+ // Parse the JSON
65
+ return await response.json();
66
+ }
67
+ catch (parseError) {
68
+ console.error(`Failed to parse response: ${parseError.message}`);
69
+ return { error: true, parseError: parseError.message };
70
+ }
71
+ }
72
+ catch (error) {
73
+ console.error('Error fetching page data:', error);
74
+ return { error: true, message: error.message };
75
+ }
76
+ }
@@ -0,0 +1,5 @@
1
+ import type { ApiResponse } from './types';
2
+ /**
3
+ * Fetches Form data from the backend by language and slug
4
+ */
5
+ export declare function fetchFormData(lang: string, slug: string, backendHost?: string): Promise<ApiResponse>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Fetches Form data from the backend by language and slug
3
+ */
4
+ export async function fetchFormData(lang, slug, backendHost = 'http://localhost:8000') {
5
+ try {
6
+ // Format the slug properly
7
+ const formattedSlug = slug.replace(/^\/forms\//, '');
8
+ // Determine the URL based on whether a language is provided
9
+ const url = `${backendHost}/form/website/${lang}/${formattedSlug}`;
10
+ // Prepare fetch options
11
+ const fetchOptions = {
12
+ method: 'GET',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ };
17
+ // Fetch the page data from the backend
18
+ const response = await fetch(url, fetchOptions);
19
+ // Only treat actual 404 as not found
20
+ if (response.status === 404) {
21
+ const { detail } = await response.json();
22
+ console.warn('404', url, { detail });
23
+ return { notFound: true, status: 404, detail };
24
+ }
25
+ try {
26
+ // Parse the JSON
27
+ return await response.json();
28
+ }
29
+ catch (parseError) {
30
+ console.error(`Failed to parse response: ${parseError.message}`);
31
+ return { error: true, parseError: parseError.message };
32
+ }
33
+ }
34
+ catch (error) {
35
+ console.error('Error fetching page data:', error);
36
+ return { error: true, message: error.message };
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ import type { ApiResponse } from './types';
2
+ /**
3
+ * Fetches page data from the backend by language and slug
4
+ */
5
+ export declare function fetchPageData(lang: string | null, slug: string, isPreview?: boolean, authToken?: string | null, astroRequest?: Request | null, backendHost?: string): Promise<ApiResponse>;
@@ -0,0 +1,104 @@
1
+ import { fetchPublicSettings } from './fetchPublicSettings';
2
+ /**
3
+ * Fetches page data from the backend by language and slug
4
+ */
5
+ export async function fetchPageData(lang, slug, isPreview = false, authToken = null, astroRequest = null, backendHost = 'http://localhost:8000') {
6
+ try {
7
+ // Format the slug properly, make sure it starts with a slash
8
+ let formattedSlug = slug.startsWith('/') ? slug : `/${slug}`;
9
+ // Backend will consider 'default' as the home slug
10
+ if (formattedSlug === '/') {
11
+ formattedSlug = '/default';
12
+ }
13
+ // Determine the URL based on whether a language is provided
14
+ let url;
15
+ if (lang && lang !== 'default') {
16
+ url = `${backendHost}/page/website/${lang}${formattedSlug}`;
17
+ }
18
+ else {
19
+ url = `${backendHost}/page/website/default${formattedSlug}`;
20
+ }
21
+ // Add preview parameter if enabled
22
+ if (isPreview) {
23
+ url += '?preview=true';
24
+ }
25
+ // Prepare fetch options
26
+ const fetchOptions = {
27
+ method: 'GET',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ };
32
+ // Send the current hostname to the backend for proper domain detection
33
+ let hostname = null;
34
+ // Server-side: Extract hostname from Astro request
35
+ if (astroRequest) {
36
+ const url = new URL(astroRequest.url);
37
+ hostname = url.hostname;
38
+ }
39
+ // Client-side: Extract hostname from window
40
+ else if (typeof window !== 'undefined') {
41
+ hostname = window.location.hostname;
42
+ }
43
+ if (hostname) {
44
+ fetchOptions.headers['X-Original-Host'] = hostname;
45
+ fetchOptions.headers['X-Frontend-Host'] = hostname;
46
+ // Note: Cannot override Host header due to browser security restrictions
47
+ }
48
+ // Add authentication headers if token exists (for both preview and protected content)
49
+ let token = authToken;
50
+ // If no token provided and we're in browser environment, try Capacitor Preferences
51
+ if (!token && typeof window !== 'undefined') {
52
+ try {
53
+ const { Preferences } = await import('@capacitor/preferences');
54
+ const tokenResult = await Preferences.get({ key: 'token' });
55
+ token = tokenResult.value;
56
+ }
57
+ catch (e) {
58
+ console.warn('Could not get token from Preferences:', e);
59
+ }
60
+ }
61
+ if (token) {
62
+ fetchOptions.headers['Authorization'] = `Bearer ${token}`;
63
+ }
64
+ // Fetch the page data from the backend
65
+ const response = await fetch(url, fetchOptions);
66
+ // Handle authentication errors
67
+ if (response.status === 401) {
68
+ return { error: true, status: 401, message: 'Authentication required' };
69
+ }
70
+ // Only treat actual 404 as not found
71
+ if (response.status === 404) {
72
+ const { detail } = await response.json();
73
+ console.warn('404', url, { detail });
74
+ // When page is not found, still fetch menus and site settings
75
+ try {
76
+ const siteSettings = await fetchPublicSettings(null, astroRequest, lang, backendHost);
77
+ return {
78
+ notFound: true,
79
+ status: 404,
80
+ detail,
81
+ public_settings: siteSettings,
82
+ lang: lang || siteSettings?.default_language?.iso_code || 'en',
83
+ };
84
+ }
85
+ catch (settingsError) {
86
+ console.warn('Could not fetch site settings for 404 page:', settingsError);
87
+ return { notFound: true, status: 404, detail };
88
+ }
89
+ }
90
+ try {
91
+ // Parse the JSON
92
+ const jsonData = await response.json();
93
+ return jsonData;
94
+ }
95
+ catch (parseError) {
96
+ console.error(`Failed to parse response: ${parseError.message}`);
97
+ return { error: true, parseError: parseError.message };
98
+ }
99
+ }
100
+ catch (error) {
101
+ console.error('Error fetching page data:', error);
102
+ return { error: true, message: error.message };
103
+ }
104
+ }
@@ -0,0 +1,5 @@
1
+ import type { SiteSettings } from '../types';
2
+ /**
3
+ * Fetches public settings from the backend
4
+ */
5
+ export declare function fetchPublicSettings(orgId?: number | null, astroRequest?: Request | null, lang?: string | null, backendHost?: string): Promise<SiteSettings | null>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Fetches public settings from the backend
3
+ */
4
+ export async function fetchPublicSettings(orgId = null, astroRequest = null, lang = null, backendHost = 'http://localhost:8000') {
5
+ try {
6
+ let url;
7
+ if (orgId === null) {
8
+ // Use domain-based detection when orgId is null
9
+ url = `${backendHost}/util/public_settings`;
10
+ if (lang) {
11
+ url += `?lang=${encodeURIComponent(lang)}`;
12
+ }
13
+ }
14
+ else {
15
+ // Handle server-side environment where localStorage is not available
16
+ let organizationId = orgId;
17
+ // Only access localStorage if we're in a browser environment and orgId not provided
18
+ if (typeof window !== 'undefined' && window.localStorage && !orgId) {
19
+ const storedOrgId = localStorage.getItem('organizationId');
20
+ organizationId = storedOrgId ? parseInt(storedOrgId) : 1;
21
+ }
22
+ url = `${backendHost}/util/public_settings/${organizationId}`;
23
+ if (lang) {
24
+ url += `?lang=${encodeURIComponent(lang)}`;
25
+ }
26
+ }
27
+ // Detect current hostname for domain-based organization detection
28
+ const headers = {
29
+ 'Content-Type': 'application/json',
30
+ };
31
+ // For domain-based detection, send the current hostname to the backend
32
+ if (orgId === null) {
33
+ let hostname = null;
34
+ // Server-side: Extract hostname from Astro request
35
+ if (astroRequest) {
36
+ const url = new URL(astroRequest.url);
37
+ hostname = url.hostname;
38
+ }
39
+ // Client-side: Extract hostname from window
40
+ else if (typeof window !== 'undefined') {
41
+ hostname = window.location.hostname;
42
+ }
43
+ if (hostname) {
44
+ headers['X-Original-Host'] = hostname;
45
+ headers['X-Frontend-Host'] = hostname;
46
+ // Note: Cannot override Host header due to browser security restrictions
47
+ }
48
+ }
49
+ const response = await fetch(url, {
50
+ method: 'GET',
51
+ headers: headers,
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(`HTTP error! status: ${response.status}`);
55
+ }
56
+ const data = (await response.json());
57
+ return data;
58
+ }
59
+ catch (error) {
60
+ console.error('Error fetching public settings:', error);
61
+ return null;
62
+ }
63
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Extracts the authentication token from cookies or URL parameter (for iframe preview)
3
+ */
4
+ export declare function getAuthToken(astro: any): any;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Extracts the authentication token from cookies or URL parameter (for iframe preview)
3
+ */
4
+ export function getAuthToken(astro) {
5
+ let authToken = null;
6
+ const cookies = astro.request.headers.get('cookie');
7
+ if (cookies) {
8
+ // Try different possible token cookie names
9
+ const tokenMatch = cookies.match(/(?:token|authToken|access_token|jwt)=([^;]+)/);
10
+ authToken = tokenMatch ? tokenMatch[1] : null;
11
+ }
12
+ // If no token from cookies, try URL parameter (for iframe preview)
13
+ if (!authToken) {
14
+ authToken = astro.url.searchParams.get('token');
15
+ }
16
+ return authToken;
17
+ }
@@ -0,0 +1,8 @@
1
+ export * from './fetchBlogListData.js';
2
+ export * from './fetchBlogPostData.js';
3
+ export * from './fetchFormData.js';
4
+ export * from './fetchPageData.js';
5
+ export * from './fetchPublicSettings.js';
6
+ export * from './getAuthToken.js';
7
+ export * from './parseSlugForLangAndPath.js';
8
+ export * from './types.js';
@@ -0,0 +1,8 @@
1
+ export * from './fetchBlogListData.js';
2
+ export * from './fetchBlogPostData.js';
3
+ export * from './fetchFormData.js';
4
+ export * from './fetchPageData.js';
5
+ export * from './fetchPublicSettings.js';
6
+ export * from './getAuthToken.js';
7
+ export * from './parseSlugForLangAndPath.js';
8
+ export * from './types.js';
@@ -0,0 +1,5 @@
1
+ import type { SlugParseResult } from './types';
2
+ /**
3
+ * Parses a slug to determine language and path
4
+ */
5
+ export declare function parseSlugForLangAndPath(slug: string | null): SlugParseResult;
@@ -0,0 +1,22 @@
1
+ import { isValidLanguageCode } from '../language';
2
+ /**
3
+ * Parses a slug to determine language and path
4
+ */
5
+ export function parseSlugForLangAndPath(slug) {
6
+ const slugParts = slug ? slug.split('/').filter(Boolean) : [];
7
+ let lang = null;
8
+ let path = '/';
9
+ // Check if the first part is a valid language code
10
+ if (slugParts.length > 0 && isValidLanguageCode(slugParts[0])) {
11
+ lang = slugParts[0];
12
+ // If there are more parts, join them as the path, otherwise keep root path
13
+ if (slugParts.length > 1) {
14
+ path = '/' + slugParts.slice(1).join('/');
15
+ }
16
+ }
17
+ else {
18
+ // No language in URL, use the path as is with a leading slash
19
+ path = slugParts.length > 0 ? '/' + slugParts.join('/') : '/';
20
+ }
21
+ return { lang, path };
22
+ }
@@ -0,0 +1,21 @@
1
+ export interface SlugParseResult {
2
+ lang: string | null;
3
+ path: string;
4
+ }
5
+ export interface ErrorResponse {
6
+ error: true;
7
+ status?: number;
8
+ message?: string;
9
+ parseError?: string;
10
+ }
11
+ export interface NotFoundResponse {
12
+ notFound: true;
13
+ status: number;
14
+ detail: string;
15
+ public_settings?: Record<string, unknown>;
16
+ lang?: string;
17
+ }
18
+ export interface BlogListResponse {
19
+ posts: Record<string, unknown>[];
20
+ }
21
+ export type ApiResponse = Record<string, unknown> | ErrorResponse | NotFoundResponse | BlogListResponse;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { MenuItem } from './menus/types';
2
+ export interface SiteSettings {
3
+ domains: string[];
4
+ available_languages: Array<{
5
+ id: number;
6
+ name: string;
7
+ iso_code: string;
8
+ emoji_flag: string;
9
+ }>;
10
+ default_language: {
11
+ id: number;
12
+ name: string;
13
+ iso_code: string;
14
+ emoji_flag: string;
15
+ };
16
+ auto_translate_pages: boolean;
17
+ auto_translate_posts: boolean;
18
+ has_openrouter_api_key: boolean;
19
+ ai_autocomplete_model_id: number;
20
+ show_post_author: boolean;
21
+ show_post_date: boolean;
22
+ show_chatbox: boolean;
23
+ website_custom_code: string | null;
24
+ menus: MenuItem[];
25
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@deepsel/cms-utils",
3
+ "version": "1.0.0",
4
+ "description": "Helper utilities for DeepCMS",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/DeepselSystems/cms-utils.git"
11
+ },
12
+ "author": "Tim Tran <tim.tran@deepsel.com> (https://deepsel.com)",
13
+ "license": "MIT",
14
+ "bugs": {
15
+ "url": "https://github.com/DeepselSystems/cms-utils/issues"
16
+ },
17
+ "homepage": "https://github.com/DeepselSystems/cms-utils#readme",
18
+ "type": "module",
19
+ "main": "dist/index.js",
20
+ "module": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsc --watch",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "eslint \"{src,tests}/**/*.ts\"",
31
+ "lint:fix": "eslint \"{src,tests}/**/*.ts\" --fix",
32
+ "format": "prettier --write .",
33
+ "format:check": "prettier --check .",
34
+ "prepush": "npm run test && npm run build && npm run lint && npm run format:check",
35
+ "publish": "npm publish --access public"
36
+ },
37
+ "keywords": [
38
+ "cms",
39
+ "utils",
40
+ "deepsel",
41
+ "typescript",
42
+ "helper",
43
+ "content-management"
44
+ ],
45
+ "devDependencies": {
46
+ "@eslint/js": "^9.39.1",
47
+ "@types/node": "^24.10.0",
48
+ "eslint": "^9.39.1",
49
+ "eslint-plugin-vitest": "^0.5.4",
50
+ "prettier": "^3.6.2",
51
+ "typescript": "^5.9.3",
52
+ "typescript-eslint": "^8.46.4",
53
+ "vitest": "^4.0.8"
54
+ },
55
+ "dependencies": {
56
+ "@capacitor/preferences": "^7.0.2"
57
+ }
58
+ }