beta.0 initial comit testing

This commit is contained in:
Adam Matthiesen 2024-01-12 23:20:44 -08:00
commit 8788d07d6d
7 changed files with 666 additions and 0 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Welcome to Astro-GhostCMS
This addon uses the `@tryghost/content-api` and creates astro friendly functions to interface between ghost and astro.
## Work In Progress README (*More Information will be provided as time goes on...*)

2
index.ts Normal file
View File

@ -0,0 +1,2 @@
export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './src/ghost';
export { api } from './src/Content-API/api-client';

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@adammatthiesen/astro-ghostcms",
"description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content",
"version": "0.0.1-beta.0",
"author": "Adam Matthiesen <adam@matthiesen.xyz>",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Adammatthiesen/astro-ghostcms.git"
},
"bugs": {
"url": "https://github.com/Adammatthiesen/astro-ghostcms/issues"
},
"homepage": "https://github.com/Adammatthiesen/astro-ghostcms",
"exports": {
".": "./index.ts"
},
"files": [
"src",
"index.ts"
],
"keywords": [
"astro-component",
"withastro",
"ghost",
"ghostcms"
],
"scripts": {},
"devDependencies": {
"astro": "^4.1.1"
},
"peerDependencies": {
"astro": "^4.0.0"
},
"dependencies": {
"@astrojs/check": "^0.3.4",
"typescript": "^5.3.3",
"axios": "^1.0.0"
},
"main": "index.ts"
}

View File

@ -0,0 +1,8 @@
import GhostContentAPI from './ghostContentAPI';
// 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"
export const api = new GhostContentAPI({ key, url, version })

View File

@ -0,0 +1,294 @@
'use strict';
import axios from 'axios';
var name$1 = "@tryghost/content-api";
var version = "1.11.20";
var repository = "https://github.com/TryGhost/SDK/tree/main/packages/content-api";
var author = "Ghost Foundation";
var license = "MIT";
var main = "cjs/content-api.js";
var unpkg = "umd/content-api.min.js";
var module$1 = "es/content-api.js";
var source = "lib/content-api.js";
var files = [
"LICENSE",
"README.md",
"cjs/",
"lib/",
"umd/",
"es/"
];
var scripts = {
dev: "echo \"Implement me!\"",
pretest: "yarn build",
test: "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
build: "rollup -c",
lint: "eslint . --ext .js --cache",
prepare: "NODE_ENV=production yarn build",
posttest: "yarn lint"
};
var publishConfig = {
access: "public"
};
var devDependencies = {
"@babel/core": "7.23.3",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.23.3",
"@rollup/plugin-json": "6.0.1",
c8: "8.0.1",
"core-js": "3.33.2",
"eslint-plugin-ghost": "3.4.0",
mocha: "10.2.0",
rollup: "2.79.1",
"rollup-plugin-babel": "4.4.0",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0",
"rollup-plugin-polyfill-node": "0.12.0",
"rollup-plugin-replace": "2.2.0",
"rollup-plugin-terser": "7.0.2",
should: "13.2.3",
sinon: "17.0.1"
};
var dependencies = {
axios: "^1.0.0"
};
var gitHead = "4839d3f97de2120d98fa47677eed7591dfa20e64";
var packageInfo = {
name: name$1,
version: version,
repository: repository,
author: author,
license: license,
main: main,
"umd:main": "umd/content-api.min.js",
unpkg: unpkg,
module: module$1,
source: source,
files: files,
scripts: scripts,
publishConfig: publishConfig,
devDependencies: devDependencies,
dependencies: dependencies,
gitHead: gitHead
};
// @NOTE: this value is dynamically replaced based on browser/node environment
const USER_AGENT_DEFAULT = true;
const packageVersion = packageInfo.version;
const defaultAcceptVersionHeader = 'v5.0';
const supportedVersions = ['v2', 'v3', 'v4', 'v5', 'canary'];
const name = '@tryghost/content-api';
/**
* This method can go away in favor of only sending 'Accept-Version` headers
* once the Ghost API removes a concept of version from it's URLS (with Ghost v5)
*
* @param {string} [version] version in `v{major}` format
* @returns {string}
*/
const resolveAPIPrefix = (version) => {
let prefix;
// NOTE: the "version.match(/^v5\.\d+/)" expression should be changed to "version.match(/^v\d+\.\d+/)" once Ghost v5 is out
if (version === 'v5' || version === undefined || version.match(/^v5\.\d+/)) {
prefix = `/content/`;
} else if (version.match(/^v\d+\.\d+/)) {
const versionPrefix = /^(v\d+)\.\d+/.exec(version)[1];
prefix = `/${versionPrefix}/content/`;
} else {
prefix = `/${version}/content/`;
}
return prefix;
};
const defaultMakeRequest = ({url, method, params, headers}) => {
return axios[method](url, {
params,
paramsSerializer: (parameters) => {
return Object.keys(parameters).reduce((parts, k) => {
const val = encodeURIComponent([].concat(parameters[k]).join(','));
return parts.concat(`${k}=${val}`);
}, []).join('&');
},
headers
});
};
/**
*
* @param {Object} options
* @param {String} options.url
* @param {String} options.key
* @param {String} [options.ghostPath]
* @param {String|Boolean} options.version - a version string like v3, v4, v5 or boolean value identifying presence of Accept-Version header
* @param {String|Boolean} [options.userAgent] - value controlling the 'User-Agent' header should be sent with a request
* @param {Function} [options.makeRequest]
* @param {String} [options.host] Deprecated
*/
function GhostContentAPI({url, key, host, version, userAgent, ghostPath = 'ghost', makeRequest = defaultMakeRequest}) {
/**
* host parameter is deprecated
* @deprecated use "url" instead
*/
if (host) {
// eslint-disable-next-line
console.warn(`${name}: The 'host' parameter is deprecated, please use 'url' instead`);
if (!url) {
url = host;
}
}
if (this instanceof GhostContentAPI) {
return GhostContentAPI({url, key, version, userAgent, ghostPath, makeRequest});
}
if (version === undefined) {
throw new Error(`${name} Config Missing: 'version' is required. E.g. ${supportedVersions.join(',')}`);
}
let acceptVersionHeader;
if (typeof version === 'boolean') {
if (version === true) {
acceptVersionHeader = defaultAcceptVersionHeader;
}
version = undefined;
} else if (version && !supportedVersions.includes(version) && !(version.match(/^v\d+\.\d+/))) {
throw new Error(`${name} Config Invalid: 'version' ${version} is not supported`);
} else {
if (version === 'canary') {
// eslint-disable-next-line
console.warn(`${name}: The 'version' parameter has a deprecated format 'canary', please use 'v{major}.{minor}' format instead`);
acceptVersionHeader = defaultAcceptVersionHeader;
} else if (version.match(/^v\d+$/)) {
// eslint-disable-next-line
console.warn(`${name}: The 'version' parameter has a deprecated format 'v{major}', please use 'v{major}.{minor}' format instead`);
acceptVersionHeader = `${version}.0`;
} else {
acceptVersionHeader = version;
}
}
if (!url) {
throw new Error(`${name} Config Missing: 'url' is required. E.g. 'https://site.com'`);
}
if (!/https?:\/\//.test(url)) {
throw new Error(`${name} Config Invalid: 'url' ${url} requires a protocol. E.g. 'https://site.com'`);
}
if (url.endsWith('/')) {
throw new Error(`${name} Config Invalid: 'url' ${url} must not have a trailing slash. E.g. 'https://site.com'`);
}
if (ghostPath.endsWith('/') || ghostPath.startsWith('/')) {
throw new Error(`${name} Config Invalid: 'ghostPath' ${ghostPath} must not have a leading or trailing slash. E.g. 'ghost'`);
}
if (key && !/[0-9a-f]{26}/.test(key)) {
throw new Error(`${name} Config Invalid: 'key' ${key} must have 26 hex characters`);
}
if (userAgent === undefined) {
userAgent = USER_AGENT_DEFAULT;
}
const api = ['posts', 'authors', 'tags', 'pages', 'settings', 'tiers', 'newsletters', 'offers'].reduce((apiObject, resourceType) => {
function browse(options = {}, memberToken) {
return makeApiRequest(resourceType, options, null, memberToken);
}
function read(data, options = {}, memberToken) {
if (!data || !data.id && !data.slug) {
return Promise.reject(new Error(`${name} read requires an id or slug.`));
}
const params = Object.assign({}, data, options);
return makeApiRequest(resourceType, params, data.id || `slug/${data.slug}`, memberToken);
}
return Object.assign(apiObject, {
[resourceType]: {
read,
browse
}
});
}, {});
// Settings, tiers & newsletters only have browse methods, offers only has read
delete api.settings.read;
delete api.tiers.read;
delete api.newsletters.read;
delete api.offers.browse;
return api;
function makeApiRequest(resourceType, params, id, membersToken = null) {
if (!membersToken && !key) {
return Promise.reject(
new Error(`${name} Config Missing: 'key' is required.`)
);
}
delete params.id;
const headers = membersToken ? {
Authorization: `GhostMembers ${membersToken}`
} : {};
if (userAgent) {
if (typeof userAgent === 'boolean') {
headers['User-Agent'] = `GhostContentSDK/${packageVersion}`;
} else {
headers['User-Agent'] = userAgent;
}
}
if (acceptVersionHeader) {
headers['Accept-Version'] = acceptVersionHeader;
}
params = Object.assign({key}, params);
const apiUrl = `${url}/${ghostPath}/api${resolveAPIPrefix(version)}${resourceType}/${id ? id + '/' : ''}`;
return makeRequest({
url: apiUrl,
method: 'get',
params,
headers
})
.then((res) => {
if (!Array.isArray(res.data[resourceType])) {
return res.data[resourceType];
}
if (res.data[resourceType].length === 1 && !res.data.meta) {
return res.data[resourceType][0];
}
return Object.assign(res.data[resourceType], {meta: res.data.meta});
}).catch((err) => {
if (err.response && err.response.data && err.response.data.errors) {
const props = err.response.data.errors[0];
const toThrow = new Error(props.message);
const keys = Object.keys(props);
toThrow.name = props.type;
keys.forEach((k) => {
toThrow[k] = props[k];
});
toThrow.response = err.response;
// @TODO: remove in 2.0. We have enhanced the error handling, but we don't want to break existing implementations.
toThrow.request = err.request;
toThrow.config = err.config;
throw toThrow;
} else {
throw err;
}
});
}
}
export default GhostContentAPI;

252
src/Types/ghost.ts Normal file
View File

@ -0,0 +1,252 @@
export type ArrayOrValue<T> = T | T[];
export type Nullable<T> = T | null;
export interface Pagination {
page: number;
limit: number;
pages: number;
total: number;
next: Nullable<number>;
prev: Nullable<number>;
}
export interface Identification {
slug: string;
id: string;
}
export interface Metadata {
meta_title?: Nullable<string> | undefined;
meta_description?: Nullable<string> | undefined;
}
export interface Excerpt {
excerpt?: string | undefined;
custom_excerpt?: string | undefined;
}
export interface CodeInjection {
codeinjection_head?: Nullable<string> | undefined;
codeinjection_foot?: Nullable<string> | undefined;
}
/** Metadata for Facebook */
export interface Facebook {
og_image?: Nullable<string> | undefined;
og_title?: Nullable<string> | undefined;
og_description?: Nullable<string> | undefined;
}
export interface Twitter {
twitter_image?: Nullable<string> | undefined;
twitter_title?: Nullable<string> | undefined;
twitter_description?: Nullable<string> | undefined;
}
export interface SocialMedia extends Facebook, Twitter {
}
export interface Settings extends Metadata, CodeInjection, SocialMedia {
title?: string | undefined;
description?: string | undefined;
logo?: string | undefined;
icon?: string | undefined;
cover_image?: string | undefined;
facebook?: string | undefined;
twitter?: string | undefined;
lang?: string | undefined;
timezone?: string | undefined;
ghost_head?: Nullable<string> | undefined;
ghost_foot?: Nullable<string> | undefined;
navigation?:
| Array<{
label: string;
url: string;
}>
| undefined;
secondary_navigation?:
| Array<{
label: string;
url: string;
}>
| undefined;
url?: string | undefined;
}
export interface Author extends Identification, Metadata {
name?: string | undefined;
profile_image?: Nullable<string> | undefined;
cover_image?: Nullable<string> | undefined;
bio?: Nullable<string> | undefined;
website?: Nullable<string> | undefined;
location?: Nullable<string> | undefined;
facebook?: Nullable<string> | undefined;
twitter?: Nullable<string> | undefined;
url?: Nullable<string> | undefined;
count?: {
posts: number;
} | undefined;
}
export type TagVisibility = "public" | "internal";
export interface Tag extends Identification, Metadata, SocialMedia {
name?: string | undefined;
description?: Nullable<string> | undefined;
feature_image?: Nullable<string> | undefined;
visibility?: TagVisibility | undefined;
url?: string | undefined;
canonical_url?: Nullable<string> | undefined;
accent_color?: Nullable<string> | undefined;
count?: {
posts: number;
} | undefined;
}
export interface PostOrPage extends Identification, Excerpt, CodeInjection, Metadata, SocialMedia {
// Identification
uuid?: string | undefined;
comment_id?: string | undefined;
featured?: boolean | undefined;
// Post or Page
title?: string | undefined;
html?: Nullable<string> | undefined;
plaintext?: Nullable<string> | undefined;
// Image
feature_image?: Nullable<string> | undefined;
feature_image_alt?: Nullable<string> | undefined;
feature_image_caption?: Nullable<string> | undefined;
// Dates
created_at?: string | undefined;
updated_at?: Nullable<string> | undefined;
published_at?: Nullable<string> | undefined;
// Custom Template for posts and pages
custom_template?: Nullable<string> | undefined;
// Post or Page
page?: boolean | undefined;
// Reading time
reading_time?: number | undefined;
// Tags - Only shown when using Include param
tags?: Tag[] | undefined;
primary_tag?: Nullable<Tag> | undefined;
// Authors - Only shown when using Include Param
authors?: Author[] | undefined;
primary_author?: Nullable<Author> | undefined;
url?: string | undefined;
canonical_url?: Nullable<string> | undefined;
}
export type GhostData = PostOrPage | Author | Tag | Settings;
export type IncludeParam = "authors" | "tags" | "count.posts";
export type FieldParam = string;
export type FormatParam = "html" | "plaintext";
export type FilterParam = string;
export type LimitParam = number | string;
export type PageParam = number;
export type OrderParam = string;
export interface Params {
include?: ArrayOrValue<IncludeParam> | undefined;
fields?: ArrayOrValue<FieldParam> | undefined;
formats?: ArrayOrValue<FormatParam> | undefined;
filter?: ArrayOrValue<FilterParam> | undefined;
limit?: ArrayOrValue<LimitParam> | undefined;
page?: ArrayOrValue<PageParam> | undefined;
order?: ArrayOrValue<OrderParam> | undefined;
}
export interface BrowseFunction<T> {
(options?: Params, memberToken?: Nullable<string>): Promise<T>;
}
export interface ReadFunction<T> {
(
data: { id: Nullable<string> } | { slug: Nullable<string> },
options?: Params,
memberToken?: Nullable<string>,
): Promise<T>;
}
interface BrowseResults<T> extends Array<T> {
meta: { pagination: Pagination };
}
export interface PostsOrPages extends BrowseResults<PostOrPage> {
}
export interface Authors extends BrowseResults<Author> {
}
export interface Tags extends BrowseResults<Tag> {
}
export interface SettingsResponse extends Settings {
meta: any;
}
export interface GhostError {
errors: Array<{
message: string;
errorType: string;
}>;
}
export interface GhostContentAPIOptions {
url: string;
/**
* Version of GhostContentAPI
*
* Supported Versions: 'v2', 'v3', 'v4', 'v5.0', 'canary'
*/
version: "v2" | "v3" | "v4" | "v5.0" | "canary";
key: string;
/** @deprecated since version v2 */
host?: string | undefined;
/** @default "ghost" */
ghostPath?: string | undefined;
}
export interface GhostAPI {
posts: {
browse: BrowseFunction<PostsOrPages>;
read: ReadFunction<PostOrPage>;
};
authors: {
browse: BrowseFunction<Authors>;
read: ReadFunction<Author>;
};
tags: {
browse: BrowseFunction<Tags>;
read: ReadFunction<Tag>;
};
pages: {
browse: BrowseFunction<PostsOrPages>;
read: ReadFunction<PostOrPage>;
};
settings: {
browse: BrowseFunction<SettingsResponse>;
};
}
declare var GhostContentAPI: {
(options: GhostContentAPIOptions): GhostAPI;
new(options: GhostContentAPIOptions): GhostAPI;
};
export default GhostContentAPI;

63
src/ghost.ts Normal file
View File

@ -0,0 +1,63 @@
// IMPORT Ghost Types
import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from './Types/ghost';
// IMPORT Ghost API Client
import { api } from './Content-API/api-client';
// SET Include params
const include:ArrayOrValue<IncludeParam> = ['authors', 'tags'];
// Get Posts (General "ALL")
export const getGhostPosts = async () => {
const ghostPosts:PostsOrPages = await api.posts.browse({include,filter:'visibility:public'})
return ghostPosts; }
// Get Posts (Recent "setLimit?")
export const getGhostRecentPosts = async (setLimit?:ArrayOrValue<LimitParam>) => {
const ghostRecentPosts:PostsOrPages = await api.posts.browse({limit:setLimit?setLimit:"6",include,filter:'visibility:public'});
return ghostRecentPosts; }
// Get Posts (Featured "setLimit?")
export const getGhostFeaturedPosts = async (setLimit?:ArrayOrValue<LimitParam>) => {
const ghostFeaturedPosts:PostsOrPages = await api.posts.browse({limit:setLimit?setLimit:"1",include,filter:'featured:true'});
return ghostFeaturedPosts; }
// Get Post (By Slug)
export const getGhostPostbySlug = async (slug:Nullable<string>) => {
const ghostPostbySlug:PostOrPage = await api.posts.read({slug},{include});
return ghostPostbySlug; }
// Get Post (By Tag)
export const getGhostPostsbyTag = async (slug:Nullable<string>) => {
const ghostPostsbyTag:PostsOrPages = await api.posts.browse({filter:`tag:${slug}`,include});
return ghostPostsbyTag; }
// Get Tags (General "ALL")
export const getGhostTags = async () => {
const ghostTags:Tags = await api.tags.browse({include:`count.posts`});
return ghostTags; }
// Get Tag (By Slug)
export const getGhostTagbySlug = async (slug:Nullable<string>) => {
const ghostTagbySlug:Tag = await api.tags.read({slug},{include:`count.posts`});
return ghostTagbySlug; }
// Get Authors (General "ALL")
export const getGhostAuthors = async () => {
const ghostAuthors:Authors = await api.authors.browse();
return ghostAuthors; }
// Get Pages (ALL)
export const getGhostPages = async () => {
const ghostPages:PostsOrPages = await api.pages.browse();
return ghostPages; }
// Get Page (by Slug)
export const getGhostPage = async (slug:Nullable<string>) => {
const ghostPage:PostOrPage = await api.pages.read({slug});
return ghostPage; }
// Get Settings
export const getGhostSettings = async () => {
const ghostSettings:Settings = await api.settings.browse();
return ghostSettings; }