diff --git a/index.ts b/index.ts index 678907b2..ac73f797 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,17 @@ -import type { AstroIntegration } from "astro" -import { ZodError, type SafeParseError, type SafeParseSuccess } from "astro/zod" -import { UserConfigSchema, type UserConfig } from "./src/utils/UserConfigSchema" +import type { AstroIntegration } from "astro"; +import type { SafeParseError, SafeParseSuccess } from "astro/zod"; +import { UserConfigSchema, type UserConfig } from "./src/utils/UserConfigSchema"; +import { ghostSitemap, ghostRobots } from "./src/integrations"; +import { loadEnv } from 'vite'; +import { fromZodError } from "zod-validation-error"; -const pkg = '@matthiesenxyz/astro-ghostcms' +// LOAD ENVIRONMENT VARIABLES +const mode = 'all'; +const prefixes = 'CONTENT_API'; +const env = loadEnv(mode, process.cwd(), prefixes); + +// SET LOCAL PACKAGE NAME +const pkg = '@matthiesenxyz/astro-ghostcms'; export default function GhostCMS(options: UserConfig): AstroIntegration { return { @@ -10,77 +19,108 @@ export default function GhostCMS(options: UserConfig): AstroIntegration { hooks: { 'astro:config:setup': async ({ injectRoute, + config, logger, }) => { - - const o = UserConfigSchema.safeParse(options || {}) as SafeParseSuccess + // Check For ENV Variables + logger.info("Checking for Environment Variables...") + // CHECK FOR API KEY + if(env.CONTENT_API_KEY === undefined){ + logger.error("CONTENT_API_KEY Missing from .env") + } + // CHECK FOR API URL + if(env.CONTENT_API_URL === undefined){ + logger.error("CONTENT_API_URL Missing from .env") + } + + // CHECK USER CONFIG + logger.info("Checking Config...") + const o = UserConfigSchema.safeParse(options || {}) as SafeParseSuccess; if (!o.success) { - const validationError = fromZodError((o as unknown as SafeParseError).error) - - logger.error(`Config Error - ${ validationError }`) - - throw validationError + const validationError = fromZodError((o as unknown as SafeParseError).error); + logger.error(`Config Error - ${ validationError }`); + throw validationError; } + const entry = o.data.theme; - const entry = o.data.theme - + // THEME SELECTOR if (entry === pkg) { - logger.info("Injecting Theme: astro-ghostcms-basetheme") + logger.info("Injecting Theme: astro-ghostcms-basetheme"); } else { - logger.info(`Injecting Theme: ${entry}`) + logger.info(`Injecting Theme: ${entry}`); } + // INJECT ROUTES + logger.info("Injecting Routes..."); - logger.info("Injecting Route: /") injectRoute({ pattern: '/', entrypoint: `${entry}/index.astro` - }) + }); - logger.info("Injecting Route: /[slug]") injectRoute({ pattern: '/[slug]', entrypoint: `${entry}/[slug].astro` - }) + }); - logger.info("Injecting Route: /tags") injectRoute({ pattern: '/tags', entrypoint: `${entry}/tags.astro` - }) + }); - logger.info("Injecting Route: /authors") injectRoute({ pattern: '/authors', entrypoint: `${entry}/authors.astro` - }) + }); - logger.info("Injecting Route: /tag/[slug]") injectRoute({ pattern: '/tag/[slug]', entrypoint: `${entry}/tag/[slug].astro` - }) + }); - logger.info("Injecting Route: /author/[slug]") injectRoute({ pattern: '/author/[slug]', entrypoint: `${entry}/author/[slug].astro` - }) - logger.info("Injecting Route: /archives/[...page]") + }); + injectRoute({ pattern: '/archives/[...page]', entrypoint: `${entry}/archives/[...page].astro` - }) + }); + + // IMPORT INTEGRATIONS & INTEGRATION ROUTES + const int = [...config.integrations]; + + // IMPORT INTEGRATION: @ASTROJS/RSS + logger.info("Injecting Integration Route: @astrojs/rss"); + injectRoute({ + pattern: '/rss.xml', + entrypoint: `${pkg}/rss.xml.js` + }); + + // IMPORT INTEGRATION: @ASTROJS/SITEMAP + logger.info("Checking for @astrojs/sitemap"); + if (!int.find(({ name }) => name === '@astrojs/sitemap')) { + logger.info("Injecting Integration: @astrojs/sitemap"); + int.push(ghostSitemap()); + } else { + logger.info("Already Imported by User: @astrojs/sitemap"); + } + + // IMPORT INTEGRATION: ASTRO-ROBOTS-TXT + logger.info("Checking for astro-robots-txt"); + if (!int.find(({ name }) => name === 'astro-robots-txt')) { + logger.info("Injecting Integration: astro-robots-txt"); + int.push(ghostRobots()); + } else { + logger.info("Already Imported by User: astro-robots-txt"); + } }, 'astro:config:done': async ({ logger }) => { - logger.info('GhostCMS Routes Injected. Integration is now ready.') + logger.info('GhostCMS Injection Complete. Integration is now ready.'); } } } } - -function fromZodError(error: ZodError<{ theme: string }>) { - throw new Error("Function not implemented.") -} diff --git a/package.json b/package.json index d30e73f9..96d6b7a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@matthiesenxyz/astro-ghostcms", "description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content", - "version": "2.0.9", + "version": "2.1.0", "author": "MatthiesenXYZ (https://matthiesen.xyz)", "type": "module", "license": "MIT", @@ -10,7 +10,8 @@ "url": "git+https://github.com/MatthiesenXYZ/astro-ghostcms.git" }, "bugs": { - "url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues" + "url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues", + "email": "issues@astro-ghostcms.xyz" }, "homepage": "https://astro-ghostcms.xyz/", "exports": { @@ -23,10 +24,10 @@ "./tag/[slug].astro": "./src/routes/tag/[slug].astro", "./author/[slug].astro": "./src/routes/author/[slug].astro", "./archives/[...page].astro": "./src/routes/archives/[...page].astro", - "./DefaultLayout": "./src/layouts/default.astro" + "./rss.xml.js": "./src/routes/rss.xml.js" }, "main": "index.ts", - "types": "src/api/ghosttypes.ts", + "types": "src/api/tryghost-content-api.d.ts", "files": [ "src", "index.ts" @@ -56,11 +57,13 @@ "dependencies": { "@astrojs/check": "^0.3.4", "typescript": "^5.3.3", + "zod-validation-error": "^3.0.0", "axios": "^1.0.0", "astro-font": "^0.0.72", "@astrojs/renderer-svelte": "0.5.2", "@astrojs/rss": "^4.0.2", "@astrojs/sitemap": "^3.0.4", + "astro-robots-txt": "^1.0.0", "@snowpack/plugin-dotenv": "^2.2.0", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", diff --git a/src/api/functions.ts b/src/api/functions.ts index be77722e..509ce4c0 100644 --- a/src/api/functions.ts +++ b/src/api/functions.ts @@ -1,8 +1,26 @@ -// IMPORT Ghost Types -import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from './ghosttypes'; +// IMPORT Ghost Types & Content-API +import type { + PostOrPage, PostsOrPages, Authors, + Tag, Tags, ArrayOrValue, IncludeParam, + LimitParam, Settings, Nullable +} from './tryghost-content-api'; +import GhostContentAPI from './tryghost-content-api'; -// IMPORT Ghost API Client -import api from './interface'; +// LOAD ENVIRONMENT VARIABLES +import { loadEnv } from 'vite'; + +const {CONTENT_API_KEY, CONTENT_API_URL} = loadEnv( + 'all', + process.cwd(), + 'CONTENT_' +); + +let key = CONTENT_API_KEY; +let url = CONTENT_API_URL; + +// SETUP API +const version = "v5.0"; +const api = GhostContentAPI({ key, url, version }); // SET Include params const include:ArrayOrValue = ['authors', 'tags']; @@ -10,54 +28,54 @@ const include:ArrayOrValue = ['authors', 'tags']; // Get Posts (General "ALL") export const getGhostPosts = async () => { const ghostPosts:PostsOrPages = await api.posts.browse({include,filter:'visibility:public'}) - return ghostPosts; } + return ghostPosts; }; // Get Posts (Recent "setLimit?") export const getGhostRecentPosts = async (setLimit?:ArrayOrValue) => { const ghostRecentPosts:PostsOrPages = await api.posts.browse({limit:setLimit?setLimit:"6",include,filter:'visibility:public'}); - return ghostRecentPosts; } + return ghostRecentPosts; }; // Get Posts (Featured "setLimit?") export const getGhostFeaturedPosts = async (setLimit?:ArrayOrValue) => { const ghostFeaturedPosts:PostsOrPages = await api.posts.browse({limit:setLimit?setLimit:"1",include,filter:'featured:true'}); - return ghostFeaturedPosts; } + return ghostFeaturedPosts; }; // Get Post (By Slug) export const getGhostPostbySlug = async (slug:Nullable) => { const ghostPostbySlug:PostOrPage = await api.posts.read({slug},{include}); - return ghostPostbySlug; } + return ghostPostbySlug; }; // Get Post (By Tag) export const getGhostPostsbyTag = async (slug:Nullable) => { const ghostPostsbyTag:PostsOrPages = await api.posts.browse({filter:`tag:${slug}`,include}); - return ghostPostsbyTag; } + return ghostPostsbyTag; }; // Get Tags (General "ALL") export const getGhostTags = async () => { const ghostTags:Tags = await api.tags.browse({include:`count.posts`}); - return ghostTags; } + return ghostTags; }; // Get Tag (By Slug) export const getGhostTagbySlug = async (slug:Nullable) => { const ghostTagbySlug:Tag = await api.tags.read({slug},{include:`count.posts`}); - return ghostTagbySlug; } + return ghostTagbySlug; }; // Get Authors (General "ALL") export const getGhostAuthors = async () => { const ghostAuthors:Authors = await api.authors.browse({include:`count.posts`}); - return ghostAuthors; } + return ghostAuthors; }; // Get Pages (ALL) export const getGhostPages = async () => { const ghostPages:PostsOrPages = await api.pages.browse(); - return ghostPages; } + return ghostPages; }; // Get Page (by Slug) export const getGhostPage = async (slug:Nullable) => { const ghostPage:PostOrPage = await api.pages.read({slug}); - return ghostPage; } + return ghostPage; }; // Get Settings export const getGhostSettings = async () => { const ghostSettings:Settings = await api.settings.browse(); - return ghostSettings; } + return ghostSettings; }; diff --git a/src/api/index.ts b/src/api/index.ts index 1621bb3a..41b0dee5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,22 @@ // FUNCTION EXPORTS -export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './functions'; +export { + getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, + getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, + getGhostTagbySlug, getGhostAuthors, getGhostPages, + getGhostPage, getGhostSettings +} from './functions'; // TYPE EXPORTS -export type { PostOrPage, ArrayOrValue, Author, Authors, BrowseFunction, CodeInjection, Excerpt, Facebook, FieldParam, FilterParam, FormatParam, GhostAPI, GhostContentAPIOptions, GhostData, GhostError, Identification, IncludeParam, LimitParam, Metadata, Nullable, OrderParam, PageParam, Pagination, Params, PostsOrPages, ReadFunction, Settings, SettingsResponse, SocialMedia, Tag, TagVisibility, Tags, Twitter } from './ghosttypes'; \ No newline at end of file +export type { + PostOrPage, ArrayOrValue, Author, + Authors, BrowseFunction, CodeInjection, + Excerpt, Facebook, FieldParam, + FilterParam, FormatParam, GhostAPI, + GhostContentAPIOptions, GhostData, GhostError, + Identification, IncludeParam, LimitParam, + Metadata, Nullable, OrderParam, + PageParam, Pagination, Params, + PostsOrPages, ReadFunction, Settings, + SettingsResponse, SocialMedia, Tag, + TagVisibility, Tags, Twitter +} from './tryghost-content-api.d'; diff --git a/src/api/interface.js b/src/api/interface.js deleted file mode 100644 index 40f8fe9f..00000000 --- a/src/api/interface.js +++ /dev/null @@ -1,10 +0,0 @@ -import GhostContentAPI from './tryghost-content-api'; - -// CALL GHOST VARS AND CREATE CLIENT -const key = import.meta.env.CONTENT_API_KEY; -const url = import.meta.env.CONTENT_API_URL; -const version = "v5.0" - -const api = new GhostContentAPI({ key, url, version }) - -export default api; \ No newline at end of file diff --git a/src/api/ghosttypes.ts b/src/api/tryghost-content-api.d.ts similarity index 100% rename from src/api/ghosttypes.ts rename to src/api/tryghost-content-api.d.ts diff --git a/src/env.d.ts b/src/env.d.ts deleted file mode 100644 index 8c34fb45..00000000 --- a/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/src/integrations/index.ts b/src/integrations/index.ts new file mode 100644 index 00000000..7afab982 --- /dev/null +++ b/src/integrations/index.ts @@ -0,0 +1,2 @@ +export { default as ghostSitemap } from "./sitemap" +export { default as ghostRobots } from "./robots-txt" \ No newline at end of file diff --git a/src/integrations/robots-txt.ts b/src/integrations/robots-txt.ts new file mode 100644 index 00000000..ea822374 --- /dev/null +++ b/src/integrations/robots-txt.ts @@ -0,0 +1,13 @@ +import robotsTxt, { type RobotsTxtOptions } from "astro-robots-txt"; + +export function getRobotsTxtConfig(): RobotsTxtOptions { + const robotsConfig: RobotsTxtOptions = {}; + return robotsConfig; +} + +/** + * A wrapped version of the `astro-robots-txt` integration for GhostCMS. + */ +export default function ghostRobots() { + return robotsTxt(getRobotsTxtConfig()); +} \ No newline at end of file diff --git a/src/integrations/sitemap.ts b/src/integrations/sitemap.ts new file mode 100644 index 00000000..db61c4c7 --- /dev/null +++ b/src/integrations/sitemap.ts @@ -0,0 +1,13 @@ +import sitemap, { type SitemapOptions } from '@astrojs/sitemap'; + +export function getSitemapConfig(): SitemapOptions { + const sitemapConfig: SitemapOptions = {}; + return sitemapConfig; +} + +/** + * A wrapped version of the `@astrojs/sitemap` integration for GhostCMS. + */ +export default function ghostSitemap() { + return sitemap(getSitemapConfig()); +} \ No newline at end of file diff --git a/src/routes/rss.xml.js b/src/routes/rss.xml.js new file mode 100644 index 00000000..828f0293 --- /dev/null +++ b/src/routes/rss.xml.js @@ -0,0 +1,23 @@ +import rss from "@astrojs/rss"; +import { getGhostPosts, getGhostSettings } from '../api'; +import invariant from "tiny-invariant"; + +export async function GET(context) { + const posts = await getGhostPosts(); + const settings = await getGhostSettings(); + invariant(settings, "Settings not found"); + const title = settings.title; + const description = settings.description; + return rss({ + title: title, + description: description, + site: context.site, + //stylesheet: '/rss/styles.xsl', + items: posts.map((post) => ({ + title: post.title, + pubDate: post.published_at, + description: post.excerpt, + link: `/blog/${post.slug}/`, + })), + }); +} \ No newline at end of file diff --git a/src/utils/UserConfigSchema.ts b/src/utils/UserConfigSchema.ts index fd059c9b..bc920848 100644 --- a/src/utils/UserConfigSchema.ts +++ b/src/utils/UserConfigSchema.ts @@ -1,7 +1,8 @@ import { z } from 'astro/zod'; export const UserConfigSchema = z.object({ - theme: z.string().default('@matthiesenxyz/astro-ghostcms') + theme: z.string().default('@matthiesenxyz/astro-ghostcms'), + rssStyle: z.string().optional() }); export type UserConfig = z.infer \ No newline at end of file