Chore: Upgrade to `AIK` & Massive Overhaul to internal processing (#78)

This PR is described under Issue #77
This commit is contained in:
Adam Matthiesen 2024-03-07 04:08:19 -08:00 committed by GitHub
commit 62159744bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
178 changed files with 1834 additions and 2771 deletions

View File

@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["playground","starlight-playground"]
"ignore": ["astro-playground", "starlight-playground"]
}

View File

@ -0,0 +1,5 @@
---
"@matthiesenxyz/create-astro-ghostcms": minor
---
Bumb all templates to new `astro-ghostcms` version v3.3

View File

@ -0,0 +1,13 @@
---
"@matthiesenxyz/astro-ghostcms": minor
---
This is a HUGE internal update, Our integration is now built on [`Astro-Integration-Kit`](https://github.com/florian-lefebvre/astro-integration-kit) to give better control over the entire `Astro-GhostCMS` Eco-System.
# Breaking Changes:
- NEW USER CONFIG! Some of the options have changed! Please check the Readme for a current version of the available options!
- Thats it! Some how even though this is almost an entire rebuild, There is no other USER breaking changes aside from the new more advanced config!
# Updates:
- Moved from `@ts-ghost/core-api` to `@ts-ghost/content-api` as it provides the same functions as the standard core-api but pre-wrapped with a nice `HTTPClientFactory` instead of `HTTPClient`.
- Updated a ton of Dependencies that Dependabot was reporting as needed updated.

View File

@ -0,0 +1,8 @@
---
"@matthiesenxyz/starlight-ghostcms": minor
---
Bumb GhostCMS API, No user facing breaking changes.
NEW:
- You can now set a `route: "blog"` in your `astro.config.mjs` to change the default `/<route>` to your blog/posts

View File

@ -30,7 +30,9 @@ This repo is structured as a `pnpm` monorepo. All of our packages can be found
In this Repo you will find the Following:
- `playground`: Development and Testing
- `playgrounds/`:
- [`astro-playground`](./playgrounds/astro-playground/): Playground for Astro-GhostCMS development and testing.
- [`starlight-playground`](./playgrounds/starlight-playground/): Playground of Starlight-GhostCMS development and testing.
- `packages/`:
- [`create-astro-ghostcms`](./packages/create-astro-ghostcms/): CLI Utility to quickly deploy new Astro-GhostCMS projects.
- [`astro-ghostcms`](./packages/astro-ghostcms/): The main Integration!
@ -39,7 +41,6 @@ In this Repo you will find the Following:
- [`astro-ghostcms-catppuccin`](./packages/astro-ghostcms-catppuccin/): A dark theme made with Catppuccin and TailwindCSS for Astro-GhostCMS Integration Mode.
- [`astro-ghostcms-brutalbyelian`](./packages/astro-ghostcms-brutalbyelian/): [ElianCodes](https://www.elian.codes/) Brutal theme modified to work with Astro-GhostCMS
- [`starlight-ghostcms`](./packages/starlight-ghostcms/) A [Starlight Plugin](https://starlight.astro.build/resources/plugins/) to integrate your GhostCMS into your documentation website
- `tsconfig`: *LOCAL* Development package for `@ts-ghost/core-api`.
## Contributing

View File

@ -6,26 +6,26 @@
"node": ">=18.19.0"
},
"scripts": {
"dev": "pnpm --filter playground dev",
"astro:dev": "pnpm --filter astro-playground dev",
"starlight:dev": "pnpm --filter starlight-playground dev",
"lint": "biome check .",
"lint:fix": "biome check --apply .",
"ci:version": "pnpm changeset version",
"ci:publish": "pnpm changeset publish",
"ci:test:api": "pnpm --filter astro-ghostcms test:ci",
"test:api": "pnpm --filter astro-ghostcms test",
"test:api:watch": "pnpm --filter astro-ghostcms test:watch",
"test:api:coverage": "pnpm --filter astro-ghostcms test:coverage",
"test:create": "pnpm --filter create-astro-ghostcms test",
"test:slg": "pnpm --filter starlight-ghostcms test",
"test:slg:watch": "pnpm --filter starlight-ghostcms test:watch",
"test:slg:coverage": "pnpm --filter starlight-ghostcms test:coverage"
"ci:test:integration": "pnpm --filter astro-ghostcms test:ci",
"test:integration": "pnpm --filter astro-ghostcms test",
"test:integration:watch": "pnpm --filter astro-ghostcms test:watch",
"test:integration:coverage": "pnpm --filter astro-ghostcms test:coverage",
"test:create-utility": "pnpm --filter create-astro-ghostcms test",
"test:starlight": "pnpm --filter starlight-ghostcms test",
"test:starlight:watch": "pnpm --filter starlight-ghostcms test:watch",
"test:starlight:coverage": "pnpm --filter starlight-ghostcms test:coverage"
},
"devDependencies": {
"@biomejs/biome": "1.5.3",
"@changesets/cli": "^2.27.1",
"vitest": "^1.3.0",
"vitest-fetch-mock": "^0.2.2",
"@vitest/ui": "1.3.0"
"@vitest/ui": "^1.3.1",
"vitest": "^1.3.1",
"vitest-fetch-mock": "^0.2.2"
}
}

View File

@ -1,2 +0,0 @@
CONTENT_API_KEY=a33da3965a3a9fb2c6b3f63b48
CONTENT_API_URL=https://ghostdemo.matthiesen.xyz

View File

@ -60,4 +60,4 @@
- Initialization of changeset cli
- Updated dependencies
- @matthiesenxyz/astro-ghostcms-theme-default@0.1.10
- @matthiesenxyz/astro-ghostcms-theme-default@0.1.10

View File

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@ -59,7 +59,31 @@ import GhostCMS from '@matthiesenxyz/astro-ghostcms';
// https://astro.build/config
export default defineConfig({
site: "https://YOUR-DOMAIN-HERE.com"
integrations: [GhostCMS()],
integrations: [
GhostCMS({
// Config Options
ghostURL: "http://example.com", // Recommended to set here, Can also set in .env as CONTENT_API_URL
ThemeProvider: { // Allows you to pass config options to our ThemeProvider if enabled.
disableThemeProvider: false, // OPTIONAL - Default False
theme: "@matthiesenxyz/astro-ghostcms-theme-default", // OPTIONAL - Default Theme shown.
};
disableDefault404: false, // Allows the user to disable the default `/404 page, to be able to create their own under `/src/pages/404.astro`.
enableRSSFeed: true, // Allows the user to Enable or disable RSS Feed Generation. Default: true
enableOGImages: true, // Allows the user to Enable or disable OG Image Generation. Default: true
verbose: false, // Show the full Log output from All parts of Astro-GhostCMS
Integrations: {
// This allows user config passthrough from Astro-GhostCMS to the Included Integrations
robotsTxt: {
// OPTIONAL
// ADVANCED USAGE - https://www.npmjs.com/package/astro-robots-txt#configuration
},
sitemap: {
// OPTIONAL
// ADVANCED USAGE - https://docs.astro.build/en/guides/integrations-guide/sitemap
},
},
})
],
});
```
@ -113,4 +137,3 @@ For more information and to see the docs please check our website: [Astro-GhostC
# Foot Notes & Credits
[^1]: Ghost.org, Ghost.io, Ghost are all trademarks of [The Ghost Foundation](https://ghost.org/). This project is Open Source and not directly related to or provided by The Ghost Foundation and is intended to help create a easier method to utilize their provided JavaScript tools to link a Headless GhostCMS install in to your Astro project.

View File

@ -1,359 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { AstroIntegration } from "astro";
import type { SafeParseError, SafeParseSuccess } from "astro/zod";
import fse from "fs-extra";
import { loadEnv } from "vite";
import ghostRobots from "./src/integrations/robots-txt";
import ghostSitemap from "./src/integrations/sitemap";
import { UserConfigSchema } from "./src/schemas";
import { addVirtualImport } from "./src/utils/add-virtual-import";
import latestVersion from "./src/utils/latestVersion.js";
import { fromZodError } from "./src/utils/zod-validation/fromZodError.js";
import type { UserConfig } from "./types";
/** INTERNAL CONSTANTS */
const IC = {
/** INTERNAL PACKAGE NAME */
PKG: "@matthiesenxyz/astro-ghostcms",
/** INTERNAL PACKAGE NAME (THEME) */
DT: "@matthiesenxyz/astro-ghostcms-theme-default",
/** INTERNAL STRING */
CHECK_ENV: "Checking for Environment Variables...",
/** SET ENV GRABBER MODE */
MODE: "all",
/** SET ENV GRABBER PREFIXES */
PREFIXES: "CONTENT_API",
/** INTERNAL STRING */
KEY_MISSING: "CONTENT_API_KEY Missing from .env/environment variables",
/** INTERNAL STRING */
URL_MISSING:
"CONTENT_API_URL Missing from .env/environment variables or ghostURL under the integration settings in `astro.config.mjs`",
/** INTERNAL STRING */
IT: "Injecting Theme: ",
/** INTERNAL STRING */
IDR: "Injecting Default Routes...",
/** INTERNAL STRING */
ITR: "Injecting Default Theme Routes...",
/** INTERNAL STRING */
IRD: "Route Injection Disabled - Skipping...",
/** INTERNAL STRING */
IIR: "Injecting Integration Route: ",
/** INTERNAL STRING */
II: "Injecting Integration: ",
/** INTERNAL STRING */
AIbU: "Already Imported by User: ",
/** INTERNAL STRING */
CF: "Checking for ",
/** INTERNAL STRING */
CONFSETUPDONE: "Step Complete",
/** INTERNAL STRING */
F0FR: "Inject `/404` Route",
/** INTERNAL STRING */
RSS: "Injecting `/rss.xml` Route and `@astrojs/rss` Integration",
/** INTERNAL STRING */
NOURL:
"No Ghost URL defined in User Config: Falling back to environment variables.",
/** INTERNAL STRING */
id404: "404 Injection Disabled",
/** INTERNAL STRING */
idRSS: "RSS Injection Disabled",
/** INTERNAL STRING */
satori_e: "Injecting Satori-OpenGraph Generator",
/** INTERNAL STRING */
satori_d: "Satori Injection disabled",
};
/** CONTENT API ENVIRONMENT VARIABLES */
const ENV = loadEnv(IC.MODE, process.cwd(), IC.PREFIXES);
/** Astro-GhostCMS Integration
* @ For more information and to see the docs check
* @see https://astro-ghostcms.xyz
*/
export default function GhostCMS(options: UserConfig): AstroIntegration {
return {
name: "astro-ghostcms",
hooks: {
"astro:config:setup": async ({
injectRoute,
config,
updateConfig,
logger,
}) => {
// DEFINE LOGGERS
const logConfigCheck = logger.fork("astro-ghostcms/config:check");
const logConfigSetup = logger.fork("astro-ghostcms/config:setup");
// CHECK USER CONFIG AND MAKE AVAILBLE TO INTEGRATIONS
logConfigCheck.info("Checking Config...");
const GhostUserConfig = UserConfigSchema.safeParse(
options || {},
) as SafeParseSuccess<UserConfig>;
if (!GhostUserConfig.success) {
const validationError = fromZodError(
(GhostUserConfig as unknown as SafeParseError<UserConfig>).error,
);
logConfigCheck.error(`Config Error - ${validationError}`);
throw Error("");
}
const GhostConfig = GhostUserConfig.data;
const GCD = {
theme: GhostConfig.theme,
dRI: GhostConfig.disableRouteInjection,
dCO: GhostConfig.disableConsoleOutput,
SM: GhostConfig.sitemap,
RTXT: GhostConfig.robotstxt,
gSite: GhostConfig.ghostURL,
dRSS: GhostConfig.disableRSS,
d404: GhostConfig.disable404,
dOG: GhostConfig.disableSatoriOG,
};
// Check For ENV Variables
if (!GCD.dCO) {
logConfigCheck.info(IC.CHECK_ENV);
}
if (ENV.CONTENT_API_KEY === undefined) {
logConfigCheck.error(IC.KEY_MISSING);
throw IC.KEY_MISSING;
}
if (GCD.gSite === undefined) {
logConfigCheck.warn(IC.NOURL);
if (ENV.CONTENT_API_URL === undefined) {
logConfigCheck.error(IC.URL_MISSING);
throw IC.URL_MISSING;
}
}
if (!GCD.dRI) {
// THEME SELECTOR
if (GCD.theme === IC.DT) {
if (!GCD.dCO) {
logConfigCheck.info(IC.IT + IC.DT);
}
} else {
if (!GCD.dCO) {
logConfigCheck.info(IC.IT + GCD.theme);
}
}
// INJECT ROUTES
//// DEFAULT PROGRAM ROUTES
if (!GCD.dCO) {
logConfigSetup.info(IC.IDR);
}
if (!GCD.d404) {
if (!GCD.dCO) {
logConfigSetup.info(IC.F0FR);
}
injectRoute({
pattern: "/404",
entrypoint: `${IC.PKG}/404.astro`,
});
} else {
if (!GCD.dCO) {
logConfigSetup.info(IC.id404);
}
}
if (!GCD.dRSS) {
if (!GCD.dCO) {
logConfigSetup.info(IC.RSS);
}
injectRoute({
pattern: "/rss-style.xsl",
entrypoint: `${IC.PKG}/rss-style.xsl.ts`,
});
injectRoute({
pattern: "/rss.xml",
entrypoint: `${IC.PKG}/rss.xml.ts`,
});
} else {
if (!GCD.dCO) {
logConfigSetup.info(IC.idRSS);
}
}
if (!GCD.dOG) {
if (!GCD.dCO) {
logConfigSetup.info(IC.satori_e);
}
injectRoute({
pattern: "/open-graph/[slug].png",
entrypoint: `${IC.PKG}/open-graph/[slug].png.ts`,
});
injectRoute({
pattern: "/open-graph/index.png",
entrypoint: `${IC.PKG}/open-graph/index.png.ts`,
});
injectRoute({
pattern: "/open-graph/authors.png",
entrypoint: `${IC.PKG}/open-graph/authors.png.ts`,
});
injectRoute({
pattern: "/open-graph/author/[slug].png",
entrypoint: `${IC.PKG}/open-graph/author/[slug].png.ts`,
});
injectRoute({
pattern: "/open-graph/tags.png",
entrypoint: `${IC.PKG}/open-graph/tags.png.ts`,
});
injectRoute({
pattern: "/open-graph/tag/[slug].png",
entrypoint: `${IC.PKG}/open-graph/tag/[slug].png.ts`,
});
} else {
if (!GCD.dCO) {
logConfigSetup.info(IC.satori_d);
}
}
// THEME ROUTES
if (!GCD.dCO) {
logConfigSetup.info(IC.ITR);
}
injectRoute({
pattern: "/",
entrypoint: `${GCD.theme}/index.astro`,
});
injectRoute({
pattern: "/[slug]",
entrypoint: `${GCD.theme}/[slug].astro`,
});
injectRoute({
pattern: "/tags",
entrypoint: `${GCD.theme}/tags.astro`,
});
injectRoute({
pattern: "/authors",
entrypoint: `${GCD.theme}/authors.astro`,
});
injectRoute({
pattern: "/tag/[slug]",
entrypoint: `${GCD.theme}/tag/[slug].astro`,
});
injectRoute({
pattern: "/author/[slug]",
entrypoint: `${GCD.theme}/author/[slug].astro`,
});
injectRoute({
pattern: "/archives/[...page]",
entrypoint: `${GCD.theme}/archives/[...page].astro`,
});
} else {
if (!GCD.dCO) {
logConfigSetup.info(IC.IRD);
}
}
// IMPORT INTEGRATIONS & INTEGRATION ROUTES
const integrations = [...config.integrations];
// IMPORT INTEGRATION: @ASTROJS/SITEMAP
if (!GCD.dCO) {
logConfigSetup.info(`${IC.CF}@astrojs/sitemap`);
}
if (!integrations.find(({ name }) => name === "@astrojs/sitemap")) {
if (!GCD.dCO) {
logConfigSetup.info(`${IC.II}@astrojs/sitemap`);
}
integrations.push(ghostSitemap(GCD.SM));
} else {
if (!GCD.dCO) {
logConfigSetup.info(`${IC.AIbU}@astrojs/sitemap`);
}
}
// IMPORT INTEGRATION: ASTRO-ROBOTS-TXT
if (!GCD.dCO) {
logConfigSetup.info(`${IC.CF}astro-robots-txt`);
}
if (!integrations.find(({ name }) => name === "astro-robots-txt")) {
if (!GCD.dCO) {
logConfigSetup.info(`${IC.II}astro-robots-txt`);
}
integrations.push(ghostRobots(GCD.RTXT));
} else {
if (!GCD.dCO) {
logConfigSetup.info(`${IC.AIbU}astro-robots-txt`);
}
}
// FINAL STEP TO KEEP INTEGRATION LIVE
try {
updateConfig({
// UPDATE ASTRO CONFIG WITH INTEGRATED INTEGRATIONS
integrations: [ghostSitemap(GCD.SM), ghostRobots(GCD.RTXT)],
vite: {
optimizeDeps: { exclude: ["@resvg/resvg-js"] },
},
});
} catch (e) {
logConfigSetup.error(e as string);
throw e;
}
addVirtualImport({
name: "virtual:@matthiesenxyz/astro-ghostcms/config",
content: `export default ${JSON.stringify(GhostUserConfig.data)}`,
updateConfig,
});
},
"astro:config:done": async ({ logger }) => {
// Config Done
const logConfigDone = logger.fork("astro-ghostcms/config:done");
const pJSON = await fse.readJson(
path.resolve(fileURLToPath(import.meta.url), "..", "package.json"),
);
const pkgVer = pJSON.version;
logConfigDone.info(`Config Done. Current Version: v${pkgVer}`);
},
"astro:server:setup": async ({ logger }) => {
// Dev Server Start
const logServerSetup = logger.fork("astro-ghostcms/server:setup");
const logCurrentVersion = logger.fork("astro-ghostcms/current-version");
const logNpmVersion = logger.fork("astro-ghostcms/npm-pub-version");
const logCheck = logger.fork("astro-ghostcms/check");
const pJSON = await fse.readJson(
path.resolve(fileURLToPath(import.meta.url), "..", "package.json"),
);
const pkgVer = pJSON.version;
const npmVER = await latestVersion(IC.PKG);
if (pkgVer !== npmVER) {
logCurrentVersion.warn(`Current Installed Version is v${pkgVer}`);
logNpmVersion.warn(`Latest Published Version is v${npmVER}`);
logCheck.warn("Please consider updating.");
}
logServerSetup.info(
"Setting up Astro-GhostCMS server for Development!",
);
},
"astro:server:start": async ({ logger }) => {
// Server Start
const logServerStart = logger.fork("astro-ghostcms/server:start");
logServerStart.info("Astro-GhostCMS Integration Ready!");
},
"astro:build:done": async ({ logger }) => {
// Build Done
const logBuildDone = logger.fork("astro-ghostcms/build:done");
const logCurrentVersion = logger.fork("astro-ghostcms/current-version");
const logNpmVersion = logger.fork("astro-ghostcms/npm-pub-version");
const logCheck = logger.fork("astro-ghostcms/check");
const pJSON = await fse.readJson(
path.resolve(fileURLToPath(import.meta.url), "..", "package.json"),
);
const pkgVer = pJSON.version;
const npmVER = await latestVersion(IC.PKG);
if (pkgVer !== npmVER) {
logCurrentVersion.warn(`Current Installed Version is v${pkgVer}`);
logNpmVersion.warn(`Latest Published Version is v${npmVER}`);
logCheck.warn("Please consider updating.");
}
logBuildDone.info(
`Build Complete, Integration Now ready for Production. Astro-GhostCMS v${pkgVer}`,
);
},
},
};
}

View File

@ -1,111 +1,85 @@
{
"name": "@matthiesenxyz/astro-ghostcms",
"description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content",
"version": "3.2.9",
"homepage": "https://astro-ghostcms.xyz/",
"type": "module",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"author": {
"email": "adam@matthiesen.xyz",
"name": "Adam Matthiesen - MatthiesenXYZ",
"url": "https://matthiesen.xyz"
},
"keywords": [
"astro-component",
"astro-integration",
"withastro",
"astro",
"blog",
"content",
"integration",
"ghost",
"ghostcms",
"ghostcms-theme",
"ghost-theme",
"astro-theme"
],
"repository": {
"type": "git",
"url": "git+https://github.com/MatthiesenXYZ/astro-ghostcms.git"
},
"bugs": {
"url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues",
"email": "issues@astro-ghostcms.xyz"
},
"main": "index.ts",
"types": "types.d.ts",
"files": [
"src",
".env.demo",
"index.ts",
"tsconfig.json",
"types.d.ts"
],
"exports": {
".": "./index.ts",
"./api": {
"types": "./src/api/index.ts",
"default": "./src/api/index.ts"
"name": "@matthiesenxyz/astro-ghostcms",
"description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content",
"version": "3.2.9",
"homepage": "https://astro-ghostcms.xyz/",
"type": "module",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"./api-core": "./src/api/content-api/index.ts",
"./config": {
"types": "./src/integrations/virtual.d.ts",
"default": "./src/integrations/virtual-config.ts"
"sideEffects": false,
"author": {
"email": "adam@matthiesen.xyz",
"name": "Adam Matthiesen - MatthiesenXYZ",
"url": "https://matthiesen.xyz"
},
"./satoriOG": "./src/integrations/satori.ts",
"./404.astro": "./src/default-routes/404/404.astro",
"./rss.xml.ts": "./src/default-routes/rss.xml.ts",
"./rss-style.xsl.ts": "./src/default-routes/rss-style.xsl.ts",
"./open-graph/index.png.ts": "./src/default-routes/open-graph/index.png.ts",
"./open-graph/authors.png.ts": "./src/default-routes/open-graph/authors.png.ts",
"./open-graph/tags.png.ts": "./src/default-routes/open-graph/tags.png.ts",
"./open-graph/[slug].png.ts": "./src/default-routes/open-graph/[slug].png.ts",
"./open-graph/author/[slug].png.ts": "./src/default-routes/open-graph/author/[slug].png.ts",
"./open-graph/tag/[slug].png.ts": "./src/default-routes/open-graph/tag/[slug].png.ts"
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'"
},
"devDependencies": {
"@astrojs/check": "^0.5.4",
"@ts-ghost/core-api": "*",
"@ts-ghost/tsconfig": "*",
"@matthiesenxyz/astro-ghostcms-theme-default": "*",
"@matthiesenxyz/astro-ghostcms-catppuccin": "*",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.11.19",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^0.31.4",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"typescript": "^5.3.3",
"vitest": "^1.3.0",
"vitest-fetch-mock": "^0.2.2"
},
"dependencies": {
"@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.13",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.0.5",
"@resvg/resvg-js": "^2.6.0",
"@ts-ghost/core-api": "^5.1.2",
"astro": "^4.4.0",
"astro-robots-txt": "^1.0.0",
"fs-extra": "^11.1.0",
"package-json": "9.0.0",
"satori": "^0.10.11",
"satori-html": "^0.3.2",
"vite": "^5.1.3",
"vite-tsconfig-paths": "^4.2.2",
"zod": "^3.22.4"
"keywords": [
"astro-component",
"astro-integration",
"withastro",
"astro",
"blog",
"content",
"integration",
"ghost",
"ghostcms",
"ghostcms-theme",
"ghost-theme",
"astro-theme"
],
"repository": {
"type": "git",
"url": "git+https://github.com/MatthiesenXYZ/astro-ghostcms.git"
},
"bugs": {
"url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues",
"email": "issues@astro-ghostcms.xyz"
},
"files": [
"src",
"CHANGELOG.md",
"LICENSE",
"README.md"
],
"exports": {
".": "./src/index.ts",
"./api": "./src/api/index.ts",
"./satoriOG": "./src/integrations/satoriog/satori.ts"
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'"
},
"enginesStrict": {
"node": ">=18.19.0"
},
"peerDependencies": {
"astro": ">=4.4.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.11.24",
"vitest": "^1.3.1",
"vitest-fetch-mock": "^0.2.2"
},
"dependencies": {
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.1",
"@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.13",
"@resvg/resvg-js": "^2.6.0",
"@ts-ghost/core-api": "^6.0.0",
"@ts-ghost/content-api": "^4.0.12",
"astro-integration-kit": "^0.5.1",
"astro-robots-txt": "^1.0.0",
"fs-extra": "^11.2.0",
"package-json": "^10.0.0",
"picocolors": "^1.0.0",
"satori": "^0.10.13",
"satori-html": "^0.3.2",
"vite": "^5.1.4"
}
}
}

View File

@ -1,116 +0,0 @@
import {
APIComposer,
BasicFetcher,
HTTPClient,
contentAPICredentialsSchema,
slugOrIdSchema,
} from "@ts-ghost/core-api";
import {
authorsIncludeSchema,
authorsSchema,
pagesIncludeSchema,
pagesSchema,
postsIncludeSchema,
postsSchema,
settingsSchema,
tagsIncludeSchema,
tagsSchema,
tiersIncludeSchema,
tiersSchema,
} from "./schemas";
export type { ContentAPICredentials, APIVersions } from "@ts-ghost/core-api";
export enum BrowseEndpointType {
authors = "authors",
tiers = "tiers",
posts = "posts",
pages = "pages",
tags = "tags",
settings = "settings",
}
export default class TS_API<Version extends `v5.${string}` = any> {
private httpClient: HTTPClient;
constructor(
protected readonly url: string,
protected readonly key: string,
protected readonly version: Version,
) {
const apiCredentials = contentAPICredentialsSchema.parse({
key,
version,
url,
});
this.httpClient = new HTTPClient({
...apiCredentials,
endpoint: "content",
});
}
get authors() {
return new APIComposer(
"authors",
{
schema: authorsSchema,
identitySchema: slugOrIdSchema,
include: authorsIncludeSchema,
},
this.httpClient,
).access(["read", "browse"]);
}
get tiers() {
return new APIComposer(
"tiers",
{
schema: tiersSchema,
identitySchema: slugOrIdSchema,
include: tiersIncludeSchema,
},
this.httpClient,
).access(["browse", "read"]);
}
get posts() {
return new APIComposer(
"posts",
{
schema: postsSchema,
identitySchema: slugOrIdSchema,
include: postsIncludeSchema,
},
this.httpClient,
).access(["browse", "read"]);
}
get pages() {
return new APIComposer(
"pages",
{
schema: pagesSchema,
identitySchema: slugOrIdSchema,
include: pagesIncludeSchema,
},
this.httpClient,
).access(["browse", "read"]);
}
get tags() {
return new APIComposer(
"tags",
{
schema: tagsSchema,
identitySchema: slugOrIdSchema,
include: tagsIncludeSchema,
},
this.httpClient,
).access(["browse", "read"]);
}
get settings() {
return new BasicFetcher(
"settings",
{ output: settingsSchema },
this.httpClient,
);
}
}

View File

@ -1,8 +0,0 @@
export { default as TS_API } from "./content-api";
export * from "./schemas";
export type {
InferFetcherDataShape,
InferResponseDataShape,
BrowseParams,
} from "@ts-ghost/core-api";

View File

@ -1 +0,0 @@
export * from "./authors";

View File

@ -1 +0,0 @@
export * from "./socials";

View File

@ -1 +0,0 @@
export * from "./pages";

View File

@ -1 +0,0 @@
export * from "./posts";

View File

@ -1 +0,0 @@
export * from "./settings";

View File

@ -1 +0,0 @@
export * from "./tags";

View File

@ -1 +0,0 @@
export * from "./tiers";

View File

@ -1,11 +1,15 @@
import { assert, beforeEach, describe, expect, test } from "vitest";
import TS_API from "./content-api";
import { TSGhostContentAPI } from "@ts-ghost/content-api";
describe("content-api", () => {
let api: TS_API;
let api: TSGhostContentAPI;
beforeEach(() => {
api = new TS_API("https://ghost.org", "59d4bf56c73c04a18c867dc3ba", "v5.0");
api = new TSGhostContentAPI(
"https://ghost.org",
"59d4bf56c73c04a18c867dc3ba",
"v5.0",
);
});
test("content-api", () => {
@ -14,21 +18,25 @@ describe("content-api", () => {
test("content-api shouldn't instantiate with an incorrect url", () => {
assert.throws(() => {
const api = new TS_API("ghost.org", "59d4bf56c73c04a18c867dc3ba", "v5.0");
const api = new TSGhostContentAPI(
"ghost.org",
"59d4bf56c73c04a18c867dc3ba",
"v5.0",
);
api.settings;
});
});
test("content-api shouldn't instantiate with an incorrect key", () => {
assert.throws(() => {
const api = new TS_API("https://ghost.org", "a", "v5.0");
const api = new TSGhostContentAPI("https://ghost.org", "a", "v5.0");
api.settings;
});
});
test("content-api shouldn't instantiate with an incorrect version", () => {
assert.throws(() => {
const api = new TS_API(
const api = new TSGhostContentAPI(
"https://ghost.org",
"1efedd9db174adee2d23d982:4b74dca0219bad629852191af326a45037346c2231240e0f7aec1f9371cc14e8",
// @ts-expect-error

View File

@ -1,23 +1,24 @@
import { TS_API } from "./content-api";
import type { Page, Post } from "./content-api/schemas";
import { TSGhostContentAPI } from "@ts-ghost/content-api";
import type { Page, Post } from "../schemas/api";
// LOAD ENVIRONMENT VARIABLES
import { loadEnv } from "vite";
import config from "../integrations/virtual-config";
const CONF_URL = config.ghostURL;
const { CONTENT_API_KEY, CONTENT_API_URL } = loadEnv(
"all",
process.cwd(),
"CONTENT_",
);
const ghostApiKey = CONTENT_API_KEY;
const ghostUrl = CONF_URL ? CONF_URL : CONTENT_API_URL;
// LOAD CONFIG
import config from "virtual:@matthiesenxyz/astro-ghostcms/config";
const CONF_URL = config.ghostURL;
// SETUP GHOST API
const ghostApiKey = CONTENT_API_KEY || "";
const ghostUrl = CONF_URL || CONTENT_API_URL || "";
const version = "v5.0";
const api = new TS_API(ghostUrl, ghostApiKey, version);
const api = new TSGhostContentAPI(ghostUrl, ghostApiKey, version);
export const getAllAuthors = async () => {
const results = await api.authors

View File

@ -1,3 +1,3 @@
export * from "./api-functions";
export * from "./content-api/schemas";
export * from "./ghostAPI";
export * from "./invariant";
export * from "../schemas/api/index";

View File

@ -20,7 +20,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const tinyinvariant = "merged";
const isProduction: boolean = process.env.NODE_ENV === "production";
const prefix: string = "Invariant failed";

View File

@ -0,0 +1,303 @@
import fse from "fs-extra";
import { createResolver, defineIntegration } from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { AstroError } from "astro/errors";
import type { AstroIntegration } from "astro";
import c from "picocolors";
import { loadEnv } from "vite";
import sitemap from "@astrojs/sitemap";
import robotsTxt from "astro-robots-txt";
// Internal Imports
import { GhostUserConfigSchema } from "./schemas/userconfig";
import ghostRSS from "./integrations/rssfeed";
import ghostOGImages from "./integrations/satoriog";
import ghostThemeProvider from "./integrations/themeprovider";
import latestVersion from "./utils/latestVersion";
// Load environment variables
const ENV = loadEnv("all", process.cwd(), "CONTENT_API");
/** Astro-GhostCMS Integration
* @description This integration allows you to use GhostCMS as a headless CMS for your Astro project
* @see https://astro-ghostcms.xyz for the most up-to-date documentation!
*/
export default defineIntegration({
name: "@matthiesenxyz/astro-ghostcms",
optionsSchema: GhostUserConfigSchema,
plugins: [...corePlugins],
setup({ options, name }) {
const { resolve } = createResolver(import.meta.url);
return {
"astro:config:setup": ({
watchIntegration,
hasIntegration,
addIntegration,
addVirtualImports,
injectRoute,
logger,
}) => {
// Set up verbose logging
const verbose = options.verbose;
// Configure Loggers
const GhostLogger = logger.fork(c.bold(c.blue("👻 Astro-GhostCMS")));
const loggerTagged = (message: string) => {
return logger.fork(`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(message)}`)
}
// Configure ENV Logger
const GhostENVLogger = loggerTagged("ENV Check");
// Configure Integration Loggers & verbose logging
const GhostIntegrationLogger = loggerTagged("Integrations");
// Configure Route Logger & verbose logging
const GhostRouteLogger = loggerTagged("Router");
// Log Info Helper
const intLogInfo = (message:string) => {
if (verbose) {
GhostIntegrationLogger.info(message);
}
};
// Log Route Info Helper
const routeLogInfo = (message:string) => {
if (verbose) {
GhostRouteLogger.info(message);
}
};
// Local Integration Helper
const localIntegration = (enabled: boolean, name: string, integration: AstroIntegration) => {
if (enabled) {
addIntegration(integration);
} else {
intLogInfo(c.gray(`${name} integration is disabled`));
}
}
// Check External Integration Helper
const checkIntegration = (name: string, integration: AstroIntegration) => {
if (!hasIntegration(name)) {
intLogInfo(c.bold(c.magenta(`Adding ${c.blue(name)} integration`)));
addIntegration(integration);
} else {
intLogInfo(c.gray(`${name} integration already exists, skipping...`));
}
}
// Inject Route Helper
const routeHelper = (routename: string, enabled: boolean, pattern: string, entrypoint: string) => {
if (enabled) {
routeLogInfo(c.bold(c.cyan(`Setting up ${routename} route`)));
injectRoute({
pattern: pattern,
entrypoint: resolve(`./routes${entrypoint}`),
prerender: true,
});
} else {
routeLogInfo(c.gray(`${routename} route is disabled, Skipping...`));
}
}
// Setup Watch Integration for Hot Reload during DEV
watchIntegration(resolve());
GhostLogger.info("Initializing @matthiesenxyz/astro-ghostcms...");
// Check for GhostCMS environment variables
GhostENVLogger.info(
c.bold(
c.yellow(
"Checking for GhostCMS environment variables & user configuration",
),
),
);
// Check for GhostCMS API Key
if (ENV.CONTENT_API_KEY === undefined) {
GhostENVLogger.error(
c.bgRed(
c.bold(
c.white("CONTENT_API_KEY is not set in environment variables"),
),
),
);
throw new AstroError(
`${name} CONTENT_API_KEY is not set in environment variables`,
);
}
// Check for GhostCMS URL
if (options.ghostURL === undefined) {
GhostENVLogger.warn(
c.bgYellow(
c.bold(
c.black(
"ghostURL is not set in user configuration falling back to environment variable",
),
),
),
);
if (ENV.CONTENT_API_URL === undefined) {
GhostENVLogger.error(
c.bgRed(
c.bold(
c.white(
"CONTENT_API_URL is not set in environment variables",
),
),
),
);
throw new AstroError(
`${name} CONTENT_API_URL is not set in environment variables`,
);
}
}
GhostENVLogger.info(
c.bold(c.green("GhostCMS environment variables are set")),
);
// Set up Astro-GhostCMS Integrations
GhostIntegrationLogger.info(
c.bold(c.magenta("Configuring Enabled Integrations")),
);
// Setup GhostCMS Theme Provider
localIntegration(
!options.ThemeProvider.disableThemeProvider,
"Theme Provider",
ghostThemeProvider({
theme: options.ThemeProvider.theme,
verbose,
})
);
// Setup GhostCMS OG Image Provider
localIntegration(
options.enableOGImages,
"Satori OG Images",
ghostOGImages({ verbose })
);
// Setup GhostCMS RSS Feed Provider
localIntegration(
options.enableRSSFeed,
"RSS Feed",
ghostRSS({ verbose })
);
// Setup @astrojs/sitemap Integration
checkIntegration(
"@astrojs/sitemap",
sitemap(options.Integrations.sitemap)
);
// Setup astro-robots-txt Integration
checkIntegration(
"astro-robots-txt",
robotsTxt(options.Integrations.robotsTxt)
);
// Setup Default 404 Page
routeHelper(
"Default 404 Page",
!options.disableDefault404,
"/404",
"/404.astro"
);
// Add virtual imports for user configuration
addVirtualImports({
"virtual:@matthiesenxyz/astro-ghostcms/config": `export default ${JSON.stringify(
options,
)}`,
});
},
"astro:config:done": ({ logger }) => {
// Configure Loggers
const GhostLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.green(
"CONFIG",
)}`,
);
// Log Configuration Complete
GhostLogger.info(
c.bold(c.green("Integration Setup & Configuration Complete")),
);
},
"astro:server:start": async ({ logger }) => {
const loggerTagged = (message: string) => {
return logger.fork(`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.green(message)}`)
}
// Configure Loggers
const GhostLogger = loggerTagged("DEV");
const GhostUpdateLogger = loggerTagged("VERSION CHECK");
// Start the DEV server
GhostLogger.info(
c.bold(c.magenta("Running Astro-GhostCMS in Deveopment mode 🚀")),
);
// Check for updates
// Get the latest version of Astro-GhostCMS
const currentNPMVersion = await latestVersion(
"@matthiesenxyz/astro-ghostcms",
);
// Get the local version of Astro-GhostCMS
const packageJson = await fse.readJson(resolve("../package.json"));
const localVersion = packageJson.version;
// Log the version check
if (currentNPMVersion !== localVersion) {
GhostUpdateLogger.warn(
`\n${c.bgYellow(
c.bold(
c.black(
" There is a new version of Astro-GhostCMS available! ",
),
),
)}\n${
c.bold(c.white(" Current Installed Version: ")) +
c.bold(c.red(`${localVersion} `))
} \n ${c.bold(c.white("New Available Version: "))} ${c.green(
currentNPMVersion,
)} \n ${c.bold(
c.white(
"Please consider updating to the latest version by running: ",
),
)} ${c.bold(
c.green("npm i @matthiesenxyz/astro-ghostcms@latest"),
)} \n`,
);
} else {
GhostUpdateLogger.info(
c.bold(c.green(`Astro-GhostCMS is up to date! v${localVersion}`)),
);
}
},
"astro:build:done": ({ logger }) => {
// Configure Loggers
const GhostLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.bold(
c.green("BUILD"),
)}`,
);
// Log Build Complete
GhostLogger.info(
c.bold(c.magenta("Running Astro-GhostCMS in Production mode 🚀")),
);
},
};
},
});

View File

@ -0,0 +1,3 @@
import astroGhostCMS from "./astro-ghostcms";
export default astroGhostCMS;

View File

@ -1,28 +0,0 @@
import robotsTxt, { type RobotsTxtOptions } from "astro-robots-txt";
import type { UserConfig } from "../schemas";
export function getRobotsTxtConfig(
opts: UserConfig["robotstxt"],
): RobotsTxtOptions {
const robotsConfig: RobotsTxtOptions = {};
if (opts?.host) {
robotsConfig.host = opts.host;
}
if (opts?.policy) {
robotsConfig.policy = opts.policy;
}
if (opts?.sitemap) {
robotsConfig.sitemap = opts.sitemap;
}
if (opts?.sitemapBaseFileName) {
robotsConfig.sitemapBaseFileName = opts.sitemapBaseFileName;
}
return robotsConfig;
}
/**
* A wrapped version of the `astro-robots-txt` integration for GhostCMS.
*/
export default function ghostRobots(opts: UserConfig["robotstxt"]) {
return robotsTxt(getRobotsTxtConfig(opts));
}

View File

@ -0,0 +1,52 @@
import { createResolver, defineIntegration } from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { z } from "astro/zod";
import c from "picocolors";
export default defineIntegration({
name: "@matthiesenxyz/astro-ghostcms-rss",
optionsSchema: z.object({
verbose: z.coerce.boolean().optional(),
}),
plugins: [...corePlugins],
setup({ options }) {
const { resolve } = createResolver(import.meta.url);
return {
"astro:config:setup": ({ watchIntegration, injectRoute, logger }) => {
watchIntegration(resolve());
const RSSLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"RSSGenerator",
)}`,
);
RSSLogger.info(c.bold(c.magenta("RSS Feed Enabled. Setting up...")));
injectRoute({
pattern: "/rss-style.xsl",
entrypoint: resolve("./routes/rss-style.xsl.ts"),
});
injectRoute({
pattern: "/rss.xml",
entrypoint: resolve("./routes/rss.xml.ts"),
});
},
"astro:config:done": ({ logger }) => {
const RSSLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"RSSGenerator",
)}`,
);
const verboseLogsInfo = (message:string) => {
if (options.verbose) {
RSSLogger.info(message);
}
};
verboseLogsInfo(c.bold(c.green("RSS Feed Setup Complete")));
},
};
},
});

View File

@ -0,0 +1,3 @@
import astroGhostcmsRSS from "./astro-ghostcms-rss";
export default astroGhostcmsRSS;

View File

@ -1,6 +1,6 @@
import rss from "@astrojs/rss";
import type { APIContext } from "astro";
import { getAllPosts, getSettings, invariant } from "../api";
import { getAllPosts, getSettings, invariant } from "../../../api";
const posts = await getAllPosts();
const settings = await getSettings();

View File

@ -0,0 +1,84 @@
import { createResolver, defineIntegration } from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { z } from "astro/zod";
import c from "picocolors";
export default defineIntegration({
name: "@matthiesenxyz/astro-ghostcms-satoriog",
optionsSchema: z.object({
verbose: z.coerce.boolean().optional(),
}),
plugins: [...corePlugins],
setup({ options }) {
const { resolve } = createResolver(import.meta.url);
return {
"astro:config:setup": ({
watchIntegration,
updateConfig,
injectRoute,
logger,
}) => {
watchIntegration(resolve());
const SatoriLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"SatoriOG",
)}`,
);
SatoriLogger.info(
c.bold(c.magenta("OG Image Integration Enabled. Setting up...")),
);
injectRoute({
pattern: "/open-graph/[slug].png",
entrypoint: resolve("./routes/[slug].png.ts"),
});
injectRoute({
pattern: "/open-graph/index.png",
entrypoint: resolve("./routes/index.png.ts"),
});
injectRoute({
pattern: "/open-graph/authors.png",
entrypoint: resolve("./routes/authors.png.ts"),
});
injectRoute({
pattern: "/open-graph/author/[slug].png",
entrypoint: resolve("./routes/author/[slug].png.ts"),
});
injectRoute({
pattern: "/open-graph/tags.png",
entrypoint: resolve("./routes/tags.png.ts"),
});
injectRoute({
pattern: "/open-graph/tag/[slug].png",
entrypoint: resolve("./routes/tag/[slug].png.ts"),
});
updateConfig({
vite: { optimizeDeps: { exclude: ["@resvg/resvg-js"] } },
});
},
"astro:config:done": ({ logger }) => {
const SatoriLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"SatoriOG",
)}`,
);
const verboseLogsInfo = (message:string) => {
if (options.verbose) {
SatoriLogger.info(message);
}
};
verboseLogsInfo(c.bold(c.green("OG Image Integration Setup Complete")));
},
};
},
});

View File

@ -0,0 +1,3 @@
import astroGhostcmsSatoriog from "./astro-ghostcms-satoriog";
export default astroGhostcmsSatoriog;

View File

@ -5,13 +5,8 @@ import type {
InferGetStaticPropsType,
} from "astro";
import { html } from "satori-html";
import {
getAllPages,
getAllPosts,
getSettings,
invariant,
} from "../../api/index.js";
import satoriOG from "../../integrations/satori.js";
import { getAllPages, getAllPosts, getSettings, invariant } from "../../../api";
import satoriOG from "../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -10,8 +10,8 @@ import {
getAllPosts,
getSettings,
invariant,
} from "../../../api/index.js";
import satoriOG from "../../../integrations/satori.js";
} from "../../../../api";
import satoriOG from "../../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -5,13 +5,8 @@ import type {
InferGetStaticPropsType,
} from "astro";
import { html } from "satori-html";
import {
getAllPages,
getAllPosts,
getSettings,
invariant,
} from "../../api/index.js";
import satoriOG from "../../integrations/satori.js";
import { getAllPages, getAllPosts, getSettings, invariant } from "../../../api";
import satoriOG from "../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -5,13 +5,8 @@ import type {
InferGetStaticPropsType,
} from "astro";
import { html } from "satori-html";
import {
getAllPages,
getAllPosts,
getSettings,
invariant,
} from "../../api/index.js";
import satoriOG from "../../integrations/satori.js";
import { getAllPages, getAllPosts, getSettings, invariant } from "../../../api";
import satoriOG from "../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -10,8 +10,8 @@ import {
getAllTags,
getSettings,
invariant,
} from "../../../api/index.js";
import satoriOG from "../../../integrations/satori.js";
} from "../../../../api";
import satoriOG from "../../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -5,13 +5,8 @@ import type {
InferGetStaticPropsType,
} from "astro";
import { html } from "satori-html";
import {
getAllPages,
getAllPosts,
getSettings,
invariant,
} from "../../api/index.js";
import satoriOG from "../../integrations/satori.js";
import { getAllPages, getAllPosts, getSettings, invariant } from "../../../api";
import satoriOG from "../satori";
export const getStaticPaths: GetStaticPaths = async () => {
const result: GetStaticPathsItem[] = [];

View File

@ -5,7 +5,7 @@ import type {
ToImageOptions,
ToResponseOptions,
ToSvgOptions,
} from "../../types.js";
} from "./types";
const satoriOG = ({ width, height, template }: SatoriAstroOGOptions) => {
return {

View File

@ -1,24 +1,5 @@
import type { Resvg } from "@resvg/resvg-js";
import type satori from "satori";
export type { UserConfig, GhostUserConfig } from "./src/schemas";
export type {
Author,
AuthorsIncludeSchema,
Page,
PagesIncludeSchema,
Post,
PostsIncludeSchema,
Settings,
Tag,
TagsIncludeSchema,
Tier,
TiersIncludeSchema,
} from "./src/api/index.ts";
export type { ContentAPICredentials, APIVersions } from "@ts-ghost/core-api";
type SatoriParameters = Parameters<typeof satori>;
type SatoriOptions = SatoriParameters[1];
type ResvgOptions = NonNullable<ConstructorParameters<typeof Resvg>[1]>;

View File

@ -1,38 +0,0 @@
import sitemap, { type SitemapOptions } from "@astrojs/sitemap";
import type { UserConfig } from "../schemas";
export function getSitemapConfig(opts: UserConfig["sitemap"]): SitemapOptions {
const sitemapConfig: SitemapOptions = {};
if (opts?.filter) {
sitemapConfig.filter = opts.filter;
}
if (opts?.changefreq) {
sitemapConfig.changefreq = opts.changefreq;
}
if (opts?.entryLimit) {
sitemapConfig.entryLimit = opts.entryLimit;
}
if (opts?.customPages) {
sitemapConfig.customPages = opts.customPages;
}
if (opts?.i18n) {
sitemapConfig.i18n = opts.i18n;
}
if (opts?.lastmod) {
sitemapConfig.lastmod = opts.lastmod;
}
if (opts?.priority) {
sitemapConfig.priority = opts.priority;
}
if (opts?.serialize) {
sitemapConfig.serialize = opts.serialize;
}
return sitemapConfig;
}
/**
* A wrapped version of the `@astrojs/sitemap` integration for GhostCMS.
*/
export default function ghostSitemap(opts: UserConfig["sitemap"]) {
return sitemap(getSitemapConfig(opts));
}

View File

@ -0,0 +1,99 @@
import { createResolver, defineIntegration } from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { z } from "astro/zod";
import c from "picocolors";
export default defineIntegration({
name: "@matthiesenxyz/astro-ghostcms-themeprovider",
optionsSchema: z.object({
theme: z.string(),
verbose: z.coerce.boolean().optional(),
}),
plugins: [...corePlugins],
setup({ options }) {
const { resolve } = createResolver(import.meta.url);
const DEFAULT_THEME = "@matthiesenxyz/astro-ghostcms-theme-default";
return {
"astro:config:setup": ({ watchIntegration, injectRoute, logger }) => {
watchIntegration(resolve());
const themeLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"Theme Provider",
)}`,
);
const verboseLogsInfo = (message:string) => {
if (options.verbose) {
themeLogger.info(message);
}
};
themeLogger.info(
c.bold(c.magenta("Theme Provider enabled. Setting up...")),
);
if (options.theme === DEFAULT_THEME) {
verboseLogsInfo(
c.blue("No theme is set, injecting default theme"),
);
} else {
verboseLogsInfo(`${c.bold(c.cyan("Injecting Theme:"))} ${c.bold(c.underline(c.magenta(options.theme)))}`);
}
injectRoute({
pattern: "/",
entrypoint: `${options.theme}/index.astro`,
});
injectRoute({
pattern: "/[slug]",
entrypoint: `${options.theme}/[slug].astro`,
});
injectRoute({
pattern: "/tags",
entrypoint: `${options.theme}/tags.astro`,
});
injectRoute({
pattern: "/authors",
entrypoint: `${options.theme}/authors.astro`,
});
injectRoute({
pattern: "/tag/[slug]",
entrypoint: `${options.theme}/tag/[slug].astro`,
});
injectRoute({
pattern: "/author/[slug]",
entrypoint: `${options.theme}/author/[slug].astro`,
});
injectRoute({
pattern: "/archives/[...page]",
entrypoint: `${options.theme}/archives/[...page].astro`,
});
},
"astro:config:done": ({ logger }) => {
const themeLogger = logger.fork(
`${c.bold(c.blue("👻 Astro-GhostCMS"))}${c.gray("/")}${c.blue(
"Theme Provider",
)}`,
);
const verboseLogsInfo = (message:string) => {
if (options.verbose) {
themeLogger.info(message);
}
};
verboseLogsInfo(c.bold(c.green("Provider Setup Complete")));
},
};
},
});

View File

@ -0,0 +1,3 @@
import astroGhostcmsThemeProvider from "./astro-ghostcms-themeprovider";
export default astroGhostcmsThemeProvider;

View File

@ -1,6 +0,0 @@
import type { GhostUserConfig } from "../schemas";
import config from "virtual:@matthiesenxyz/astro-ghostcms/config";
const UserConfig = config as GhostUserConfig;
export default UserConfig;

View File

@ -1,6 +1,6 @@
---
import './404.css';
import { getSettings, invariant } from '../../api';
import { getSettings, invariant } from '../api';
const settings = await getSettings();
invariant(settings, "Settings not found");

View File

@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";
import TS_API from "../../content-api";
import { TSGhostContentAPI } from "@ts-ghost/content-api";
const fetchMocker = createFetchMock(vi);
@ -9,7 +9,7 @@ describe("authors api .browse() Args Type-safety", () => {
const url = process.env.VITE_GHOST_URL || "https://my-ghost-blog.com";
const key =
process.env.VITE_GHOST_CONTENT_API_KEY || "59d4bf56c73c04a18c867dc3ba";
const api = new TS_API(url, key, "v5.0");
const api = new TSGhostContentAPI(url, key, "v5.0");
test(".browse() params shouldnt accept invalid params", () => {
// @ts-expect-error - shouldnt accept invalid params
const browse = api.authors.browse({ pp: 2 });
@ -95,10 +95,10 @@ describe("authors api .browse() Args Type-safety", () => {
});
describe("authors resource mocked", () => {
let api: TS_API;
let api: TSGhostContentAPI;
beforeEach(() => {
api = new TS_API(
api = new TSGhostContentAPI(
"https://my-ghost-blog.com",
"59d4bf56c73c04a18c867dc3ba",
"v5.0",

View File

@ -1,7 +1,7 @@
export * from "./authors";
export * from "./helpers";
export * from "./pages";
export * from "./posts";
export * from "./settings";
export * from "./socials";
export * from "./tags";
export * from "./tiers";

View File

@ -7,8 +7,8 @@ import {
} from "@ts-ghost/core-api";
import { z } from "astro/zod";
import { authorsSchema } from "../authors";
import { tagsSchema } from "../tags";
import { authorsSchema } from "./authors";
import { tagsSchema } from "./tags";
const postsAuthorSchema = authorsSchema.extend({
url: z.string().nullish(),

View File

@ -1,14 +1,14 @@
import { describe, expect, test } from "vitest";
import TS_API from "../../content-api";
import type { Post } from "./posts";
import { TSGhostContentAPI } from "@ts-ghost/content-api";
import type { Post } from "./index";
const url = process.env.VITE_GHOST_URL || "https://my-ghost-blog.com";
const key =
process.env.VITE_GHOST_CONTENT_API_KEY || "59d4bf56c73c04a18c867dc3ba";
describe("posts api .browse() Args Type-safety", () => {
const api = new TS_API(url, key, "v5.0");
const api = new TSGhostContentAPI(url, key, "v5.0");
test(".browse() params shouldnt accept invalid params", () => {
// @ts-expect-error - shouldnt accept invalid params
const browse = api.posts.browse({ pp: 2 });
@ -21,8 +21,7 @@ describe("posts api .browse() Args Type-safety", () => {
foo: true,
} satisfies { [k in keyof Post]?: true | undefined };
// biome-ignore lint/style/useConst: <explanation>
let test = api.posts
const test = api.posts
.browse()
// @ts-expect-error - shouldnt accept invalid params
.fields(outputFields);
@ -45,8 +44,7 @@ describe("posts api .browse() Args Type-safety", () => {
title: true,
} satisfies { [k in keyof Post]?: true | undefined };
// biome-ignore lint/style/useConst: <explanation>
let test = api.posts.browse().fields(outputFields);
const test = api.posts.browse().fields(outputFields);
expect(test.getOutputFields()).toEqual(["slug", "title"]);
// @ts-expect-error - shouldnt accept invalid params

View File

@ -7,8 +7,8 @@ import {
} from "@ts-ghost/core-api";
import { z } from "astro/zod";
import { authorsSchema } from "../authors";
import { tagsSchema } from "../tags";
import { authorsSchema } from "./authors";
import { tagsSchema } from "./tags";
const postsAuthorSchema = authorsSchema.extend({
url: z.string().nullish(),

View File

@ -0,0 +1,39 @@
import { beforeEach, describe, expect, test } from "vitest";
import { TSGhostContentAPI } from "@ts-ghost/content-api";
const url = process.env.VITE_GHOST_URL || "https://my-ghost-blog.com";
const key =
process.env.VITE_GHOST_CONTENT_API_KEY || "59d4bf56c73c04a18c867dc3ba";
describe("settings integration tests browse", () => {
let api: TSGhostContentAPI;
beforeEach(() => {
api = new TSGhostContentAPI(url, key, "v5.0");
});
test("settings.fetch()", async () => {
const result = await api.settings.fetch();
expect(result).not.toBeUndefined();
expect(result).not.toBeNull();
if (!result.success) {
expect(result.errors).toBeDefined();
expect(result.errors).toHaveLength(1);
} else {
expect(result.data).toBeDefined();
const settings = result.data;
expect(settings).toBeDefined();
expect(settings.title).toBe("Astro Starter");
expect(settings.description).toBe("Thoughts, stories and ideas.");
expect(settings.logo).toBeNull();
expect(settings.cover_image).toBe(
"https://static.ghost.org/v4.0.0/images/publication-cover.jpg",
);
expect(settings.icon).toBeNull();
expect(settings.lang).toBe("en");
expect(settings.timezone).toBe("Etc/UTC");
expect(settings.codeinjection_head).toBeNull();
expect(settings.codeinjection_foot).toBeNull();
expect(settings.members_support_address).toBe("noreply");
}
});
});

View File

@ -1,3 +0,0 @@
export * from "./robots";
export * from "./sitemap";
export * from "./userconfig";

View File

@ -1,37 +0,0 @@
import { z } from "astro/zod";
const RobotsPolicySchema = z.object({
/** You must provide a name of the automatic client (search engine crawler).
* Wildcards are allowed.
*/
userAgent: z.string(),
/** Allowed paths for crawling */
allow: z.string().optional(),
/** Disallowed paths for crawling */
disallow: z.string().optional(),
/** Indicates that the page's URL contains parameters that should be ignored during crawling.
* Maximum string length is limited to 500.
*/
cleanParam: z.string().optional(),
/** Minimum interval (in secs) for the crawler to wait after loading one page, before starting other */
crawlDelay: z.number().optional(),
});
export const RobotsTxtSchema = z.object({
/** @example host: true
* // Automatically resolve using the site option from Astro config
* @example host: 'example.com'
*/
host: z.string().optional(),
/** @example sitemap: "https://example.com/sitemap-0.xml"
* @example sitemap: ['https://example.com/sitemap-0.xml','https://example.com/sitemap-1.xml']
* @example sitemap: false - If you want to get the robots.txt file without the Sitemap: ... entry, set the sitemap parameter to false.
*/
sitemap: z.union([z.string(), z.string().array(), z.boolean()]).optional(),
/** astrojs/sitemap and astro-robots-txt integrations have the sitemap-index.xml as their primary output. That is why the default value of sitemapBaseFileName is set to sitemap-index.
* @example sitemapBaseFileName: 'custom-sitemap'
*/
sitemapBaseFileName: z.string().optional(),
/** SET POLICY RULES */
policy: RobotsPolicySchema.array().optional(),
});

View File

@ -1,38 +0,0 @@
import { z } from "astro/zod";
const localeKeySchema = z.string().min(1);
enum EnumChangefreq {
DAILY = "daily",
MONTHLY = "monthly",
ALWAYS = "always",
HOURLY = "hourly",
WEEKLY = "weekly",
YEARLY = "yearly",
NEVER = "never",
}
export const SitemapSchema = z.object({
filter: z.function().args(z.string()).returns(z.boolean()).optional(),
customPages: z.string().url().array().optional(),
i18n: z
.object({
defaultLocale: localeKeySchema,
locales: z.record(
localeKeySchema,
z
.string()
.min(2)
.regex(/^[a-zA-Z\-]+$/gm, {
message: "Only English alphabet symbols and hyphen allowed",
}),
),
})
.refine((val) => !val || val.locales[val.defaultLocale], {
message: "`defaultLocale` must exist in `locales` keys",
})
.optional(),
entryLimit: z.number().nonnegative().optional(),
serialize: z.function().args(z.any()).returns(z.any()).optional(),
changefreq: z.nativeEnum(EnumChangefreq).optional(),
lastmod: z.date().optional(),
priority: z.number().min(0).max(1).optional(),
});

View File

@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { GhostUserConfigSchema } from "./userconfig";
describe("GhostUserConfigSchema", () => {
it("should validate a valid user config", () => {
const validConfig = {
ghostURL: "https://ghostdemo.matthiesen.xyz",
ThemeProvider: {
disableThemeProvider: true,
theme: "@matthiesenxyz/astro-ghostcms-theme-default",
},
disableDefault404: false,
enableRSSFeed: true,
enableOGImages: true,
verbose: false,
Integrations: {},
};
const result = GhostUserConfigSchema.safeParse(validConfig);
expect(result.success).to.be.true;
// @ts-expect-error
expect(result.data).to.deep.equal(validConfig);
});
it("should invalidate an invalid user config", () => {
const invalidConfig = {
ghostURL: "invalid-url",
ThemeProvider: {
disableThemeProvider: "true",
theme: 123,
},
disableDefault404: "false",
enableRSSFeed: "true",
enableOGImages: "true",
Integrations: {
sitemap: {
// invalid sitemap configuration
},
robotstxt: {
// invalid robotstxt configuration
},
},
verbose: "false",
};
const result = GhostUserConfigSchema.safeParse(invalidConfig);
expect(result.success).to.be.false;
expect(!result.success).to.exist;
});
});

View File

@ -1,7 +1,8 @@
import type { SitemapOptions } from "@astrojs/sitemap";
import type { RobotsTxtOptions } from "astro-robots-txt";
import { z } from "astro/zod";
import { RobotsTxtSchema, SitemapSchema } from "./index";
export const UserConfigSchema = z.object({
export const GhostUserConfigSchema = z.object({
/** OPTIONAL - Either set the URL in your .env or put it here
* @example
* // https://astro.build/config
@ -12,46 +13,59 @@ export const UserConfigSchema = z.object({
* })
* ],
* }); */
ghostURL: z.string().url().optional(),
/** OPTIONAL - Allows the user to disable the `/rss.xml` injection */
disableRSS: z.boolean().default(false),
/** OPTIONAL - Allows the user to disable the `/open-graph/*` route injection
* @ This feature uses `satori` to generate OpenGraph Images
ghostURL: z.coerce.string().url("Must be a URL").optional(),
/** OPTIONAL - Configure the Theme Provider
* @ This option allows the user to configure the Theme Provider
*/
disableSatoriOG: z.boolean().default(false),
/** OPTIONAL - Allows the user to disable the `/404` injection */
disable404: z.boolean().default(false),
/** OPTIONAL - Disable Route Injector
* This option allows the user to disable the route injection system and utilize just the integraions other functions. Such as API, sitemap and robotstxt integrations. */
disableRouteInjection: z.boolean().default(false),
/** OPTIONAL - (Default: true) Allows the user to disable "info" console output */
disableConsoleOutput: z.boolean().default(true),
/** OPTIONAL - Theme Selector
* This option allows the user to replace the included theme with an external npm module
* @example
* // https://astro.build/config
* export default defineConfig({
* integrations: [
* ghostcms({
* theme: "@matthiesenxyz/astro-ghostcms-theme-default"
* })
* ],
* }); */
theme: z.string().default("@matthiesenxyz/astro-ghostcms-theme-default"),
/** OPTIONAL - astrojs/sitemap
* This option allows the user to configure the included integration
ThemeProvider: z
.object({
/** OPTIONAL - Disable the theme provider
* @default false
*/
disableThemeProvider: z.coerce.boolean(),
/** OPTIONAL - Set the theme you want to use
* @default "@matthiesenxyz/astro-ghostcms-theme-default"
*/
theme: z.string(),
})
.optional()
.default({
disableThemeProvider: false,
theme: "@matthiesenxyz/astro-ghostcms-theme-default"
}),
/** Allows the user to disable the provided 404 page */
disableDefault404: z.coerce.boolean().optional(),
/** Allows the user to disable the provided RSS Feed */
enableRSSFeed: z.coerce.boolean().optional().default(true),
/** Allows the user to Enable or Disable the default Satori OG Image Generation
* @default true
*/
enableOGImages: z.coerce.boolean().optional().default(true),
/** Allows the user to turn on/off Full Console Logs
* @default true
*/
verbose: z.coerce.boolean().optional(),
/** OPTIONAL - Integrations Configuration
* This option allows the user to configure the included integrations
* Options shown are the availble options
* REFERENCE https://docs.astro.build/en/guides/integrations-guide/sitemap
*/
sitemap: SitemapSchema.optional(),
/** OPTIONAL - astro-robots-txt
* This option allows the user to configure the included integration
* Options shown are the availble options
* REFERENCE https://www.npmjs.com/package/astro-robots-txt#configuration
*/
robotstxt: RobotsTxtSchema.optional(),
Integrations: z
.object({
/** Optional - astro-robots-txt
* This option allows the user to configure the included integration
* Options shown are the availble options
* @see https://www.npmjs.com/package/astro-robots-txt#configuration
*/
robotsTxt: z.custom<RobotsTxtOptions>().optional(),
/** OPTIONAL - astrojs/sitemap
* This option allows the user to configure the included integration
* Options shown are the availble options
* @see https://docs.astro.build/en/guides/integrations-guide/sitemap
*/
sitemap: z.custom<SitemapOptions>().optional(),
})
.optional().default({}),
});
/** USER CONFIGURATION SCHEMA */
export type UserConfig = z.infer<typeof UserConfigSchema>;
export type GhostUserConfig = z.infer<typeof UserConfigSchema>;
export type GhostUserConfig = z.infer<typeof GhostUserConfigSchema>;

View File

@ -1,82 +0,0 @@
/** MIT License
Copyright (c) 2024 Florian Lefebvre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import { type Mock, afterEach, describe, expect, test, vi } from "vitest";
import { addVirtualImport } from "./add-virtual-import.js";
import { addVitePlugin } from "./add-vite-plugin.js";
vi.mock("./add-vite-plugin");
const pluginNameStub = <T extends string>(name: T): `vite-plugin-${T}` =>
`vite-plugin-${name}`;
describe("add-virtual-import", () => {
const name = "test-module";
const content = "export default {}";
afterEach(() => {
vi.clearAllMocks();
});
test("It should call `addVitePlugin`", () => {
const updateConfig = vi.fn();
addVirtualImport({
name,
content,
updateConfig,
});
expect(addVitePlugin).toHaveBeenCalled();
});
test("`addVitePlugin` should get called with the correct plugin name", () => {
const updateConfig = vi.fn();
addVirtualImport({
name,
content,
updateConfig,
});
const expectedName = pluginNameStub(name);
const { plugin } = (addVitePlugin as Mock).mock.lastCall[0];
expect(plugin.name).toEqual(expectedName);
});
test("Virtual module should resolve correct name", () => {
const updateConfig = vi.fn();
addVirtualImport({
name,
content,
updateConfig,
});
const { plugin } = (addVitePlugin as Mock).mock.lastCall[0];
const resolvedVirtualModuleId = plugin.resolveId(name);
expect(resolvedVirtualModuleId).toEqual(`\0${name}`);
});
});

View File

@ -1,93 +0,0 @@
/** MIT License
Copyright (c) 2024 Florian Lefebvre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import type { HookParameters } from "astro";
import type { Plugin } from "vite";
import { addVitePlugin } from "./add-vite-plugin.js";
const resolveVirtualModuleId = <T extends string>(id: T): `\0${T}` => {
return `\0${id}`;
};
const createVirtualModule = (name: string, content: string): Plugin => {
const pluginName = `vite-plugin-${name}`;
return {
name: pluginName,
resolveId(id) {
if (id === name) {
return resolveVirtualModuleId(id);
}
},
load(id) {
if (id === resolveVirtualModuleId(name)) {
return content;
}
},
};
};
/**
* Creates a Vite virtual module and updates the Astro config.
* Virtual imports are useful for passing things like config options, or data computed within the integration.
*
* @param {object} params
* @param {string} params.name
* @param {string} params.content
* @param {import("astro").HookParameters<"astro:config:setup">["updateConfig"]} params.updateConfig
*
* @see https://astro-integration-kit.netlify.app/utilities/add-virtual-import/
*
* @example
* ```ts
* // my-integration/index.ts
* import { addVirtualImport } from "astro-integration-kit";
*
* addVirtualImport(
* name: 'virtual:my-integration/config',
* content: `export default ${ JSON.stringify({foo: "bar"}) }`,
* updateConfig
* );
* ```
*
* This is then readable anywhere else in your integration:
*
* ```ts
* // myIntegration/src/component/layout.astro
* import config from "virtual:my-integration/config";
*
* console.log(config.foo) // "bar"
* ```
*/
export const addVirtualImport = ({
name,
content,
updateConfig,
}: {
name: string;
content: string;
updateConfig: HookParameters<"astro:config:setup">["updateConfig"];
}) => {
addVitePlugin({
plugin: createVirtualModule(name, content),
updateConfig,
});
};

View File

@ -1,69 +0,0 @@
import type { Plugin } from "vite";
import { describe, expect, test, vi } from "vitest";
import { addVitePlugin } from "./add-vite-plugin.js";
describe("addVitePlugin", () => {
test("Should run", () => {
const updateConfig = vi.fn();
expect(() =>
addVitePlugin({
plugin: null,
updateConfig,
}),
).not.toThrow();
});
test("Should call updateConfig", () => {
const updateConfig = vi.fn();
addVitePlugin({
plugin: null,
updateConfig,
});
expect(updateConfig).toHaveBeenCalled();
});
test("Should add vite plugin", () => {
let plugin: Plugin;
const pluginName = "test-plugin";
const updateConfig = vi.fn((config) => {
plugin = config.vite.plugins[0];
});
const expectedPlugin = {
name: pluginName,
};
addVitePlugin({
plugin: expectedPlugin,
updateConfig,
});
// @ts-ignore
expect(plugin).toBeDefined();
});
test("Plugin name should match", () => {
let plugin: Plugin;
const pluginName = "test-plugin";
const updateConfig = vi.fn((config) => {
plugin = config.vite.plugins[0];
});
const expectedPlugin = {
name: pluginName,
};
addVitePlugin({
plugin: expectedPlugin,
updateConfig,
});
// @ts-ignore
expect(plugin.name).toBe(pluginName);
});
});

View File

@ -1,55 +0,0 @@
/** MIT License
Copyright (c) 2024 Florian Lefebvre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import type { HookParameters } from "astro";
import type { PluginOption } from "vite";
/**
* Adds a [vite plugin](https://vitejs.dev/guide/using-plugins) to the
* Astro config.
*
* @param {Params} params
* @param {import("vite").Plugin} params.plugin
* @param {import("astro").HookParameters<"astro:config:setup">["updateConfig"]} params.updateConfig
*
* @see https://astro-integration-kit.netlify.app/utilities/add-vite-plugin/
*
* @example
* ```ts
* addVitePlugin({
* plugin,
* updateConfig
* })
* ```
*/
export const addVitePlugin = ({
plugin,
updateConfig,
}: {
plugin: PluginOption;
updateConfig: HookParameters<"astro:config:setup">["updateConfig"];
}) => {
updateConfig({
vite: {
plugins: [plugin],
},
});
};

View File

@ -1,22 +0,0 @@
import * as zod from "zod";
export class ValidationError extends Error {
name;
details;
constructor(message, options) {
super(message, options);
this.name = "ZodValidationError";
this.details = getIssuesFromErrorOptions(options);
}
toString() {
return this.message;
}
}
function getIssuesFromErrorOptions(options) {
if (options) {
const cause = options.cause;
if (cause instanceof zod.ZodError) {
return cause.issues;
}
}
return [];
}

View File

@ -1,5 +0,0 @@
export const ISSUE_SEPARATOR = "; ";
export const MAX_ISSUES_IN_MESSAGE = 99;
export const PREFIX = "Validation error";
export const PREFIX_SEPARATOR = ": ";
export const UNION_SEPARATOR = ", or ";

View File

@ -1,10 +0,0 @@
import { fromZodIssue } from "./fromZodIssue";
export const errorMap = (issue, ctx) => {
const error = fromZodIssue({
...issue,
message: issue.message ?? ctx.defaultError,
});
return {
message: error.message,
};
};

View File

@ -1,37 +0,0 @@
import { ValidationError } from "./ValidationError";
import {
ISSUE_SEPARATOR,
MAX_ISSUES_IN_MESSAGE,
PREFIX,
PREFIX_SEPARATOR,
UNION_SEPARATOR,
} from "./config";
import { getMessageFromZodIssue } from "./fromZodIssue";
import { prefixMessage } from "./prefixMessage";
export function fromZodError(zodError, options = {}) {
const {
maxIssuesInMessage = MAX_ISSUES_IN_MESSAGE,
issueSeparator = ISSUE_SEPARATOR,
unionSeparator = UNION_SEPARATOR,
prefixSeparator = PREFIX_SEPARATOR,
prefix = PREFIX,
includePath = true,
} = options;
const zodIssues = zodError.errors;
const reason =
zodIssues.length === 0
? zodError.message
: zodIssues
.slice(0, maxIssuesInMessage)
.map((issue) =>
getMessageFromZodIssue({
issue,
issueSeparator,
unionSeparator,
includePath,
}),
)
.join(issueSeparator);
const message = prefixMessage(reason, prefix, prefixSeparator);
return new ValidationError(message, { cause: zodError });
}

View File

@ -1,61 +0,0 @@
import * as zod from "zod";
import { ValidationError } from "./ValidationError";
import {
ISSUE_SEPARATOR,
PREFIX,
PREFIX_SEPARATOR,
UNION_SEPARATOR,
} from "./config";
import { prefixMessage } from "./prefixMessage";
import { isNonEmptyArray } from "./utils/NonEmptyArray";
import { joinPath } from "./utils/joinPath";
export function getMessageFromZodIssue(props) {
const { issue, issueSeparator, unionSeparator, includePath } = props;
if (issue.code === "invalid_union") {
return issue.unionErrors
.reduce((acc, zodError) => {
const newIssues = zodError.issues
.map((issue) =>
getMessageFromZodIssue({
issue,
issueSeparator,
unionSeparator,
includePath,
}),
)
.join(issueSeparator);
if (!acc.includes(newIssues)) {
acc.push(newIssues);
}
return acc;
}, [])
.join(unionSeparator);
}
if (includePath && isNonEmptyArray(issue.path)) {
if (issue.path.length === 1) {
const identifier = issue.path[0];
if (typeof identifier === "number") {
return `${issue.message} at index ${identifier}`;
}
}
return `${issue.message} at "${joinPath(issue.path)}"`;
}
return issue.message;
}
export function fromZodIssue(issue, options = {}) {
const {
issueSeparator = ISSUE_SEPARATOR,
unionSeparator = UNION_SEPARATOR,
prefixSeparator = PREFIX_SEPARATOR,
prefix = PREFIX,
includePath = true,
} = options;
const reason = getMessageFromZodIssue({
issue,
issueSeparator,
unionSeparator,
includePath,
});
const message = prefixMessage(reason, prefix, prefixSeparator);
return new ValidationError(message, { cause: new zod.ZodError([issue]) });
}

View File

@ -1,7 +0,0 @@
export { ValidationError } from "./ValidationError";
export { isValidationError } from "./isValidationError";
export { isValidationErrorLike } from "./isValidationErrorLike";
export { errorMap } from "./errorMap";
export { fromZodIssue } from "./fromZodIssue";
export { fromZodError } from "./fromZodError";
export { toValidationError } from "./toValidationError";

View File

@ -1,4 +0,0 @@
import { ValidationError } from "./ValidationError";
export function isValidationError(err) {
return err instanceof ValidationError;
}

View File

@ -1,3 +0,0 @@
export function isValidationErrorLike(err) {
return err instanceof Error && err.name === "ZodValidationError";
}

View File

@ -1,13 +0,0 @@
import { PREFIX } from "./config";
export function prefixMessage(message, prefix, prefixSeparator) {
if (prefix !== null) {
if (message.length > 0) {
return [prefix, message].join(prefixSeparator);
}
return prefix;
}
if (message.length > 0) {
return message;
}
return PREFIX;
}

View File

@ -1,14 +0,0 @@
import * as zod from "zod";
import { ValidationError } from "./ValidationError";
import { fromZodError } from "./fromZodError";
export const toValidationError =
(options = {}) =>
(err) => {
if (err instanceof zod.ZodError) {
return fromZodError(err, options);
}
if (err instanceof Error) {
return new ValidationError(err.message, { cause: err });
}
return new ValidationError("Unknown error");
};

View File

@ -1,3 +0,0 @@
export function isNonEmptyArray(value) {
return value.length !== 0;
}

View File

@ -1,22 +0,0 @@
const identifierRegex = /[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*/u;
export function joinPath(path) {
if (path.length === 1) {
return path[0].toString();
}
return path.reduce((acc, item) => {
if (typeof item === "number") {
return `${acc}[${item.toString()}]`;
}
if (item.includes('"')) {
return `${acc}["${escapeQuotes(item)}"]`;
}
if (!identifierRegex.test(item)) {
return `${acc}["${item}"]`;
}
const separator = acc.length === 0 ? "" : ".";
return acc + separator + item;
}, "");
}
function escapeQuotes(str) {
return str.replace(/"/g, '\\"');
}

View File

@ -0,0 +1,6 @@
{
"extends": "astro/tsconfigs/strictest",
"compilerOptions": {
"jsx": "preserve"
}
}

View File

@ -1,4 +1,4 @@
declare module "virtual:@matthiesenxyz/astro-ghostcms/config" {
const Config: import("../schemas/index").GhostUserConfig;
const Config: import("./src/schemas/userconfig").GhostUserConfig;
export default Config;
}

View File

@ -1,11 +1,8 @@
/// <reference types="vitest" />
/// <reference types="vite/client" />
import tsconfigPaths from "vite-tsconfig-paths";
import { defineProject } from "vitest/config";
export default defineProject({
plugins: [tsconfigPaths()],
test: {
globals: true,
include: ["./**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],

View File

@ -46,17 +46,7 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/node": "^20.11.19",
"@types/tar-fs": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-node": "^0.3.7",
"eslint-import-resolver-typescript": "^3.5.3",
"prettier": "^3.2.5",
"typescript": "^5.3.3",
"vitest": "^1.3.0",
"vite": "^5.1.3",
"vite-tsconfig-paths": "^4.2.2"
"typescript": "^5.3.3"
},
"publishConfig": {
"access": "public"

View File

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.2.8",
"@matthiesenxyz/astro-ghostcms": "^3.1.5",
"@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.5"
"astro": "^4.4.0",
"@matthiesenxyz/astro-ghostcms": "^3.3.0",
"@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.13"
},
"devDependencies": {
"@astrojs/check": "^0.4.1",

View File

@ -1,16 +1,18 @@
import astroGhostCMS from "@matthiesenxyz/astro-ghostcms";
import { defineConfig } from "astro/config";
import ghostcms from "@matthiesenxyz/astro-ghostcms";
import UnoCSS from 'unocss/astro';
import UnoCSS from "unocss/astro";
// https://astro.build/config
export default defineConfig({
site: "https://example.xyz/",
trailingSlash: 'ignore',
trailingSlash: "ignore",
integrations: [
UnoCSS({ injectReset: true }),
ghostcms({
theme: "@matthiesenxyz/astro-ghostcms-brutalbyelian",
astroGhostCMS({
ghostURL: "https://ghostdemo.matthiesen.xyz",
})
ThemeProvider: {
theme: "@matthiesenxyz/astro-ghostcms-brutal",
},
}),
],
});

View File

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.3.2",
"@matthiesenxyz/astro-ghostcms": "^3.2.2",
"@matthiesenxyz/astro-ghostcms-brutalbyelian": "^0.0.5",
"astro": "^4.4.0",
"@matthiesenxyz/astro-ghostcms": "^3.3.0",
"@matthiesenxyz/astro-ghostcms-brutalbyelian": "^0.0.11",
"@unocss/astro": "^0.57.7"
},
"devDependencies": {

View File

@ -1,9 +1,9 @@
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly CONTENT_API_KEY: string
readonly CONTENT_API_URL: string
readonly CONTENT_API_KEY: string;
readonly CONTENT_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
readonly env: ImportMetaEnv;
}

View File

@ -1,6 +1,6 @@
import brutalTheme from '@matthiesenxyz/astro-ghostcms-brutalbyelian';
import { defineConfig } from 'unocss';
import brutalTheme from "@matthiesenxyz/astro-ghostcms-brutalbyelian";
import { defineConfig } from "unocss";
export default defineConfig({
presets: [ brutalTheme() ],
presets: [brutalTheme()],
});

View File

@ -1,5 +1,5 @@
import tailwind from "@astrojs/tailwind";
import ghostcms from "@matthiesenxyz/astro-ghostcms";
import astroGhostCMS from "@matthiesenxyz/astro-ghostcms";
import { defineConfig } from "astro/config";
// https://astro.build/config
@ -7,9 +7,11 @@ export default defineConfig({
site: "https://example.xyz/",
integrations: [
tailwind(),
ghostcms({
theme: "@matthiesenxyz/astro-ghostcms-catppuccin",
astroGhostCMS({
ghostURL: "https://ghostdemo.matthiesen.xyz",
ThemeProvider: {
theme: "@matthiesenxyz/astro-ghostcms-catppuccin",
},
}),
],
});

View File

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.2.8",
"@matthiesenxyz/astro-ghostcms": "^3.1.8",
"@matthiesenxyz/astro-ghostcms-catppuccin": "^0.0.3",
"astro": "^4.4.0",
"@matthiesenxyz/astro-ghostcms": "^3.3.0",
"@matthiesenxyz/astro-ghostcms-catppuccin": "^0.0.8",
"@astrojs/tailwind": "^5.1.0",
"tailwindcss": "^3.3.5"
},

View File

@ -9,10 +9,13 @@ export default defineConfig({
// Includes GhostCMS API, @astrojs/rss, @astrojs/sitemap, and astro-robots-txt
GhostCMS({
ghostURL: "https://ghostdemo.matthiesen.xyz",
// This Option Disables all default theme injection and allows DIY mode.
disableRouteInjection: true,
// Enable this to disable the extra console logs
disableConsoleOutput: false,
ThemeProvider: {
disableThemeProvider: true,
},
disableDefault404: true,
enableRSSFeed: false,
enableOGImages: false,
verbose: false,
}),
],
});

View File

@ -11,8 +11,8 @@
},
"dependencies": {
"@astrojs/check": "^0.4.1",
"@matthiesenxyz/astro-ghostcms": "^3.1.4",
"astro": "^4.2.8",
"@matthiesenxyz/astro-ghostcms": "^3.3.0",
"astro": "^4.4.0",
"typescript": "^5.3.3",
"astro-font": "^0.0.77"
},

View File

@ -48,7 +48,9 @@ import starlightGhostCMS from '@matthiesenxyz/starlight-ghostcms';
export default defineConfig({
integrations: [
starlight({
plugins: [starlightGhostCMS()],
plugins: [
starlightGhostCMS()
],
title: 'My Docs',
}),
],

View File

@ -1,91 +1,117 @@
import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types'
import type { AstroIntegrationLogger } from 'astro'
import { type StarlightGhostConfig, validateConfig } from './src/schemas/config'
import { vitePluginStarlightGhostConfig } from './src/integrations/vite'
import { facebook, getSettings, invariant, twitter } from './src/utils/api'
import type {
StarlightPlugin,
StarlightUserConfig,
} from "@astrojs/starlight/types";
import type { AstroIntegrationLogger } from "astro";
import {
type StarlightGhostConfig,
validateConfig,
} from "./src/schemas/config";
import { facebook, getSettings, invariant, twitter } from "./src/utils/api";
import starlightGhostcms from "./src/integrations/starlight-ghostcms";
const settings = await getSettings()
const settings = await getSettings();
export type { StarlightGhostConfig }
export type { StarlightGhostConfig };
export default function starlightGhostCMS(userConfig?: StarlightGhostConfig): StarlightPlugin {
const config: StarlightGhostConfig = validateConfig(userConfig)
invariant(settings, "Settings not available... check your api key/url")
export default function starlightGhostCMS(
userConfig?: StarlightGhostConfig,
): StarlightPlugin {
const config: StarlightGhostConfig = validateConfig(userConfig);
invariant(settings, "Settings not available... check your api key/url");
return {
name: '@matthiesenxyz/starlight-ghostcms-plugin',
hooks: {
setup({ astroConfig, addIntegration, config: starlightConfig, logger, updateConfig: updateStarlightConfig }) {
updateStarlightConfig({
social: {
...starlightConfig.social,
rss: `${astroConfig.site}/rss.xml`,
twitter: twitter(settings.twitter?settings.twitter:""),
facebook: facebook(settings.facebook?settings.facebook:""),
},
components: {
...starlightConfig.components,
...overrideStarlightComponent(starlightConfig.components, logger, 'MarkdownContent'),
...overrideStarlightComponent(starlightConfig.components, logger, 'Sidebar'),
...overrideStarlightComponent(starlightConfig.components, logger, "SiteTitle"),
}
})
return {
name: "@matthiesenxyz/starlight-ghostcms-plugin",
hooks: {
setup({
astroConfig,
addIntegration,
config: starlightConfig,
logger,
updateConfig: updateStarlightConfig,
}) {
// Add the Starlight-GhostCMS integration
addIntegration(starlightGhostcms(config));
// Update the Starlight config with the GhostCMS config
updateStarlightConfig({
social: {
...starlightConfig.social,
...overrideRSS(starlightConfig.social, astroConfig.site),
...overrideTwitter(starlightConfig.social),
...overrideFacebook(starlightConfig.social),
},
components: {
...starlightConfig.components,
...overrideStarlightComponent(
starlightConfig.components,
logger,
"MarkdownContent",
),
...overrideStarlightComponent(
starlightConfig.components,
logger,
"Sidebar",
),
...overrideStarlightComponent(
starlightConfig.components,
logger,
"SiteTitle",
),
},
});
},
},
};
}
addIntegration({
name: '@matthiesenxyz/starlight-ghostcms',
hooks: {
'astro:config:setup': ({ injectRoute, updateConfig }) => {
injectRoute({
pattern: '/blog',
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/index.astro',
prerender: true,
})
injectRoute({
pattern: '/blog/[slug]',
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/[slug].astro',
prerender: true,
})
injectRoute({
pattern: '/blog/about',
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/about.astro',
prerender: true,
})
injectRoute({
pattern: '/blog/authors',
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/authors.astro',
})
injectRoute({
pattern: '/rss.xml',
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/rss.xml.ts'
})
function overrideRSS(
socials: StarlightUserConfig["social"],
url: string | undefined
) {
if (socials?.rss) { return {}; }
if (url === undefined) { return undefined; }
return { rss: `${url}/rss.xml` };
}
updateConfig({
vite: {
plugins: [vitePluginStarlightGhostConfig(config)],
},
})
}
}
})
}
},
}
function overrideTwitter(
socials: StarlightUserConfig["social"],
) {
if (socials?.twitter) { return {}; }
if (settings?.twitter) {
return { twitter: twitter(settings.twitter), }
}
return undefined;
}
function overrideFacebook(
socials: StarlightUserConfig["social"],
) {
if (socials?.facebook) { return {}; }
if (settings?.facebook) {
return { facebook: facebook(settings.facebook), }
}
return undefined;
}
function overrideStarlightComponent(
components: StarlightUserConfig['components'],
logger: AstroIntegrationLogger,
component: keyof NonNullable<StarlightUserConfig['components']>,
) {
if (components?.[component]) {
logger.warn(`It looks like you already have a \`${component}\` component override in your Starlight configuration.`)
logger.warn(`To use \`starlight-ghostcms\`, remove the override for the \`${component}\` component.\n`)
logger.warn("This Warning can be ignored if you know what your doing ;)")
return {}
}
return {
[component]: `@matthiesenxyz/starlight-ghostcms/overrides/${component}.astro`,
}
}
components: StarlightUserConfig["components"],
logger: AstroIntegrationLogger,
component: keyof NonNullable<StarlightUserConfig["components"]>,
) {
if (components?.[component]) {
logger.warn(
`It looks like you already have a \`${component}\` component override in your Starlight configuration.`,
);
logger.warn(
`To use \`starlight-ghostcms\`, remove the override for the \`${component}\` component.\n`,
);
logger.warn("This Warning can be ignored if you know what your doing ;)");
return {};
}
return {
[component]: `@matthiesenxyz/starlight-ghostcms/overrides/${component}.astro`,
};
}

View File

@ -36,21 +36,15 @@
"types": "index.ts",
"files": [
"src",
"CHANGELOG.md",
"index.ts",
"tsconfig.json",
"types.d.ts"
"LICENSE",
"package.json",
"README.md"
],
"exports": {
".": "./index.ts",
"./overrides/MarkdownContent.astro": "./src/overrides/MarkdownContent.astro",
"./overrides/Sidebar.astro": "./src/overrides/Sidebar.astro",
"./overrides/SiteTitle.astro": "./src/overrides/SiteTitle.astro",
"./routes/index.astro": "./src/routes/index.astro",
"./routes/about.astro": "./src/routes/about.astro",
"./routes/authors.astro": "./src/routes/authors.astro",
"./routes/[slug].astro": "./src/routes/[slug].astro",
"./routes/rss.xml.ts": "./src/routes/rss.xml.ts",
"./schema": "./src/schemas/config.ts"
"./overrides/*": "./src/overrides/*"
},
"scripts": {
"test": "vitest run",
@ -60,19 +54,20 @@
},
"devDependencies": {
"@astrojs/starlight": "^0.19.0",
"@ts-ghost/tsconfig": "workspace:*",
"astro": "^4.4.0",
"vitest": "^1.2.2",
"astro": "^4.4.1",
"vitest": "^1.3.1",
"vitest-fetch-mock": "^0.2.2"
},
"dependencies": {
"@astrojs/rss": "^4.0.5",
"@ts-ghost/core-api": "^5.1.2",
"vite": "^5.1.2",
"vite-tsconfig-paths": "^4.2.2"
"@ts-ghost/core-api": "^6.0.0",
"@ts-ghost/content-api": "^4.0.12",
"astro-integration-kit": "^0.5.1",
"vite": "^5.1.4",
"vite-tsconfig-paths": "^4.3.1"
},
"peerDependencies": {
"@astrojs/starlight": ">=0.19.0",
"astro": ">=4.3.7"
"astro": ">=4.4.1"
}
}

View File

@ -0,0 +1,52 @@
import {
createResolver,
defineIntegration,
} from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { z } from "astro/zod";
import { type StarlightGhostConfig } from "../schemas/config";
export default defineIntegration({
name: "@matthiesenxyz/starlight-ghostcms",
optionsSchema: z.custom<StarlightGhostConfig>(),
plugins: [...corePlugins],
setup({ options }) {
const { resolve } = createResolver(import.meta.url);
return {
"astro:config:setup": ({
watchIntegration,
addVirtualImports,
injectRoute
}) => {
watchIntegration(resolve());
addVirtualImports({
'virtual:starlight-ghostcms/config': `export default ${JSON.stringify(options)}`,
});
const makeRoute = (endpoint: string, entrypoint: string) => {
injectRoute({
pattern: `/${endpoint}`,
entrypoint: resolve(`../routes/${entrypoint}`),
prerender: true,
});
};
const sanitisedRoute = options.route
.replace(/^\//, '')
.replace(/\/$/, '');
makeRoute(`${sanitisedRoute}`,
"index.astro");
makeRoute(`${sanitisedRoute}/[slug]`,
"[slug].astro");
makeRoute(`${sanitisedRoute}/about`,
"about.astro");
makeRoute(`${sanitisedRoute}/authors`,
"authors.astro");
makeRoute("rss.xml",
"rss.xml.ts");
}
}
}
})

View File

@ -1,22 +0,0 @@
import type { ViteUserConfig } from 'astro'
import type { StarlightGhostConfig } from '../schemas/config.ts'
// Expose the starlight-blog plugin configuration.
export function vitePluginStarlightGhostConfig(config: StarlightGhostConfig): VitePlugin {
const moduleId = 'virtual:starlight-ghost-config'
const resolvedModuleId = `\0${moduleId}`
const moduleContent = `export default ${JSON.stringify(config)}`
return {
name: 'vite-plugin-starlight-ghost-config',
load(id) {
return id === resolvedModuleId ? moduleContent : undefined
},
resolveId(id) {
return id === moduleId ? resolvedModuleId : undefined
},
}
}
type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number]

View File

@ -1,22 +1,27 @@
---
import StarlightMarkdownContent from '@astrojs/starlight/components/MarkdownContent.astro'
import type { Props } from '@astrojs/starlight/props'
import config from 'virtual:starlight-ghostcms/config'
import { isAnyBlogPostPage } from '../utils/page'
import Metadata from '../components/Metadata.astro'
import type { Post } from '../schemas/posts'
export function checkpath(path: string){
return path.split('/').includes(config.route) ? true : false
}
const isBlog = checkpath(Astro.url.pathname)
const isBlogPost = isAnyBlogPostPage(Astro.props.slug)
let blogEntry: Post | undefined = undefined
---
<StarlightMarkdownContent {...Astro.props}>
{isBlogPost && blogEntry ? <Metadata entry={blogEntry} /> : null}
{isBlog && blogEntry ? <Metadata entry={blogEntry} /> : null}
<slot />
{
isBlogPost && blogEntry ? (
isBlog && blogEntry ? (
<div class="post-footer">
</div>
) : null

View File

@ -1,9 +1,9 @@
---
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro'
import type { Props } from '@astrojs/starlight/props'
import config from 'virtual:starlight-ghost-config'
import config from 'virtual:starlight-ghostcms/config'
import { isBlogPostPage, isBlogRoot } from '../utils/page'
import { getAllPages, getAllPosts, getSluggedPage } from '../utils/api/api-functions.js'
import { getAllPages, getAllPosts, getSluggedPage } from '../utils/api'
import type { SidebarEntry } from './sidebartypes'
export async function getRecentBlogEntries(){
@ -17,19 +17,17 @@ export async function getBlogPageEntries(){
}
export function checkpath(path: string){
if ( path.slice(0, 5) === "/blog" ){
return true
} else { return false }
return path.split('/').includes(config.route) ? true : false
}
export function isAbout(path: string){
if ( path === "/blog/about" ){
if ( path === `/${config.route}/about` ){
return true
} else { return false }
}
export function isAuthors(path: string){
if ( path === "/blog/authors" ){
if ( path === `/${config.route}/authors` ){
return true
} else { return false }
}
@ -38,7 +36,7 @@ const recentEntries = isBlog ? await getRecentBlogEntries() : []
const aboutPage = await getSluggedPage("about");
const AboutEntry:SidebarEntry = {
attrs: {}, badge: undefined,
href: '/blog/about',
href: `/${config.route}/about`,
isCurrent: isAbout(Astro.url.pathname),
type: 'link',
label: aboutPage?.post?.title
@ -55,7 +53,7 @@ const blogSidebar: Props['sidebar'] = isBlog
{
attrs: {},
badge: undefined,
href: '/blog/authors',
href: `/${config.route}/authors`,
isCurrent: isAuthors(Astro.url.pathname),
label: 'Our Authors',
type: 'link',
@ -63,7 +61,7 @@ const blogSidebar: Props['sidebar'] = isBlog
{
attrs: {},
badge: undefined,
href: '/blog',
href: `/${config.route}`,
isCurrent: isBlogRoot(Astro.props.slug),
label: 'All posts',
type: 'link',
@ -74,8 +72,8 @@ const blogSidebar: Props['sidebar'] = isBlog
entries: recentEntries.map((blogEntry) => ({
attrs: {},
badge: blogEntry.featured?({text: "⭐", variant: "note"}):undefined,
href: `/blog/${blogEntry.slug}`,
isCurrent: isBlogPostPage(Astro.props.slug, `blog/${blogEntry.slug}`),
href: `/${config.route}/${blogEntry.slug}`,
isCurrent: isBlogPostPage(Astro.props.slug, `${config.route}/${blogEntry.slug}`),
label: blogEntry.title,
type: 'link',
})),
@ -89,7 +87,7 @@ const blogSidebar: Props['sidebar'] = isBlog
{
!isBlog && (
<div class="md:sl-hidden">
<a href="/blog">Blog</a>
<a href={`/${config.route}`}>Blog</a>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More