Integrate ghost cms

Signed-off-by: baalajimaestro <me@baalajimaestro.me>
This commit is contained in:
baalajimaestro 2024-07-13 00:36:15 +05:30
parent d59a8e91a6
commit 7d4dbb00a0
Signed by: baalajimaestro
GPG key ID: F93C394FE9BBAFD5
9 changed files with 164 additions and 93 deletions

View file

@ -16,6 +16,7 @@
"@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6",
"@astrojs/vue": "^4.5.0",
"@tryghost/content-api": "^1.11.21",
"@unocss/reset": "^0.61.0",
"astro": "^4.11.3",
"nprogress": "^0.2.0",
@ -27,6 +28,7 @@
"@iconify/json": "^2.2.204",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@types/tryghost__content-api": "^1.3.16",
"@vueuse/core": "^10.11.0",
"bumpp": "^9.4.1",
"eslint": "^8.57.0",

View file

@ -20,6 +20,9 @@ importers:
'@astrojs/vue':
specifier: ^4.5.0
version: 4.5.0(astro@4.11.5(typescript@5.5.3))(rollup@4.18.1)(vite@5.3.3)(vue@3.4.31(typescript@5.5.3))
'@tryghost/content-api':
specifier: ^1.11.21
version: 1.11.21
'@unocss/reset':
specifier: ^0.61.0
version: 0.61.3
@ -48,6 +51,9 @@ importers:
'@types/nprogress':
specifier: ^0.2.3
version: 0.2.3
'@types/tryghost__content-api':
specifier: ^1.3.16
version: 1.3.16
'@vueuse/core':
specifier: ^10.11.0
version: 10.11.0(vue@3.4.31(typescript@5.5.3))
@ -846,6 +852,9 @@ packages:
peerDependencies:
eslint: '>=8.40.0'
'@tryghost/content-api@1.11.21':
resolution: {integrity: sha512-ozJqEMHDUO7D0SGxPbUnG+RvwBbzC3zmdGOW8cFvkcKzrhe7uOAmVKyq7/J3kRAM2QthTlmiDpqp7NEo9ZLlKg==}
'@types/acorn@4.0.6':
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
@ -915,6 +924,9 @@ packages:
'@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
'@types/tryghost__content-api@1.3.16':
resolution: {integrity: sha512-i3TryXx8ZoW5LC9dMDWLkCg6vFMeCh4tAvr0ijAdlEzIhoSC8q5RPMANNmkiDxDcNMRCByVMoT4+RfB5qeURZQ==}
'@types/unist@2.0.10':
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
@ -1301,6 +1313,12 @@ packages:
peerDependencies:
'@astrojs/compiler': '>=0.27.0'
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.7.2:
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
axobject-query@4.0.0:
resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==}
@ -1490,6 +1508,10 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@ -1582,6 +1604,10 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@ -2006,6 +2032,19 @@ packages:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'}
follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
@ -2657,6 +2696,14 @@ packages:
resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@ -2962,6 +3009,9 @@ packages:
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -4490,6 +4540,12 @@ snapshots:
- supports-color
- typescript
'@tryghost/content-api@1.11.21':
dependencies:
axios: 1.7.2
transitivePeerDependencies:
- debug
'@types/acorn@4.0.6':
dependencies:
'@types/estree': 1.0.5
@ -4570,6 +4626,8 @@ snapshots:
dependencies:
'@types/node': 17.0.45
'@types/tryghost__content-api@1.3.16': {}
'@types/unist@2.0.10': {}
'@types/unist@3.0.2': {}
@ -5197,6 +5255,16 @@ snapshots:
'@astrojs/compiler': 2.8.2
synckit: 0.9.0
asynckit@0.4.0: {}
axios@1.7.2:
dependencies:
follow-redirects: 1.15.6
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.0.0:
dependencies:
dequal: 2.0.3
@ -5392,6 +5460,10 @@ snapshots:
colorette@2.0.20: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@2.0.3: {}
commander@12.1.0: {}
@ -5458,6 +5530,8 @@ snapshots:
defu@6.1.4: {}
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
destr@2.0.3: {}
@ -6010,6 +6084,14 @@ snapshots:
flattie@1.1.1: {}
follow-redirects@1.15.6: {}
form-data@4.0.0:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
fs-extra@11.2.0:
dependencies:
graceful-fs: 4.2.11
@ -6980,6 +7062,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
@ -7293,6 +7381,8 @@ snapshots:
property-information@6.5.0: {}
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
queue-microtask@1.2.3: {}

View file

@ -2,10 +2,14 @@
interface Post {
id: string
slug: string
body: string
data: Record<string, any>
collection: string
render: any
title: string
html: string
excerpt: string
feature_image: string
published_at: string
reading_time: number
tags: Array<{ name: string, slug: string }>
primary_author: { name: string, profile_image: string }
}
withDefaults(defineProps<{
@ -19,17 +23,9 @@ function getDate(date: string) {
}
function getHref(post: Post) {
if (post.data.redirect)
return post.data.redirect
return `/posts/${post.slug}`
}
function getTarget(post: Post) {
if (post.data.redirect)
return '_blank'
return '_self'
}
function isSameYear(a: Date | string | number, b: Date | string | number) {
return a && b && getYear(a) === getYear(b)
}
@ -46,30 +42,26 @@ function getYear(date: Date | string | number) {
nothing here yet.
</div>
</template>
<li v-for="(post, index) in list " :key="post.data.title" mb-8>
<div v-if="!isSameYear(post.data.date, list[index - 1]?.data.date)" select-none relative h18 pointer-events-none>
<li v-for="(post, index) in list " :key="post.id" mb-8>
<div v-if="!isSameYear(post.published_at, list[index - 1]?.published_at)" select-none relative h18 pointer-events-none>
<span text-7em color-transparent font-bold text-stroke-2 text-stroke-hex-aaa op14 absolute top--0.2em>
{{ getYear(post.data.date) }}
{{ getYear(post.published_at) }}
</span>
</div>
<a text-lg lh-tight nav-link flex="~ col gap-2" :aria-label="post.data.title" :target="getTarget(post)" :href="getHref(post)">
<a text-lg lh-tight nav-link flex="~ col gap-2" :aria-label="post.title" :href="getHref(post)">
<div flex="~ col md:row gap-2 md:items-center">
<div flex="~ gap-2 items-center text-wrap">
<span lh-normal>
<i v-if="post.data.draft" text-base vertical-mid i-ri-draft-line />
{{ post.data.title }}
{{ post.title }}
</span>
</div>
<div opacity-50 text-sm ws-nowrap flex="~ gap-2 items-center">
<i v-if="post.data.redirect" text-base i-ri-external-link-line />
<i v-if="post.data.recording || post.data.video" text-base i-ri:film-line />
<time v-if="post.data.date" :datetime="getDate(post.data.date)">{{ post.data.date.split(',')[0] }}</time>
<span v-if="post.data.duration">· {{ post.data.duration }}</span>
<span v-if="post.data.tag">· {{ post.data.tag }}</span>
<span v-if="post.data.lang && post.data.lang.includes('zh')">· 中文</span>
<time v-if="post.published_at" :datetime="getDate(post.published_at)">{{ new Date(post.published_at).toLocaleDateString() }}</time>
<span v-if="post.reading_time">· {{ post.reading_time }} min read</span>
<span v-if="post.tags && post.tags.length">· {{ post.tags[0].name }}</span>
</div>
</div>
<div opacity-50 text-sm>{{ post.data.description }}</div>
<div opacity-50 text-sm>{{ post.excerpt }}</div>
</a>
</li>
</ul>

View file

@ -12,6 +12,7 @@ const { ...head } = Astro.props
<!doctype html>
<html lang="en">
<head>
<meta name="darkreader-lock">
<BaseHead {...head} />
<ViewTransitions />
</head>

View file

@ -2,23 +2,16 @@
import BaseLayout from '@/layouts/BaseLayout.astro'
import ListPosts from '@/components/ListPosts.vue'
import siteConfig from '@/site-config'
import { getPosts } from '@/utils/posts'
export async function getStaticPaths() {
const paths = siteConfig.page.blogLinks.map((nav) => {
const href = nav.href.replace('/blog', '')
return {
params: {
path: href === '' ? undefined : href.replace(/^\/+|\/+$/g, ''),
},
}
})
return paths
}
import { posts } from '@/utils/ghost'
const { path } = Astro.params
const posts = await getPosts(path)
const ghostPosts = await posts
.browse({
limit: 'all',
include: ['tags', 'authors'],
filter: path ? `tag:${path}` : undefined
})
function activeLink(pathname: string) {
return Astro.url.pathname.replace(/\/+/g, '/').replace(/\/$/, '') === pathname
@ -44,5 +37,5 @@ function activeLink(pathname: string) {
))
}
</div>
<ListPosts list={posts} />
<ListPosts list={ghostPosts} />
</BaseLayout>

View file

@ -2,7 +2,6 @@
import BaseLayout from '@/layouts/BaseLayout.astro'
import siteConfig from '@/site-config'
---
<BaseLayout description={siteConfig.description}>
<article class="prose">
<h1 class="text-title">Vitesse theme</h1>

View file

@ -1,47 +1,44 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
import { type CollectionPosts } from '@/types'
import { getPosts } from '@/utils/posts'
import { api } from '@/utils/ghost'
export async function getStaticPaths() {
const posts = await getPosts()
return posts.map((post) => {
return {
params: { slug: post.slug },
props: {
post,
},
}
})
const posts = await api.posts
.browse({
limit: 'all',
fields: ['slug', 'title']
})
.catch(err => {
console.error('Error fetching posts:', err);
return [];
});
return posts.map((post) => ({
params: { slug: post.slug },
props: { slug: post.slug },
}));
}
type Props = { post: CollectionPosts }
const { slug } = Astro.props;
const { post } = Astro.props
const { title, image, description, date, duration, tag } = post.data
const { Content } = await post.render()
function getDate(date: string) {
return new Date(date).toISOString()
let post;
post = await api.posts.read({ slug }, { include: ['tags', 'authors'] });
if (!post) {
return Astro.redirect('/404');
}
---
<BaseLayout title={title} description={description} pageType="article">
<BaseLayout title={post.title} description={post.excerpt} pageType="article">
<article class="prose">
<h1>{title}</h1>
<h1>{post.title}</h1>
<p op-50>
{date && <time datetime={getDate(date)}>{date.split(',')}</time>}
{duration && <span>· {duration}</span>}
{tag && <span>· {tag}</span>}
{post.published_at && <time datetime={post.published_at}>{new Date(post.published_at).toLocaleDateString()}</time>}
{post.reading_time && <span>· {post.reading_time} min read</span>}
{post.primary_tag && <span>· {post.primary_tag.name}</span>}
</p>
{
image && (
<p>
<img width="640" height="360" src={image.src} alt={image.alt || ''} />
</p>
)
}
<Content />
{post.feature_image && (
<img width="640" height="360" src={post.feature_image} alt={post.feature_image_alt || ''} />
)}
<div set:html={post.html}></div>
</article>
</BaseLayout>

View file

@ -47,14 +47,6 @@ export const siteConfig = {
text: 'Blog',
href: '/blog',
},
{
text: 'Notes',
href: '/blog/notes',
},
{
text: 'Talks',
href: '/blog/talks',
},
{
text: 'Projects',
href: '/projects',
@ -66,15 +58,7 @@ export const siteConfig = {
{
text: 'Blog',
href: '/blog',
},
{
text: 'Notes',
href: '/blog/notes',
},
{
text: 'Talks',
href: '/blog/talks',
},
}
],
},
footer: {

13
src/utils/ghost.ts Normal file
View file

@ -0,0 +1,13 @@
import GhostContentAPI from '@tryghost/content-api';
export const api = new GhostContentAPI({
url: import.meta.env.CONTENT_API_URL,
key: import.meta.env.CONTENT_API_KEY,
version: 'v5.0',
});
export const posts = api.posts
export const tags = api.tags
export const authors = api.authors
export const pages = api.pages
export const settings = api.settings