Add new Starlight-GhostCMS plugin #66
|
@ -7,5 +7,5 @@
|
|||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["playground"]
|
||||
"ignore": ["playground","starlight-playground"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@matthiesenxyz/starlight-ghostcms": patch
|
||||
---
|
||||
|
||||
Initial Public Release, Added Readme, Basics Working, Ready Set GO!
|
|
@ -7,6 +7,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter playground dev",
|
||||
"starlight:dev": "pnpm --filter starlight-playground dev",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --apply .",
|
||||
"ci:version": "pnpm changeset version",
|
||||
|
@ -15,7 +16,10 @@
|
|||
"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: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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.5.3",
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<img src="https://ghostdemo.matthiesen.xyz/content/images/size/w50/2024/01/logo-1.png" width="64px" />
|
||||
|
||||
# Welcome to Starlight-GhostCMS by [Astro-GhostCMS](https://astro-ghostcms.xyz)
|
||||
|
||||
Starlight-GhostCMS is a [Starlight](https://starlight.astro.build/) plugin to add a blog to your documentation site.
|
||||
|
||||
- Customizable Header link *(Defaults to "Blog")*
|
||||
- Pulls all the Posts from your GhostCMS install
|
||||
- Pulls all Authors & Featured Images and adds then to your Posts
|
||||
- Creates a custom section on your website linkable from the Header
|
||||
|
||||
For a full always up to date documentation please checkout [Our Website](https://astro-ghostcms.xyz)
|
||||
|
||||
- [Starlight Demo **COMING SOON**](#)
|
||||
- [Astro-GhostCMS Website](https://astro-ghostcms.xyz) Check the website for the most up-to-date Documentation!
|
||||
- [Ghost.org](https://ghost.org) Get your own Ghost[^1] Install
|
||||
|
||||
*Need help but don't have Github? Email us at [issues@astro-ghostcms.xyz](mailto:issues@astroghostcms.xyz) to create an issue here on github! Or join our [Discord](https://discord.gg/u7NZqUyeAR)*
|
||||
|
||||
Astro minimum Version: **Starlight v0.19 & Astro v4**
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites:
|
||||
|
||||
1. You will need to have a Starlight Website Setup. If you dont have one yet, you can follow the ["Getting Started"](https://starlight.astro.build/getting-started) guide in the Starlight docs to create one.
|
||||
2. You will need a GhostCMS server, and your `CONTENT_API_KEY` & `CONTENT_API_URL`. Your GhostCMS server must also support the `v5` Version of the GhostAPI. If you dont already have your Key and Url, you can find out how to get those on the Ghost docs [HERE](https://ghost.org/docs/content-api/)
|
||||
|
||||
### Install the plugin
|
||||
|
||||
Starlight GhostCMS is a Starlight [plugin](https://starlight.astro.build/reference/plugins/). Install it using your favorite package manager. Below is 3 examples of what to run. **CHOOSE ONE**.
|
||||
|
||||
```
|
||||
npm install @matthiesenxyz/starlight-ghostcms
|
||||
pnpm add @matthiesenxyz/starlight-ghostcms
|
||||
yarn add @matthiesenxyz/starlight-ghostcms
|
||||
```
|
||||
|
||||
### Configure the pluign
|
||||
|
||||
The Starlight GhostCMS plugin can be configured in your Starlight [configuration](https://starlight.astro.build/reference/configuration/#plugins) in the `astro.config.mjs` file.
|
||||
|
||||
```ts
|
||||
import starlight from '@astrojs/starlight'
|
||||
import { defineConfig } from 'astro/config'
|
||||
import starlightGhostCMS from '@matthiesenxyz/starlight-ghostcms';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
plugins: [starlightGhostCMS()],
|
||||
title: 'My Docs',
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Set your GhostCMS ENV Variables
|
||||
|
||||
You must also create 2 environment variables in a `.env` file with the following:
|
||||
|
||||
```env
|
||||
CONTENT_API_KEY=a33da3965a3a9fb2c6b3f63b48
|
||||
CONTENT_API_URL=https://ghostdemo.matthiesen.xyz
|
||||
```
|
||||
|
||||
**When you deploy your install dont forget to set the above ENVIRONMENT VARIABLES on your provider!**
|
||||
|
||||
|
||||
For more information and to see the docs please check our website: [Astro-GhostCMS.xyz](https://astro-ghostcms.xyz)
|
||||
|
||||
# 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.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
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'
|
||||
|
||||
export type { StarlightGhostConfig }
|
||||
|
||||
export default function starlightGhostCMS(userConfig?: StarlightGhostConfig): StarlightPlugin {
|
||||
const config: StarlightGhostConfig = validateConfig(userConfig)
|
||||
|
||||
return {
|
||||
name: '@matthiesenxyz/starlight-ghostcms-plugin',
|
||||
hooks: {
|
||||
setup({ addIntegration, config: starlightConfig, logger, updateConfig: updateStarlightConfig }) {
|
||||
updateStarlightConfig({
|
||||
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'
|
||||
})
|
||||
injectRoute({
|
||||
pattern: '/blog/[slug]',
|
||||
entrypoint: '@matthiesenxyz/starlight-ghostcms/routes/[slug].astro'
|
||||
})
|
||||
|
||||
updateConfig({
|
||||
vite: {
|
||||
plugins: [vitePluginStarlightGhostConfig(config)],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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`,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name": "@matthiesenxyz/starlight-ghostcms",
|
||||
"description": "Starlight GhostCMS plugin to allow easier importing of GhostCMS Content into your starlight website",
|
||||
"version": "0.0.1",
|
||||
"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": [
|
||||
"starlight",
|
||||
"starlight-plugin",
|
||||
"astro",
|
||||
"plugin",
|
||||
"blog",
|
||||
"content",
|
||||
"ghost",
|
||||
"ghostcms"
|
||||
],
|
||||
"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": "index.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"index.ts",
|
||||
"tsconfig.json",
|
||||
"types.d.ts"
|
||||
],
|
||||
"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/[slug].astro": "./src/routes/[slug].astro",
|
||||
"./schema": "./src/schemas/config.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/starlight": "0.19.0",
|
||||
"@ts-ghost/core-api": "5.1.2",
|
||||
"@ts-ghost/tsconfig": "workspace:*",
|
||||
"astro": "4.4.0",
|
||||
"vite": "^5.1.2",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-fetch-mock": "^0.2.2"
|
||||
},
|
||||
"peerdependencies": {
|
||||
"@astrojs/starlight": ">=0.19.0",
|
||||
"astro": ">=4.3.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import type { Author } from '../schemas/authors'
|
||||
|
||||
interface Props {
|
||||
author: Author
|
||||
}
|
||||
|
||||
const { author } = Astro.props
|
||||
|
||||
const isLink = author.website !== undefined
|
||||
const Element = isLink ? 'a' : 'div'
|
||||
---
|
||||
|
||||
<Element href={isLink ? author.website : undefined} class="author">
|
||||
{author.profile_image && <img alt={author.name} src={author.profile_image} />}
|
||||
<div class="text">
|
||||
<div class="name">{author.name}</div>
|
||||
{author.bio && <div class="title">{author.bio}</div>}
|
||||
</div>
|
||||
</Element>
|
||||
|
||||
<style>
|
||||
.author {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
line-height: var(--sl-line-height-headings);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--sl-text-base);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.author[href] .name {
|
||||
color: var(--sl-color-text-accent);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--sl-text-xs);
|
||||
color: var(--sl-color-text);
|
||||
}
|
||||
|
||||
.author[href]:hover .name {
|
||||
color: var(--sl-color-text);
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid var(--sl-color-gray-2);
|
||||
border-radius: 9999px;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import type { Post } from '../schemas/posts'
|
||||
import Author from './Author.astro'
|
||||
|
||||
interface Props {
|
||||
entry: Post
|
||||
}
|
||||
|
||||
const { entry } = Astro.props
|
||||
const { authors, published_at, created_at } = entry
|
||||
|
||||
const dateC = new Date(published_at?published_at:created_at)
|
||||
const dateISO = dateC.toISOString()
|
||||
const dateLocal = dateC
|
||||
.toLocaleDateString(
|
||||
"en-US", {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}
|
||||
)
|
||||
|
||||
const hasAuthors = authors !== undefined
|
||||
---
|
||||
|
||||
<div class="metadata not-content">
|
||||
<time datetime={dateISO}>
|
||||
{dateLocal}
|
||||
</time>
|
||||
{
|
||||
hasAuthors ? (
|
||||
<div class="authors">
|
||||
{authors.map((author: any) => (
|
||||
<Author {author} />
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
time {
|
||||
font-size: var(--sl-text-sm);
|
||||
}
|
||||
|
||||
.authors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import StarlightPage, { type StarlightPageProps as Props } from '@astrojs/starlight/components/StarlightPage.astro'
|
||||
---
|
||||
|
||||
<StarlightPage {...Astro.props}>
|
||||
<slot />
|
||||
</StarlightPage>
|
||||
|
||||
<style>
|
||||
:global(:is(.right-sidebar-panel, mobile-starlight-toc)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.main-frame) {
|
||||
padding-top: var(--sl-nav-height);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import Preview from './Preview.astro'
|
||||
import type { Post } from '../schemas/posts'
|
||||
|
||||
interface Props {
|
||||
entries: Post[]
|
||||
}
|
||||
|
||||
const { entries } = Astro.props
|
||||
---
|
||||
|
||||
<div class="posts">
|
||||
{entries.map((entry) => <Preview {entry} />)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.posts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import type { Post } from '../schemas/posts'
|
||||
import Metadata from './Metadata.astro'
|
||||
|
||||
interface Props {
|
||||
entry: Post
|
||||
}
|
||||
|
||||
const { entry } = Astro.props
|
||||
|
||||
const Excerpt = entry.excerpt
|
||||
---
|
||||
|
||||
<article class="preview">
|
||||
<header>
|
||||
<h2>
|
||||
<a href={`/blog/${entry.slug}`}>{entry.title}</a>
|
||||
</h2>
|
||||
<Metadata entry={entry} />
|
||||
</header>
|
||||
<div class="sl-markdown-content">
|
||||
{typeof Excerpt === 'string' ? Excerpt : entry.excerpt}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
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]
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import StarlightMarkdownContent from '@astrojs/starlight/components/MarkdownContent.astro'
|
||||
import type { Props } from '@astrojs/starlight/props'
|
||||
|
||||
import { isAnyBlogPostPage } from '../utils/page'
|
||||
import Metadata from '../components/Metadata.astro'
|
||||
import type { Post } from '../schemas/posts'
|
||||
|
||||
const isBlogPost = isAnyBlogPostPage(Astro.props.slug)
|
||||
let blogEntry: Post | undefined = undefined
|
||||
|
||||
---
|
||||
|
||||
<StarlightMarkdownContent {...Astro.props}>
|
||||
{isBlogPost && blogEntry ? <Metadata entry={blogEntry} /> : null}
|
||||
<slot />
|
||||
|
||||
{
|
||||
isBlogPost && blogEntry ? (
|
||||
<div class="post-footer">
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</StarlightMarkdownContent>
|
||||
|
||||
<style>
|
||||
.post-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-block-start: 2rem !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import StarlightSidebar from '@astrojs/starlight/components/Sidebar.astro'
|
||||
import type { Props } from '@astrojs/starlight/props'
|
||||
import config from 'virtual:starlight-ghost-config'
|
||||
import { isBlogPostPage, isBlogRoot } from '../utils/page'
|
||||
import { getAllPosts } from '../utils/api'
|
||||
|
||||
export async function getRecentBlogEntries(){
|
||||
const entries = await getAllPosts()
|
||||
return entries.slice(0, config.recentPostCount)
|
||||
}
|
||||
|
||||
export function checkpath(path: string){
|
||||
if ( path.slice(0, 5) === "/blog" ){
|
||||
return true
|
||||
} else { return false }
|
||||
}
|
||||
const isBlog = checkpath(Astro.url.pathname)
|
||||
const recentEntries = isBlog ? await getRecentBlogEntries() : []
|
||||
|
||||
const blogSidebar: Props['sidebar'] = isBlog
|
||||
? [
|
||||
{
|
||||
attrs: {},
|
||||
badge: undefined,
|
||||
href: '/blog',
|
||||
isCurrent: isBlogRoot(Astro.props.slug),
|
||||
label: 'All posts',
|
||||
type: 'link',
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
collapsed: false,
|
||||
entries: recentEntries.map((blogEntry) => ({
|
||||
attrs: {},
|
||||
badge: undefined,
|
||||
href: `/blog/${blogEntry.slug}`,
|
||||
isCurrent: isBlogPostPage(Astro.props.slug, `blog/${blogEntry.slug}`),
|
||||
label: blogEntry.title,
|
||||
type: 'link',
|
||||
})),
|
||||
label: 'Recent posts',
|
||||
type: 'group',
|
||||
},
|
||||
]
|
||||
: []
|
||||
---
|
||||
|
||||
{
|
||||
!isBlog && (
|
||||
<div class="md:sl-hidden">
|
||||
<a href="/blog">Blog</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<StarlightSidebar {...Astro.props} sidebar={isBlog ? blogSidebar : Astro.props.sidebar} />
|
||||
|
||||
<style>
|
||||
div {
|
||||
border-bottom: 1px solid var(--sl-color-gray-6);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
div a {
|
||||
color: var(--sl-color-white);
|
||||
font-size: var(--sl-text-lg);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
import type { Props } from "@astrojs/starlight/props";
|
||||
import AstrolightSiteTitle from "@astrojs/starlight/components/SiteTitle.astro";
|
||||
import config from 'virtual:starlight-ghost-config'
|
||||
---
|
||||
|
||||
<AstrolightSiteTitle {...Astro.props} />
|
||||
<div>
|
||||
<a href="/blog">{config.title}</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
align-items: center;
|
||||
gap: var(--sl-nav-gap);
|
||||
font-size: var(--sl-text-h4);
|
||||
font-weight: 600;
|
||||
color: var(--sl-color-text-accent);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
margin-left: 1rem;
|
||||
border-inline-start: 1px solid var(--sl-color-gray-5);
|
||||
display: flex;
|
||||
padding-inline-start: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--sl-color-text-accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import config from 'virtual:starlight-ghost-config'
|
||||
import { Image } from "astro:assets";
|
||||
import Page from '../components/Page.astro'
|
||||
import { getPageProps } from '../utils/page'
|
||||
import { getAllPosts } from '../utils/api'
|
||||
import Metadata from '../components/Metadata.astro'
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getAllPosts();
|
||||
return entries.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post, slug:post.slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props
|
||||
const pageProps = getPageProps(post.title)
|
||||
---
|
||||
|
||||
<Page {...pageProps}>
|
||||
{config.supportGhost && (
|
||||
<div id="pghost">Powered by <a href="https://ghost.org">Ghost</a></div>
|
||||
)}
|
||||
|
||||
<header>
|
||||
<Metadata entry={post} />
|
||||
{post.feature_image && (
|
||||
<figure>
|
||||
<Image
|
||||
src={post.feature_image}
|
||||
alt={post.feature_image_alt?post.feature_image_alt:""}
|
||||
title={post.feature_image_alt?post.feature_image_alt:""}
|
||||
width={1000}
|
||||
height={800}
|
||||
/>
|
||||
<figcaption>
|
||||
<Fragment set:html={post.feature_image_caption} />
|
||||
</figcaption>
|
||||
</figure>
|
||||
)}
|
||||
</header>
|
||||
<br />
|
||||
<Fragment set:html={post.html} />
|
||||
|
||||
<footer class="not-content">
|
||||
</footer>
|
||||
</Page>
|
||||
|
||||
<style>
|
||||
#pghost {
|
||||
color: gray;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
}
|
||||
#pghost a {
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
//import type { InferGetStaticPropsType } from 'astro'
|
||||
import config from 'virtual:starlight-ghost-config'
|
||||
|
||||
import Page from '../components/Page.astro'
|
||||
import Posts from '../components/Posts.astro'
|
||||
//import PrevNextLinks from '../components/PrevNextLinks.astro'
|
||||
import { getPageProps } from '../utils/page'
|
||||
import { getAllPosts } from '../utils/api'
|
||||
|
||||
const entries = await getAllPosts();
|
||||
|
||||
//const { entries, nextLink, prevLink } = Astro.props
|
||||
|
||||
const pageProps = getPageProps(config.title)
|
||||
---
|
||||
|
||||
<Page {...pageProps}>
|
||||
{config.supportGhost && (
|
||||
<div id="pghost">Powered by <a href="https://ghost.org">Ghost</a></div>
|
||||
)}
|
||||
<Posts {entries} />
|
||||
<footer class="not-content">
|
||||
<!--PrevNextLinks next={nextLink} prev={prevLink} /-->
|
||||
</footer>
|
||||
</Page>
|
||||
|
||||
<style>
|
||||
#pghost {
|
||||
color: gray;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
}
|
||||
#pghost a {
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
ghostIdentitySchema,
|
||||
ghostMetaSchema,
|
||||
ghostMetadataSchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
export const authorsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
name: z.string(),
|
||||
profile_image: z.string().nullable(),
|
||||
cover_image: z.string().nullable(),
|
||||
bio: z.string().nullable(),
|
||||
website: z.string().nullable(),
|
||||
location: z.string().nullable(),
|
||||
facebook: z.string().nullable(),
|
||||
twitter: z.string().nullable(),
|
||||
count: z
|
||||
.object({
|
||||
posts: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type Author = z.infer<typeof authorsSchema>;
|
||||
|
||||
export const ghostFetchAuthorsSchema = z.object({
|
||||
meta: ghostMetaSchema,
|
||||
authors: z.array(authorsSchema),
|
||||
});
|
||||
|
||||
export const authorsIncludeSchema = z.object({
|
||||
"count.posts": z.literal(true).optional(),
|
||||
});
|
||||
export type AuthorsIncludeSchema = z.infer<typeof authorsIncludeSchema>;
|
|
@ -0,0 +1,46 @@
|
|||
import { AstroError } from 'astro/errors'
|
||||
import { z } from 'astro/zod'
|
||||
|
||||
const configSchema = z
|
||||
.object({
|
||||
/**
|
||||
* The number of blog posts to display per page in the blog post list.
|
||||
*/
|
||||
postCount: z.number().min(1).default(5),
|
||||
/**
|
||||
* The number of recent blog posts to display in the sidebar.
|
||||
*/
|
||||
recentPostCount: z.number().min(1).default(10),
|
||||
/**
|
||||
* The title of the blog.
|
||||
*/
|
||||
title: z.string().default('Blog'),
|
||||
/**
|
||||
* Turn on and off "Powered by Ghost"
|
||||
*/
|
||||
supportGhost: z.boolean().default(true),
|
||||
})
|
||||
.default({})
|
||||
|
||||
export function validateConfig(userConfig: unknown): StarlightGhostConfig {
|
||||
const config = configSchema.safeParse(userConfig)
|
||||
|
||||
if (!config.success) {
|
||||
const errors = config.error.flatten()
|
||||
|
||||
throw new AstroError(
|
||||
`Invalid starlight-GhostCMS configuration:
|
||||
|
||||
${errors.formErrors.map((formError) => ` - ${formError}`).join('\n')}
|
||||
${Object.entries(errors.fieldErrors)
|
||||
.map(([fieldName, fieldErrors]) => ` - ${fieldName}: ${fieldErrors.join(' - ')}`)
|
||||
.join('\n')}
|
||||
`,
|
||||
"See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/matthiesenxyz/astro-ghostcms/issues/new/choose",
|
||||
)
|
||||
}
|
||||
|
||||
return config.data
|
||||
}
|
||||
|
||||
export type StarlightGhostConfig = z.infer<typeof configSchema>
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
ghostCodeInjectionSchema,
|
||||
ghostIdentitySchema,
|
||||
ghostMetadataSchema,
|
||||
ghostSocialMediaSchema,
|
||||
ghostVisibilitySchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
import { authorsSchema } from "./authors";
|
||||
import { tagsSchema } from "./tags";
|
||||
|
||||
const postsAuthorSchema = authorsSchema.extend({
|
||||
url: z.string().nullish(),
|
||||
});
|
||||
export const postsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
title: z.string(),
|
||||
html: z.string().catch(""),
|
||||
plaintext: z.string().nullish(),
|
||||
comment_id: z.string().nullable(),
|
||||
feature_image: z.string().nullable(),
|
||||
feature_image_alt: z.string().nullable(),
|
||||
feature_image_caption: z.string().nullable(),
|
||||
featured: z.boolean(),
|
||||
custom_excerpt: z.string().nullable(),
|
||||
...ghostCodeInjectionSchema.shape,
|
||||
...ghostSocialMediaSchema.shape,
|
||||
visibility: ghostVisibilitySchema,
|
||||
custom_template: z.string().nullable(),
|
||||
canonical_url: z.string().nullable(),
|
||||
authors: z.array(postsAuthorSchema).optional(),
|
||||
tags: z.array(tagsSchema).optional(),
|
||||
primary_author: postsAuthorSchema.nullish(),
|
||||
primary_tag: tagsSchema.nullish(),
|
||||
url: z.string(),
|
||||
excerpt: z.string().catch(""),
|
||||
reading_time: z.number().optional().default(0),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullish(),
|
||||
published_at: z.string().nullish(),
|
||||
access: z.boolean(),
|
||||
comments: z.boolean(),
|
||||
email_subject: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type Post = z.infer<typeof postsSchema>;
|
||||
|
||||
export const postsIncludeSchema = z.object({
|
||||
authors: z.literal(true).optional(),
|
||||
tags: z.literal(true).optional(),
|
||||
});
|
||||
export type PostsIncludeSchema = z.infer<typeof postsIncludeSchema>;
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
ghostCodeInjectionSchema,
|
||||
ghostIdentitySchema,
|
||||
ghostMetadataSchema,
|
||||
ghostSocialMediaSchema,
|
||||
ghostVisibilitySchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
export const tagsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
...ghostCodeInjectionSchema.shape,
|
||||
...ghostSocialMediaSchema.shape,
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
feature_image: z.string().nullable(),
|
||||
visibility: ghostVisibilitySchema,
|
||||
canonical_url: z.string().nullable(),
|
||||
accent_color: z.string().nullable(),
|
||||
url: z.string(),
|
||||
count: z
|
||||
.object({
|
||||
posts: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type Tag = z.infer<typeof tagsSchema>;
|
||||
|
||||
export const tagsIncludeSchema = z.object({
|
||||
"count.posts": z.literal(true).optional(),
|
||||
});
|
||||
export type TagsIncludeSchema = z.infer<typeof tagsIncludeSchema>;
|
|
@ -0,0 +1,146 @@
|
|||
import { TS_API } from "./content-api";
|
||||
import type { Page, Post } from "./content-api/schemas";
|
||||
import type { ContentAPICredentials } from './content-api/content-api'
|
||||
// LOAD ENVIRONMENT VARIABLES
|
||||
import { loadEnv } from "vite";
|
||||
import { invariant } from "./invariant.js";
|
||||
|
||||
const { CONTENT_API_KEY, CONTENT_API_URL } = loadEnv(
|
||||
"all",
|
||||
process.cwd(),
|
||||
"CONTENT_",
|
||||
);
|
||||
|
||||
invariant(
|
||||
CONTENT_API_KEY,
|
||||
"CONTENT_API_KEY Missing from .env"
|
||||
)
|
||||
invariant(
|
||||
CONTENT_API_URL,
|
||||
"CONTENT_API_URL Missing from .env"
|
||||
)
|
||||
|
||||
const key:ContentAPICredentials["key"] = CONTENT_API_KEY;
|
||||
const url:ContentAPICredentials["url"] = CONTENT_API_URL;
|
||||
const version = "v5.0";
|
||||
const api = new TS_API(url, key, version);
|
||||
|
||||
export const getAllAuthors = async () => {
|
||||
const results = await api.authors
|
||||
.browse()
|
||||
.include({ "count.posts": true })
|
||||
.fetch();
|
||||
if (!results.success) {
|
||||
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||
}
|
||||
return {
|
||||
authors: results.data,
|
||||
meta: results.meta,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPosts = async () => {
|
||||
const results = await api.posts
|
||||
.browse()
|
||||
.include({
|
||||
authors: true,
|
||||
tags: true,
|
||||
})
|
||||
.fetch();
|
||||
if (!results.success) {
|
||||
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||
}
|
||||
return {
|
||||
posts: results.data,
|
||||
meta: results.meta,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllPosts = async () => {
|
||||
const posts: Post[] = [];
|
||||
let cursor = await api.posts
|
||||
.browse()
|
||||
.include({
|
||||
authors: true,
|
||||
tags: true,
|
||||
})
|
||||
.paginate();
|
||||
if (cursor.current.success) posts.push(...cursor.current.data);
|
||||
while (cursor.next) {
|
||||
cursor = await cursor.next.paginate();
|
||||
if (cursor.current.success) posts.push(...cursor.current.data);
|
||||
}
|
||||
return posts;
|
||||
};
|
||||
|
||||
export const getSluggedPost = async (slug:string) => {
|
||||
const results = await api.posts
|
||||
.read({slug: slug})
|
||||
.include({
|
||||
authors: true,
|
||||
tags: true,
|
||||
}).fetch()
|
||||
|
||||
if (!results.success) {
|
||||
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||
}
|
||||
return {
|
||||
post: results.data,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllPages = async () => {
|
||||
const pages: Page[] = [];
|
||||
let cursor = await api.pages
|
||||
.browse()
|
||||
.include({
|
||||
authors: true,
|
||||
tags: true,
|
||||
})
|
||||
.paginate();
|
||||
if (cursor.current.success) pages.push(...cursor.current.data);
|
||||
while (cursor.next) {
|
||||
cursor = await cursor.next.paginate();
|
||||
if (cursor.current.success) pages.push(...cursor.current.data);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
export const getSettings = async () => {
|
||||
const res = await api.settings.fetch();
|
||||
if (res.success) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getAllTags = async () => {
|
||||
const results = await api.tags
|
||||
.browse()
|
||||
.include({ "count.posts": true })
|
||||
.fetch();
|
||||
if (!results.success) {
|
||||
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||
}
|
||||
return {
|
||||
tags: results.data,
|
||||
meta: results.meta,
|
||||
};
|
||||
};
|
||||
|
||||
export const getFeaturedPosts = async () => {
|
||||
const results = await api.posts
|
||||
.browse({ filter: "featured:true" })
|
||||
.include({
|
||||
authors: true,
|
||||
tags: true,
|
||||
})
|
||||
.fetch();
|
||||
if (!results.success) {
|
||||
throw new Error(results.errors.map((e) => e.message).join(", "));
|
||||
}
|
||||
return {
|
||||
posts: results.data,
|
||||
meta: results.meta,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
import { assert, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import TS_API from "./content-api";
|
||||
|
||||
describe("content-api", () => {
|
||||
let api: TS_API;
|
||||
beforeEach(() => {
|
||||
api = new TS_API("https://ghost.org", "59d4bf56c73c04a18c867dc3ba", "v5.0");
|
||||
});
|
||||
|
||||
test("content-api", () => {
|
||||
expect(api).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api shouldn't instantiate with an incorrect url", () => {
|
||||
assert.throws(() => {
|
||||
const api = new TS_API("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");
|
||||
api.settings;
|
||||
});
|
||||
});
|
||||
|
||||
test("content-api shouldn't instantiate with an incorrect version", () => {
|
||||
assert.throws(() => {
|
||||
const api = new TS_API(
|
||||
"https://ghost.org",
|
||||
"1efedd9db174adee2d23d982:4b74dca0219bad629852191af326a45037346c2231240e0f7aec1f9371cc14e8",
|
||||
// @ts-expect-error
|
||||
"v4.0",
|
||||
);
|
||||
api.settings;
|
||||
});
|
||||
});
|
||||
|
||||
test("content-api.posts", () => {
|
||||
expect(api.posts).toBeDefined();
|
||||
expect(api.posts.browse).toBeDefined();
|
||||
expect(api.posts.read).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api.pages", () => {
|
||||
expect(api.pages).toBeDefined();
|
||||
expect(api.pages.browse).toBeDefined();
|
||||
expect(api.pages.read).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api.tags", () => {
|
||||
expect(api.tags).toBeDefined();
|
||||
expect(api.tags.browse).toBeDefined();
|
||||
expect(api.tags.read).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api.tiers", () => {
|
||||
expect(api.tiers).toBeDefined();
|
||||
expect(api.tiers.browse).toBeDefined();
|
||||
expect(api.tiers.read).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api.authors", () => {
|
||||
expect(api.authors).toBeDefined();
|
||||
expect(api.authors.browse).toBeDefined();
|
||||
expect(api.authors.read).toBeDefined();
|
||||
// @ts-expect-error
|
||||
expect(api.authors.add).toBeUndefined();
|
||||
// @ts-expect-error
|
||||
expect(api.authors.edit).toBeUndefined();
|
||||
expect(api.authors).toBeDefined();
|
||||
});
|
||||
|
||||
test("content-api.settings", () => {
|
||||
expect(api.settings).toBeDefined();
|
||||
expect(api.settings.fetch).toBeDefined();
|
||||
// @ts-expect-error
|
||||
expect(api.settings.read).toBeUndefined();
|
||||
// @ts-expect-error
|
||||
expect(api.settings.browse).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export { default as TS_API } from "./content-api";
|
||||
export * from "./schemas";
|
||||
|
||||
export type {
|
||||
InferFetcherDataShape,
|
||||
InferResponseDataShape,
|
||||
BrowseParams,
|
||||
} from "@ts-ghost/core-api";
|
|
@ -0,0 +1,164 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import createFetchMock from "vitest-fetch-mock";
|
||||
|
||||
import TS_API from "../../content-api";
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
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");
|
||||
test(".browse() params shouldnt accept invalid params", () => {
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
const browse = api.authors.browse({ pp: 2 });
|
||||
expect(browse.getParams().browseParams).toStrictEqual({});
|
||||
});
|
||||
|
||||
test(".browse() 'order' params should ony accept fields values", () => {
|
||||
// @ts-expect-error - order should ony contain field
|
||||
expect(() => api.authors.browse({ order: "foo ASC" })).toThrow();
|
||||
// valid
|
||||
expect(
|
||||
api.authors.browse({ order: "name ASC" }).getParams().browseParams,
|
||||
).toStrictEqual({
|
||||
order: "name ASC",
|
||||
});
|
||||
expect(
|
||||
api.authors.browse({ order: "name ASC,slug DESC" }).getParams()
|
||||
.browseParams,
|
||||
).toStrictEqual({
|
||||
order: "name ASC,slug DESC",
|
||||
});
|
||||
expect(
|
||||
api.authors
|
||||
.browse({ order: "name ASC,slug DESC,location ASC" })
|
||||
.getParams().browseParams,
|
||||
).toStrictEqual({
|
||||
order: "name ASC,slug DESC,location ASC",
|
||||
});
|
||||
// @ts-expect-error - order should ony contain field (There is a typo in location)
|
||||
expect(() =>
|
||||
api.authors.browse({ order: "name ASC,slug DESC,locaton ASC" }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test(".browse() 'filter' params should ony accept valid field", () => {
|
||||
expect(() =>
|
||||
api.authors.browse({
|
||||
// @ts-expect-error - order should ony contain field
|
||||
filter: "foo:bar",
|
||||
}),
|
||||
).toThrow();
|
||||
expect(
|
||||
api.authors
|
||||
.browse({
|
||||
filter: "name:bar",
|
||||
})
|
||||
.getParams().browseParams,
|
||||
).toStrictEqual({
|
||||
filter: "name:bar",
|
||||
});
|
||||
expect(
|
||||
api.authors
|
||||
.browse({
|
||||
filter: "name:bar+slug:-test",
|
||||
})
|
||||
.getParams().browseParams,
|
||||
).toStrictEqual({
|
||||
filter: "name:bar+slug:-test",
|
||||
});
|
||||
});
|
||||
|
||||
test(".browse 'fields' argument should ony accept valid fields", () => {
|
||||
expect(
|
||||
api.authors
|
||||
.browse()
|
||||
.fields({
|
||||
// @ts-expect-error - order should ony contain field
|
||||
foo: true,
|
||||
})
|
||||
.getOutputFields(),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
api.authors.browse().fields({ location: true }).getOutputFields(),
|
||||
).toEqual(["location"]);
|
||||
expect(
|
||||
api.authors
|
||||
.browse()
|
||||
.fields({ name: true, website: true })
|
||||
.getOutputFields(),
|
||||
).toEqual(["name", "website"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authors resource mocked", () => {
|
||||
let api: TS_API;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TS_API(
|
||||
"https://my-ghost-blog.com",
|
||||
"59d4bf56c73c04a18c867dc3ba",
|
||||
"v5.0",
|
||||
);
|
||||
fetchMocker.enableMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("aouthors should be fetched correctly", async () => {
|
||||
const authors = api.authors;
|
||||
expect(authors).not.toBeUndefined();
|
||||
const browseQuery = authors
|
||||
.browse({
|
||||
page: 2,
|
||||
})
|
||||
.fields({
|
||||
name: true,
|
||||
id: true,
|
||||
});
|
||||
expect(browseQuery).not.toBeUndefined();
|
||||
expect(browseQuery.getOutputFields()).toStrictEqual(["name", "id"]);
|
||||
|
||||
fetchMocker.doMockOnce(
|
||||
JSON.stringify({
|
||||
authors: [
|
||||
{
|
||||
name: "foo",
|
||||
id: "eaoizdjoa1321123",
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 15,
|
||||
pages: 1,
|
||||
total: 1,
|
||||
next: null,
|
||||
prev: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await browseQuery.fetch();
|
||||
expect(fetchMocker).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMocker).toHaveBeenCalledWith(
|
||||
"https://my-ghost-blog.com/ghost/api/content/authors/?page=2&fields=name%2Cid&key=59d4bf56c73c04a18c867dc3ba",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Version": "v5.0",
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).not.toBeUndefined();
|
||||
if (result.success) {
|
||||
expect(result.data.length).toBe(1);
|
||||
expect(result.data[0].name).toBe("foo");
|
||||
expect(result.data[0].id).toBe("eaoizdjoa1321123");
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
ghostIdentitySchema,
|
||||
ghostMetaSchema,
|
||||
ghostMetadataSchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
export const authorsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
name: z.string(),
|
||||
profile_image: z.string().nullable(),
|
||||
cover_image: z.string().nullable(),
|
||||
bio: z.string().nullable(),
|
||||
website: z.string().nullable(),
|
||||
location: z.string().nullable(),
|
||||
facebook: z.string().nullable(),
|
||||
twitter: z.string().nullable(),
|
||||
count: z
|
||||
.object({
|
||||
posts: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type Author = z.infer<typeof authorsSchema>;
|
||||
|
||||
export const ghostFetchAuthorsSchema = z.object({
|
||||
meta: ghostMetaSchema,
|
||||
authors: z.array(authorsSchema),
|
||||
});
|
||||
|
||||
export const authorsIncludeSchema = z.object({
|
||||
"count.posts": z.literal(true).optional(),
|
||||
});
|
||||
export type AuthorsIncludeSchema = z.infer<typeof authorsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from "./authors";
|
|
@ -0,0 +1 @@
|
|||
export * from "./socials";
|
|
@ -0,0 +1,32 @@
|
|||
// Transformed to TypeScript, original Code by Ghost Foundation, License:
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2013-2022 Ghost Foundation
|
||||
|
||||
// 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.
|
||||
|
||||
export const twitter = (username: string) => {
|
||||
// Creates the canonical twitter URL without the '@'
|
||||
return `https://twitter.com/${username.replace(/^@/, "")}`;
|
||||
};
|
||||
|
||||
export const facebook = (username: string) => {
|
||||
// Handles a starting slash, this shouldn't happen, but just in case
|
||||
return `https://www.facebook.com/${username.replace(/^\//, "")}`;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export * from "./authors";
|
||||
export * from "./helpers";
|
||||
export * from "./pages";
|
||||
export * from "./posts";
|
||||
export * from "./settings";
|
||||
export * from "./tags";
|
||||
export * from "./tiers";
|
|
@ -0,0 +1 @@
|
|||
export * from "./pages";
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
ghostCodeInjectionSchema,
|
||||
ghostIdentitySchema,
|
||||
ghostMetadataSchema,
|
||||
ghostSocialMediaSchema,
|
||||
ghostVisibilitySchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
import { authorsSchema } from "../authors";
|
||||
import { tagsSchema } from "../tags";
|
||||
|
||||
const postsAuthorSchema = authorsSchema.extend({
|
||||
url: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const pagesSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
title: z.string(),
|
||||
html: z.string().catch(""),
|
||||
plaintext: z.string().nullish(),
|
||||
comment_id: z.string().nullable(),
|
||||
feature_image: z.string().nullable(),
|
||||
feature_image_alt: z.string().nullable(),
|
||||
feature_image_caption: z.string().nullable(),
|
||||
featured: z.boolean(),
|
||||
custom_excerpt: z.string().nullable(),
|
||||
...ghostCodeInjectionSchema.shape,
|
||||
...ghostSocialMediaSchema.shape,
|
||||
visibility: ghostVisibilitySchema,
|
||||
custom_template: z.string().nullable(),
|
||||
canonical_url: z.string().nullable(),
|
||||
authors: z.array(postsAuthorSchema).optional(),
|
||||
tags: z.array(tagsSchema).optional(),
|
||||
primary_author: postsAuthorSchema.nullish(),
|
||||
primary_tag: tagsSchema.nullish(),
|
||||
url: z.string(),
|
||||
excerpt: z.string().catch(""),
|
||||
reading_time: z.number().optional().default(0),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
published_at: z.string(),
|
||||
access: z.boolean(),
|
||||
comments: z.boolean(),
|
||||
email_subject: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type Page = z.infer<typeof pagesSchema>;
|
||||
|
||||
export const pagesIncludeSchema = z.object({
|
||||
authors: z.literal(true).optional(),
|
||||
tags: z.literal(true).optional(),
|
||||
});
|
||||
export type PagesIncludeSchema = z.infer<typeof pagesIncludeSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from "./posts";
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import TS_API from "../../content-api";
|
||||
import type { Post } from "./posts";
|
||||
|
||||
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");
|
||||
test(".browse() params shouldnt accept invalid params", () => {
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
const browse = api.posts.browse({ pp: 2 });
|
||||
expect(browse.getParams().browseParams).toStrictEqual({});
|
||||
|
||||
const outputFields = {
|
||||
slug: true,
|
||||
title: true,
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
foo: true,
|
||||
} satisfies { [k in keyof Post]?: true | undefined };
|
||||
|
||||
// biome-ignore lint/style/useConst: <explanation>
|
||||
let test = api.posts
|
||||
.browse()
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
.fields(outputFields);
|
||||
expect(test.getOutputFields()).toEqual(["slug", "title"]);
|
||||
|
||||
const fields = ["slug", "title", "foo"] as const;
|
||||
const unknownOriginFields = fields.reduce(
|
||||
(acc, k) => {
|
||||
acc[k as keyof Post] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as { [k in keyof Post]?: true | undefined },
|
||||
);
|
||||
const result = api.posts.browse().fields(unknownOriginFields);
|
||||
expect(result.getOutputFields()).toEqual(["slug", "title"]);
|
||||
});
|
||||
test(".browse() params, output fields declare const", () => {
|
||||
const outputFields = {
|
||||
slug: true,
|
||||
title: true,
|
||||
} satisfies { [k in keyof Post]?: true | undefined };
|
||||
|
||||
// biome-ignore lint/style/useConst: <explanation>
|
||||
let test = api.posts.browse().fields(outputFields);
|
||||
expect(test.getOutputFields()).toEqual(["slug", "title"]);
|
||||
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
expect(() => api.posts.browse({ filter: "slugg:test" })).toThrow();
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
expect(() =>
|
||||
api.posts.browse({ filter: "slug:test,foo:-[bar,baz]" }),
|
||||
).toThrow();
|
||||
expect(
|
||||
api.posts.browse({ filter: "slug:test,tags:-[bar,baz]" }),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
api.posts.browse({ filter: "slug:test,tags:[bar,baz]" }),
|
||||
).toBeDefined();
|
||||
// @ts-expect-error - shouldnt accept invalid params
|
||||
expect(() =>
|
||||
api.posts.browse({ filter: "slug:test,food:-[bar,baz]" }),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
ghostCodeInjectionSchema,
|
||||
ghostIdentitySchema,
|
||||
ghostMetadataSchema,
|
||||
ghostSocialMediaSchema,
|
||||
ghostVisibilitySchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
import { authorsSchema } from "../authors";
|
||||
import { tagsSchema } from "../tags";
|
||||
|
||||
const postsAuthorSchema = authorsSchema.extend({
|
||||
url: z.string().nullish(),
|
||||
});
|
||||
export const postsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
title: z.string(),
|
||||
html: z.string().catch(""),
|
||||
plaintext: z.string().nullish(),
|
||||
comment_id: z.string().nullable(),
|
||||
feature_image: z.string().nullable(),
|
||||
feature_image_alt: z.string().nullable(),
|
||||
feature_image_caption: z.string().nullable(),
|
||||
featured: z.boolean(),
|
||||
custom_excerpt: z.string().nullable(),
|
||||
...ghostCodeInjectionSchema.shape,
|
||||
...ghostSocialMediaSchema.shape,
|
||||
visibility: ghostVisibilitySchema,
|
||||
custom_template: z.string().nullable(),
|
||||
canonical_url: z.string().nullable(),
|
||||
authors: z.array(postsAuthorSchema).optional(),
|
||||
tags: z.array(tagsSchema).optional(),
|
||||
primary_author: postsAuthorSchema.nullish(),
|
||||
primary_tag: tagsSchema.nullish(),
|
||||
url: z.string(),
|
||||
excerpt: z.string().catch(""),
|
||||
reading_time: z.number().optional().default(0),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullish(),
|
||||
published_at: z.string().nullish(),
|
||||
access: z.boolean(),
|
||||
comments: z.boolean(),
|
||||
email_subject: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type Post = z.infer<typeof postsSchema>;
|
||||
|
||||
export const postsIncludeSchema = z.object({
|
||||
authors: z.literal(true).optional(),
|
||||
tags: z.literal(true).optional(),
|
||||
});
|
||||
export type PostsIncludeSchema = z.infer<typeof postsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from "./settings";
|
|
@ -0,0 +1,40 @@
|
|||
import { z } from "astro/zod";
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
logo: z.string().nullable(),
|
||||
icon: z.string().nullable(),
|
||||
accent_color: z.string().nullable(),
|
||||
cover_image: z.string().nullable(),
|
||||
facebook: z.string().nullable(),
|
||||
twitter: z.string().nullable(),
|
||||
lang: z.string(),
|
||||
timezone: z.string(),
|
||||
codeinjection_head: z.string().nullable(),
|
||||
codeinjection_foot: z.string().nullable(),
|
||||
navigation: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
),
|
||||
secondary_navigation: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
),
|
||||
meta_title: z.string().nullable(),
|
||||
meta_description: z.string().nullable(),
|
||||
og_image: z.string().nullable(),
|
||||
og_title: z.string().nullable(),
|
||||
og_description: z.string().nullable(),
|
||||
twitter_image: z.string().nullable(),
|
||||
twitter_title: z.string().nullable(),
|
||||
twitter_description: z.string().nullable(),
|
||||
members_support_address: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type Settings = z.infer<typeof settingsSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from "./tags";
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
ghostCodeInjectionSchema,
|
||||
ghostIdentitySchema,
|
||||
ghostMetadataSchema,
|
||||
ghostSocialMediaSchema,
|
||||
ghostVisibilitySchema,
|
||||
} from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
export const tagsSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
...ghostMetadataSchema.shape,
|
||||
...ghostCodeInjectionSchema.shape,
|
||||
...ghostSocialMediaSchema.shape,
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
feature_image: z.string().nullable(),
|
||||
visibility: ghostVisibilitySchema,
|
||||
canonical_url: z.string().nullable(),
|
||||
accent_color: z.string().nullable(),
|
||||
url: z.string(),
|
||||
count: z
|
||||
.object({
|
||||
posts: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type Tag = z.infer<typeof tagsSchema>;
|
||||
|
||||
export const tagsIncludeSchema = z.object({
|
||||
"count.posts": z.literal(true).optional(),
|
||||
});
|
||||
export type TagsIncludeSchema = z.infer<typeof tagsIncludeSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from "./tiers";
|
|
@ -0,0 +1,40 @@
|
|||
import { ghostIdentitySchema, ghostVisibilitySchema } from "@ts-ghost/core-api";
|
||||
import { z } from "astro/zod";
|
||||
|
||||
export const tiersSchema = z.object({
|
||||
...ghostIdentitySchema.shape,
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
active: z.boolean(),
|
||||
type: z.union([z.literal("free"), z.literal("paid")]),
|
||||
welcome_page_url: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
stripe_prices: z
|
||||
.array(z.number())
|
||||
.optional()
|
||||
.transform((v) => (v?.length ? v : [])),
|
||||
monthly_price: z
|
||||
.number()
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform((v) => (v ? v : null)),
|
||||
yearly_price: z
|
||||
.number()
|
||||
.nullable()
|
||||
.optional()
|
||||
.transform((v) => (v ? v : null)),
|
||||
benefits: z.array(z.string()),
|
||||
visibility: ghostVisibilitySchema,
|
||||
currency: z.string().nullish(),
|
||||
trial_days: z.number().default(0),
|
||||
});
|
||||
|
||||
export type Tier = z.infer<typeof tiersSchema>;
|
||||
|
||||
export const tiersIncludeSchema = z.object({
|
||||
monthly_price: z.literal(true).optional(),
|
||||
yearly_price: z.literal(true).optional(),
|
||||
benefits: z.literal(true).optional(),
|
||||
});
|
||||
export type TiersIncludeSchema = z.infer<typeof tiersIncludeSchema>;
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./api-functions";
|
||||
export * from "./content-api/schemas";
|
||||
export * from "./invariant";
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// Modified version of invariant script to allow tests
|
||||
const isProduction = false;
|
||||
const prefix: string = "Invariant failed";
|
||||
function invariant(condition: any, message?: string | (() => string)) {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
if (isProduction) {
|
||||
throw new Error(prefix);
|
||||
}
|
||||
const provided: string | undefined =
|
||||
typeof message === "function" ? message() : message;
|
||||
const value: string = provided ? `${prefix}: ${provided}` : prefix;
|
||||
return value;
|
||||
}
|
||||
|
||||
// TEST SECTION
|
||||
const testTrue = true;
|
||||
const testFalse = false;
|
||||
describe("test invariant", () => {
|
||||
it("Test `true` value", () => {
|
||||
invariant(testTrue, "This should not error");
|
||||
expect(null);
|
||||
});
|
||||
|
||||
it("Test `false` value", () => {
|
||||
invariant(testFalse, "This should Error");
|
||||
expect(String("Invariant failed"));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/** MIT License
|
||||
|
||||
Copyright (c) 2019 Alexander Reardon
|
||||
|
||||
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.
|
||||
*/
|
||||
const tinyinvariant = "merged";
|
||||
const isProduction: boolean = process.env.NODE_ENV === "production";
|
||||
const prefix: string = "Invariant failed";
|
||||
|
||||
/** Throw an error if the condition is false
|
||||
* @example
|
||||
* import { invariant } from '@matthiesenxyz/astro-ghostcms/api';
|
||||
* invariant(var, "var is false but its not supposed to be!")
|
||||
*/
|
||||
export function invariant(
|
||||
condition: any,
|
||||
message?: string | (() => string),
|
||||
): asserts condition {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
if (isProduction) {
|
||||
throw new Error(prefix);
|
||||
}
|
||||
|
||||
const provided: string | undefined =
|
||||
typeof message === "function" ? message() : message;
|
||||
|
||||
const value: string = provided ? `${prefix}: ${provided}` : prefix;
|
||||
throw new Error(value);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
export function isAnyBlogPage(slug: string) {
|
||||
return slug.match(/^blog(\/?$|\/.+\/?$)/) !== null
|
||||
}
|
||||
|
||||
export function isBlogRoot(slug: string) {
|
||||
return slug === 'blog'
|
||||
}
|
||||
|
||||
export function isAnyBlogPostPage(slug: string) {
|
||||
return slug.match(/^blog\/(?!(\d+\/?|tags\/.+)$).+$/) !== null
|
||||
}
|
||||
|
||||
export function isBlogPostPage(slug: string, postSlug: string) {
|
||||
return slug === postSlug
|
||||
}
|
||||
|
||||
export function isBlogTagsPage(slug: string, tag: string) {
|
||||
return slug === `blog/tags/${tag}`
|
||||
}
|
||||
|
||||
export function getPageProps(title: string): StarlightPageProps {
|
||||
return {
|
||||
frontmatter: {
|
||||
title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface StarlightPageProps {
|
||||
frontmatter: {
|
||||
title: string
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
declare module 'virtual:starlight/user-config' {
|
||||
const Config: import('@astrojs/starlight/types').StarlightConfig
|
||||
|
||||
export default Config
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"esModuleInterop": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
declare module 'virtual:starlight-ghost-config' {
|
||||
const StarlightGhostConfig: import('./src/schemas/config').StarlightGhostConfig
|
||||
|
||||
export default StarlightGhostConfig
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/// <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}"],
|
||||
watchExclude: [".*\\/node_modules\\/.*", ".*\\/build\\/.*"],
|
||||
exclude: ["node_modules", "dist", ".idea", ".git", ".cache"],
|
||||
},
|
||||
});
|
1066
pnpm-lock.yaml
1066
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,4 @@
|
|||
packages:
|
||||
- "packages/*"
|
||||
- "playground"
|
||||
- "starlight-playground"
|
|
@ -0,0 +1,21 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import starlightGhostCMS from '@matthiesenxyz/starlight-ghostcms';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'My Docs',
|
||||
plugins: [starlightGhostCMS()],
|
||||
social: {
|
||||
github: 'https://github.com/withastro/starlight',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
// Each item here is one entry in the navigation menu.
|
||||
{ label: 'Example Guide', link: '/guides/example/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
autogenerate: { directory: 'reference' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "starlight-playground",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.19.0",
|
||||
"@matthiesenxyz/starlight-ghostcms": "workspace:*",
|
||||
"astro": "^4.4.0",
|
||||
"sharp": "^0.32.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
After Width: | Height: | Size: 696 B |
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
|
@ -0,0 +1,6 @@
|
|||
import { defineCollection } from 'astro:content';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsSchema() }),
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Example Guide
|
||||
description: A guide in my new Starlight docs site.
|
||||
---
|
||||
|
||||
Guides lead a user through a specific task they want to accomplish, often with a sequence of steps.
|
||||
Writing a good guide requires thinking about what your users are trying to do.
|
||||
|
||||
## Further reading
|
||||
|
||||
- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diátaxis framework
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
title: Welcome to Starlight
|
||||
description: Get started building your docs site with Starlight.
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Congrats on setting up a new Starlight project!
|
||||
image:
|
||||
file: ../../assets/houston.webp
|
||||
actions:
|
||||
- text: Example Guide
|
||||
link: /guides/example/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: Read the Starlight docs
|
||||
link: https://starlight.astro.build
|
||||
icon: external
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Update content" icon="pencil">
|
||||
Edit `src/content/docs/index.mdx` to see this page change.
|
||||
</Card>
|
||||
<Card title="Add new content" icon="add-document">
|
||||
Add Markdown or MDX files to `src/content/docs` to create new pages.
|
||||
</Card>
|
||||
<Card title="Configure your site" icon="setting">
|
||||
Edit your `sidebar` and other config in `astro.config.mjs`.
|
||||
</Card>
|
||||
<Card title="Read the docs" icon="open-book">
|
||||
Learn more in [the Starlight Docs](https://starlight.astro.build/).
|
||||
</Card>
|
||||
</CardGrid>
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Example Reference
|
||||
description: A reference page in my new Starlight docs site.
|
||||
---
|
||||
|
||||
Reference pages are ideal for outlining how things work in terse and clear terms.
|
||||
Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you're documenting.
|
||||
|
||||
## Further reading
|
||||
|
||||
- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
Loading…
Reference in New Issue