Add new Starlight-GhostCMS plugin #66
|
@ -7,5 +7,5 @@
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"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": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter playground dev",
|
"dev": "pnpm --filter playground dev",
|
||||||
|
"starlight:dev": "pnpm --filter starlight-playground dev",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:fix": "biome check --apply .",
|
"lint:fix": "biome check --apply .",
|
||||||
"ci:version": "pnpm changeset version",
|
"ci:version": "pnpm changeset version",
|
||||||
|
@ -15,7 +16,10 @@
|
||||||
"test:api": "pnpm --filter astro-ghostcms test",
|
"test:api": "pnpm --filter astro-ghostcms test",
|
||||||
"test:api:watch": "pnpm --filter astro-ghostcms test:watch",
|
"test:api:watch": "pnpm --filter astro-ghostcms test:watch",
|
||||||
"test:api:coverage": "pnpm --filter astro-ghostcms test:coverage",
|
"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": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.5.3",
|
"@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:
|
||||||
- "packages/*"
|
- "packages/*"
|
||||||
- "playground"
|
- "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