diff --git a/.changeset/clever-yaks-push.md b/.changeset/clever-yaks-push.md
new file mode 100644
index 0000000000..8f0b743b85
--- /dev/null
+++ b/.changeset/clever-yaks-push.md
@@ -0,0 +1,6 @@
+---
+"@studiocms/devapps": patch
+"@studiocms/core": patch
+---
+
+Expand PageData table schema and add Catagory and Tags schemas, and extend WP-importer
diff --git a/packages/studiocms_core/src/db/config.ts b/packages/studiocms_core/src/db/config.ts
index d323753654..28be127aff 100644
--- a/packages/studiocms_core/src/db/config.ts
+++ b/packages/studiocms_core/src/db/config.ts
@@ -3,6 +3,8 @@ import { defineDb } from 'astro:db';
import {
StudioCMSPageContent,
StudioCMSPageData,
+ StudioCMSPageDataCategories,
+ StudioCMSPageDataTags,
StudioCMSPermissions,
StudioCMSSessionTable,
StudioCMSSiteConfig,
@@ -14,6 +16,8 @@ export default defineDb({
tables: {
StudioCMSPageContent,
StudioCMSPageData,
+ StudioCMSPageDataCategories,
+ StudioCMSPageDataTags,
StudioCMSPermissions,
StudioCMSSessionTable,
StudioCMSSiteConfig,
diff --git a/packages/studiocms_core/src/db/tables.ts b/packages/studiocms_core/src/db/tables.ts
index 6ca1bd86fc..2ba60b045d 100644
--- a/packages/studiocms_core/src/db/tables.ts
+++ b/packages/studiocms_core/src/db/tables.ts
@@ -46,6 +46,29 @@ export const StudioCMSPageData = defineTable({
default:
'https://images.unsplash.com/photo-1707343843982-f8275f3994c5?q=80&w=1032&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
}),
+ catagories: column.json({ default: [], optional: true }),
+ tags: column.json({ default: [], optional: true }),
+ },
+});
+
+export const StudioCMSPageDataTags = defineTable({
+ columns: {
+ id: column.number({ primaryKey: true }),
+ description: column.text(),
+ name: column.text(),
+ slug: column.text(),
+ meta: column.json({ default: {} }),
+ },
+});
+
+export const StudioCMSPageDataCategories = defineTable({
+ columns: {
+ id: column.number({ primaryKey: true }),
+ parent: column.number({ optional: true }),
+ description: column.text(),
+ name: column.text(),
+ slug: column.text(),
+ meta: column.json({ default: {} }),
},
});
@@ -65,6 +88,8 @@ export const StudioCMSSiteConfig = defineTable({
id: column.number({ primaryKey: true }),
title: column.text(),
description: column.text(),
+ defaultOgImage: column.text({ optional: true }),
+ siteIcon: column.text({ optional: true }),
},
});
diff --git a/packages/studiocms_core/src/db/tsTables.ts b/packages/studiocms_core/src/db/tsTables.ts
index a014bba615..9fac4e3b4f 100644
--- a/packages/studiocms_core/src/db/tsTables.ts
+++ b/packages/studiocms_core/src/db/tsTables.ts
@@ -2,6 +2,8 @@ import { asDrizzleTable } from '@astrojs/db/utils';
import {
StudioCMSPageContent,
StudioCMSPageData,
+ StudioCMSPageDataCategories,
+ StudioCMSPageDataTags,
StudioCMSPermissions,
StudioCMSSessionTable,
StudioCMSSiteConfig,
@@ -20,6 +22,21 @@ export const tsPageContent = asDrizzleTable('StudioCMSPageContent', StudioCMSPag
*/
export const tsPageData = asDrizzleTable('StudioCMSPageData', StudioCMSPageData);
+/**
+ * # StudioCMS - Page Data Categories Table
+ * @description Exported TypeSafe Table definition for use in StudioCMS Integrations
+ */
+export const tsPageDataCategories = asDrizzleTable(
+ 'StudioCMSPageDataCategories',
+ StudioCMSPageDataCategories
+);
+
+/**
+ * # StudioCMS - Page Data Tags Table
+ * @description Exported TypeSafe Table definition for use in StudioCMS Integrations
+ */
+export const tsPageDataTags = asDrizzleTable('StudioCMSPageDataTags', StudioCMSPageDataTags);
+
/**
* # StudioCMS - Permissions Table
* @description Exported TypeSafe Table definition for use in StudioCMS Integrations
diff --git a/packages/studiocms_devapps/src/apps/wp-importer.ts b/packages/studiocms_devapps/src/apps/wp-importer.ts
index 4847fb9015..e27d900a4c 100644
--- a/packages/studiocms_devapps/src/apps/wp-importer.ts
+++ b/packages/studiocms_devapps/src/apps/wp-importer.ts
@@ -139,6 +139,7 @@ export default defineToolbarApp({
diff --git a/packages/studiocms_devapps/src/routes/wp-api-importer.ts b/packages/studiocms_devapps/src/routes/wp-api-importer.ts
index 34e38b1163..46d68a264f 100644
--- a/packages/studiocms_devapps/src/routes/wp-api-importer.ts
+++ b/packages/studiocms_devapps/src/routes/wp-api-importer.ts
@@ -1,5 +1,9 @@
import type { APIContext, APIRoute } from 'astro';
-import { importPagesFromWPAPI, importPostsFromWPAPI } from '../utils/wp-api';
+import {
+ importPagesFromWPAPI,
+ importPostsFromWPAPI,
+ importSettingsFromWPAPI,
+} from '../utils/wp-api';
export const POST: APIRoute = async ({ request }: APIContext) => {
const data = await request.formData();
@@ -41,6 +45,9 @@ export const POST: APIRoute = async ({ request }: APIContext) => {
case 'posts':
await importPostsFromWPAPI(url, useBlogPluginValue);
break;
+ case 'settings':
+ await importSettingsFromWPAPI(url);
+ break;
default:
throw new Error('Invalid import type');
}
diff --git a/packages/studiocms_devapps/src/schema/wp-api/index.ts b/packages/studiocms_devapps/src/schema/wp-api/index.ts
index cae120d1a2..07f3ab4292 100644
--- a/packages/studiocms_devapps/src/schema/wp-api/index.ts
+++ b/packages/studiocms_devapps/src/schema/wp-api/index.ts
@@ -43,5 +43,35 @@ export const PostSchema = PageSchema.extend({
tags: z.array(z.number()),
});
+export const TagSchema = z.object({
+ id: z.number(),
+ count: z.number(),
+ description: z.string(),
+ link: z.string().url(),
+ name: z.string(),
+ slug: z.string(),
+ taxonomy: z.string(),
+ meta: z.array(z.any()).or(z.record(z.any())),
+});
+
+export const CategorySchema = TagSchema.extend({
+ parent: z.number(),
+});
+
+export const SiteSettingsSchema = z.object({
+ name: z.string(),
+ description: z.string(),
+ url: z.string(),
+ home: z.string(),
+ gmt_offset: z.coerce.number(),
+ timezone_string: z.string(),
+ site_logo: z.number().optional(),
+ site_icon: z.number().optional(),
+ site_icon_url: z.string().optional(),
+});
+
export type Page = typeof PageSchema._output;
export type Post = typeof PostSchema._output;
+export type Tag = typeof TagSchema._output;
+export type Category = typeof CategorySchema._output;
+export type SiteSettings = typeof SiteSettingsSchema._output;
diff --git a/packages/studiocms_devapps/src/utils/wp-api/converters.ts b/packages/studiocms_devapps/src/utils/wp-api/converters.ts
index ae431b9b1b..13b43ca477 100644
--- a/packages/studiocms_devapps/src/utils/wp-api/converters.ts
+++ b/packages/studiocms_devapps/src/utils/wp-api/converters.ts
@@ -1,8 +1,10 @@
import path from 'node:path';
+import { db, eq } from 'astro:db';
import Config from 'virtual:studiocms-devapps/wp-api/configPath';
+import { tsPageDataCategories, tsPageDataTags } from '@studiocms/core/db/tsTables';
import { decode } from 'html-entities';
import TurndownService from 'turndown';
-import type { Page, Post } from '../../schema/wp-api';
+import type { Category, Page, Post, Tag } from '../../schema/wp-api';
import type { PageContent, PageData } from './index';
import {
apiEndpoint,
@@ -17,11 +19,11 @@ const WPImportFolder = path.resolve(ASTROPUBLICFOLDER, 'wp-import');
const pagesImagesFolder = path.resolve(WPImportFolder, 'pages');
const postsImagesFolder = path.resolve(WPImportFolder, 'posts');
-export const ConvertToPageData = async (page: unknown): Promise => {
+export const ConvertToPageData = async (page: unknown, endpoint: string): Promise => {
const data = page as Page;
const titleImageId = data.featured_media;
- const titleImageURL = apiEndpoint(`${titleImageId}`, 'media');
+ const titleImageURL = apiEndpoint(endpoint, 'media', `${titleImageId}`);
const titleImageResponse = await fetch(titleImageURL);
const titleImageJson = await titleImageResponse.json();
const titleImage = await downloadPostImage(titleImageJson.source_url, pagesImagesFolder);
@@ -76,17 +78,112 @@ export const ConvertToPageContent = async (
return pageContent;
};
-export const ConvertToPostData = async (post: unknown, useBlogPkg: boolean): Promise => {
+export const generateCatagories = async (categories: number[], endpoint: string) => {
+ const newCatagories: Category[] = [];
+
+ for (const catagoryId of categories) {
+ // Check if catagory already exists in the database
+ const catagoryExists = await db
+ .select()
+ .from(tsPageDataCategories)
+ .where(eq(tsPageDataCategories.id, catagoryId))
+ .get();
+
+ if (catagoryExists) {
+ console.log(`Catagory with id ${catagoryId} already exists in the database`);
+ continue;
+ }
+
+ const catagoryURL = apiEndpoint(endpoint, 'catagories', `${catagoryId}`);
+ const response = await fetch(catagoryURL);
+ const json = await response.json();
+ newCatagories.push(json);
+ }
+
+ if (newCatagories.length > 0) {
+ const catagoryData = newCatagories.map((catagory) => {
+ const data: typeof tsPageDataCategories.$inferInsert = {
+ id: catagory.id,
+ name: catagory.name,
+ slug: catagory.slug,
+ description: catagory.description,
+ meta: JSON.stringify(catagory.meta),
+ };
+
+ if (catagory.parent) {
+ data.parent = catagory.parent;
+ }
+
+ return data;
+ });
+
+ for (const catagory of catagoryData) {
+ console.log(`Inserting catagory with id ${catagory.id} into the database`);
+ await db.insert(tsPageDataCategories).values(catagory);
+ }
+ }
+};
+
+export const generateTags = async (tags: number[], endpoint: string) => {
+ const newTags: Tag[] = [];
+
+ for (const tagId of tags) {
+ // Check if tag already exists in the database
+ const tagExists = await db
+ .select()
+ .from(tsPageDataTags)
+ .where(eq(tsPageDataTags.id, tagId))
+ .get();
+
+ if (tagExists) {
+ console.log(`Tag with id ${tagId} already exists in the database`);
+ continue;
+ }
+
+ const tagURL = apiEndpoint(endpoint, 'tags', `${tagId}`);
+ const response = await fetch(tagURL);
+ const json = await response.json();
+ newTags.push(json);
+ }
+
+ if (newTags.length > 0) {
+ const tagData = newTags.map((tag) => {
+ const data: typeof tsPageDataTags.$inferInsert = {
+ id: tag.id,
+ name: tag.name,
+ slug: tag.slug,
+ description: tag.description,
+ meta: JSON.stringify(tag.meta),
+ };
+
+ return data;
+ });
+
+ for (const tag of tagData) {
+ console.log(`Inserting tag with id ${tag.id} into the database`);
+ await db.insert(tsPageDataTags).values(tag);
+ }
+ }
+};
+
+export const ConvertToPostData = async (
+ post: unknown,
+ useBlogPkg: boolean,
+ endpoint: string
+): Promise => {
const data = post as Post;
const titleImageId = data.featured_media;
- const titleImageURL = apiEndpoint(`${titleImageId}`, 'media');
+ const titleImageURL = apiEndpoint(endpoint, 'media', `${titleImageId}`);
const titleImageResponse = await fetch(titleImageURL);
const titleImageJson = await titleImageResponse.json();
const titleImage = await downloadPostImage(titleImageJson.source_url, pagesImagesFolder);
const pkg = useBlogPkg ? '@studiocms/blog' : 'studiocms';
+ await generateCatagories(data.categories, endpoint);
+ await generateTags(data.tags, endpoint);
+
const pageData: PageData = {
id: crypto.randomUUID(),
title: data.title.rendered,
@@ -97,6 +194,8 @@ export const ConvertToPostData = async (post: unknown, useBlogPkg: boolean): Pro
showOnNav: false,
contentLang: 'default',
package: pkg,
+ catagories: JSON.stringify(data.categories),
+ tags: JSON.stringify(data.tags),
};
if (titleImage) {
diff --git a/packages/studiocms_devapps/src/utils/wp-api/index.ts b/packages/studiocms_devapps/src/utils/wp-api/index.ts
index a5ed7c12fe..68166bf8ac 100644
--- a/packages/studiocms_devapps/src/utils/wp-api/index.ts
+++ b/packages/studiocms_devapps/src/utils/wp-api/index.ts
@@ -1,34 +1,39 @@
+import path from 'node:path';
///
-import { db } from 'astro:db';
-import { tsPageContent, tsPageData } from '@studiocms/core/db/tsTables';
-import type { Page } from '../../schema/wp-api';
+import { db, eq } from 'astro:db';
+import Config from 'virtual:studiocms-devapps/wp-api/configPath';
+import { CMSSiteConfigId } from '@studiocms/core/consts';
+import { tsPageContent, tsPageData, tsSiteConfig } from '@studiocms/core/db/tsTables';
+import type { Page, SiteSettings } from '../../schema/wp-api';
import {
ConvertToPageContent,
ConvertToPageData,
ConvertToPostContent,
ConvertToPostData,
} from './converters';
-import { apiEndpoint, fetchAll } from './utils';
+import { apiEndpoint, downloadPostImage, fetchAll } from './utils';
+
+const ASTROPUBLICFOLDER = path.resolve(Config.projectRoot, 'public');
export type PageData = typeof tsPageData.$inferInsert;
export type PageContent = typeof tsPageContent.$inferInsert;
-const generatePageFromData = async (page: unknown) => {
- const pageData = await ConvertToPageData(page);
+const generatePageFromData = async (page: unknown, endpoint: string) => {
+ const pageData = await ConvertToPageData(page, endpoint);
const pageContent = await ConvertToPageContent(pageData, page);
return { pageData, pageContent };
};
-const generatePostFromData = async (post: unknown, useBlogPkg: boolean) => {
- const pageData = await ConvertToPostData(post, useBlogPkg);
+const generatePostFromData = async (post: unknown, useBlogPkg: boolean, endpoint: string) => {
+ const pageData = await ConvertToPostData(post, useBlogPkg, endpoint);
const pageContent = await ConvertToPostContent(pageData, post);
return { pageData, pageContent };
};
-const importPage = async (page: unknown) => {
- const { pageData, pageContent } = await generatePageFromData(page);
+const importPage = async (page: unknown, endpoint: string) => {
+ const { pageData, pageContent } = await generatePageFromData(page, endpoint);
const pageDataResult = await db
.insert(tsPageData)
@@ -65,15 +70,15 @@ export const importPagesFromWPAPI = async (endpoint: string) => {
try {
for (const page of pages) {
console.log('importing page:', page.title.rendered);
- await importPage(page);
+ await importPage(page, endpoint);
}
} catch (error) {
console.error('Failed to import pages from WP-API:', error);
}
};
-const importPost = async (post: unknown, useBlogPkg: boolean) => {
- const { pageData, pageContent } = await generatePostFromData(post, useBlogPkg);
+const importPost = async (post: unknown, useBlogPkg: boolean, endpoint: string) => {
+ const { pageData, pageContent } = await generatePostFromData(post, useBlogPkg, endpoint);
const pageDataResult = await db
.insert(tsPageData)
@@ -110,9 +115,60 @@ export const importPostsFromWPAPI = async (endpoint: string, useBlogPkg: boolean
try {
for (const post of posts) {
console.log('importing post: ', post.title.rendered);
- await importPost(post, useBlogPkg);
+ await importPost(post, useBlogPkg, endpoint);
}
} catch (error) {
console.error('Failed to import posts from WP-API: ', error);
}
};
+
+export const importSettingsFromWPAPI = async (endpoint: string) => {
+ const url = apiEndpoint(endpoint, 'settings');
+
+ console.log('Fetching site settings from: ', url.origin);
+
+ const response = await fetch(url);
+ const settings: SiteSettings = await response.json();
+
+ console.log('Importing site settings: ', settings);
+
+ let siteIcon: string | undefined = undefined;
+
+ if (settings.site_icon_url) {
+ siteIcon = await downloadPostImage(settings.site_icon_url, ASTROPUBLICFOLDER);
+ }
+
+ if (!settings.site_icon_url && settings.site_logo) {
+ const siteLogoURL = apiEndpoint(endpoint, 'media', `${settings.site_logo}`);
+ const siteLogoResponse = await fetch(siteLogoURL);
+ const siteLogoJson = await siteLogoResponse.json();
+ siteIcon = await downloadPostImage(siteLogoJson.source_url, ASTROPUBLICFOLDER);
+ }
+
+ const siteConfig: typeof tsSiteConfig.$inferInsert = {
+ id: CMSSiteConfigId,
+ title: settings.name,
+ description: settings.description,
+ };
+
+ if (siteIcon) {
+ siteConfig.siteIcon = siteIcon;
+ }
+
+ try {
+ const insert = await db
+ .update(tsSiteConfig)
+ .set(siteConfig)
+ .where(eq(tsSiteConfig.id, CMSSiteConfigId))
+ .returning({ id: tsSiteConfig.id })
+ .get();
+
+ if (insert) {
+ console.log('Updated site settings');
+ } else {
+ console.error('Failed to update site settings');
+ }
+ } catch (error) {
+ console.error('Failed to import site settings from WP-API: ', error);
+ }
+};
diff --git a/packages/studiocms_devapps/src/utils/wp-api/utils.ts b/packages/studiocms_devapps/src/utils/wp-api/utils.ts
index dcdc920d87..e09486476c 100644
--- a/packages/studiocms_devapps/src/utils/wp-api/utils.ts
+++ b/packages/studiocms_devapps/src/utils/wp-api/utils.ts
@@ -106,7 +106,11 @@ export const downloadAndUpdateImages = async (html: string, pathToFolder: string
return $.html();
};
-export const apiEndpoint = (endpoint: string, type: 'posts' | 'pages' | 'media') => {
+export const apiEndpoint = (
+ endpoint: string,
+ type: 'posts' | 'pages' | 'media' | 'catagories' | 'tags' | 'settings',
+ path?: string
+) => {
if (!endpoint) {
throw new AstroError(
'Missing `endpoint` argument.',
@@ -115,8 +119,15 @@ export const apiEndpoint = (endpoint: string, type: 'posts' | 'pages' | 'media')
}
let newEndpoint = endpoint;
if (!newEndpoint.endsWith('/')) newEndpoint += '/';
+
const apiBase = new URL(newEndpoint);
- apiBase.pathname = `wp-json/wp/v2/${type}`;
+
+ if (type === 'settings') {
+ apiBase.pathname = 'wp-json/';
+ return apiBase;
+ }
+
+ apiBase.pathname = `wp-json/wp/v2/${type}/${path ? `${path}/` : ''}`;
return apiBase;
};