diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..92068a14
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist
+.vercel
+# generated types
+.astro/
+# dependencies
+node_modules/
+.snowpack/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# editor
+.idea
+package-lock.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 4253ed39..ad74383d 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,18 @@
# Welcome to Astro-GhostCMS
+Astro minimum Version: **Astro v4.0**
+
This addon uses the `@tryghost/content-api` and creates astro friendly functions to interface between ghost and astro.
*This package contains a independent copy of the tryghost content-api.js that is used to establish the connection so this package dose not depend on `@tryghost/content-api` package.*
-## Astro Integration Mode *(Planned for V2)*
+## Astro Integration Mode
-This is coming soon. And will allow the user to utilize the prebuilt astro-ghostcms-basetheme to be integrated through this main project. This feature is not yet setup or integrated. If you want a easy quick and simple deploy please copy this Template Repo, [astro-ghostcms-basetheme](https://github.com/MatthiesenXYZ/astro-ghostcms-basetheme) This will get you setup and ready to deploy in minutes using this addon!
+In this mode, the addon will not be just an API, but will be a full Route takeover, there is plans to add more themes in time, but for now there is only the base Casper theme based on Ghost's main Theme.
+
+```
+astro add @matthiesenxyz/astro-ghostcms
+```
## Manual Installation
@@ -17,19 +23,13 @@ npm i @matthiesenxyz/astro-ghostcms
Must create `.env` with the following:
```env
-CONTENT_API_KEY=
-CONTENT_API_URL=
+CONTENT_API_KEY=a33da3965a3a9fb2c6b3f63b48
+CONTENT_API_URL=https://ghostdemo.matthiesen.xyz
```
**When you deploy your install dont forget to set the above ENVIRONMENT VARIABLES!**
-Astro minimum Version: **Astro v4.0**
-
-Dependencies:
-- **Axios v1.0** *Will be auto installed*
-- **Typescript v5.3.3** *Will be auto installed*
-
-## Function Usage Examples:
+## Manual Function Usage Examples:
### getGhostPosts() - Get list of posts
diff --git a/astro.config.ts b/astro.config.ts
new file mode 100644
index 00000000..9d96843a
--- /dev/null
+++ b/astro.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "astro/config";
+import sitemap from "@astrojs/sitemap";
+import GhostCMS from './index';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [sitemap(), GhostCMS()],
+});
diff --git a/index.ts b/index.ts
index b19b164e..f78cb5d1 100644
--- a/index.ts
+++ b/index.ts
@@ -1,5 +1,51 @@
-// FUNCTION EXPORTS
-export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './src/functions';
+import type { AstroIntegration } from "astro"
-// TYPE EXPORTS
-export type { PostOrPage, ArrayOrValue, Author, Authors, BrowseFunction, CodeInjection, Excerpt, Facebook, FieldParam, FilterParam, FormatParam, GhostAPI, GhostContentAPIOptions, GhostData, GhostError, Identification, IncludeParam, LimitParam, Metadata, Nullable, OrderParam, PageParam, Pagination, Params, PostsOrPages, ReadFunction, Settings, SettingsResponse, SocialMedia, Tag, TagVisibility, Tags, Twitter } from './index.d';
\ No newline at end of file
+export default function GhostCMS(): AstroIntegration {
+ return {
+ name: 'astro-ghostcms',
+ hooks: {
+ 'astro:config:setup': async ({
+ injectRoute,
+ logger,
+ }) => {
+
+ injectRoute({
+ pattern: '/',
+ entrypoint: './src/routes/index.astro'
+ })
+
+ injectRoute({
+ pattern: '/[slug]',
+ entrypoint: './src/routes/[slug].astro'
+ })
+
+ injectRoute({
+ pattern: '/tags',
+ entrypoint: './src/routes/tags.astro'
+ })
+
+ injectRoute({
+ pattern: '/authors',
+ entrypoint: './src/routes/authors.astro'
+ })
+
+ injectRoute({
+ pattern: '/tag/[slug]',
+ entrypoint: './src/routes/tag/[slug].astro'
+ })
+
+ injectRoute({
+ pattern: '/author/[slug]',
+ entrypoint: './src/routes/author/[slug].astro'
+ })
+
+ injectRoute({
+ pattern: '/archives/[...page]',
+ entrypoint: './src/routes/archives/[...page].astro'
+ })
+
+ logger.info('Astro GhostCMS Plugin Loaded!')
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 98395732..d9c0ffe8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@matthiesenxyz/astro-ghostcms",
"description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content",
- "version": "1.0.6",
+ "version": "2.0.0-dev.1",
"author": "MatthiesenXYZ (https://matthiesen.xyz)",
"type": "module",
"license": "MIT",
@@ -14,14 +14,21 @@
},
"homepage": "https://github.com/MatthiesenXYZ/astro-ghostcms",
"exports": {
- ".": "./index.ts"
+ "./api": "./src/api/index.ts",
+ "./index.astro": "./src/routes/index.astro",
+ "./[slug].astro": "./src/routes/[slug].astro",
+ "./tags.astro": "./src/routes/tags.astro",
+ "./authors.astro": "./src/routes/authors.astro",
+ "./tag/[slug].astro": "./src/routes/tag/[slug].astro",
+ "./author/[slug].astro": "./src/routes/author/[slug].astro",
+ "./archives/[...page].astro": "./src/routes/archives/[...page].astro",
+ "./DefaultLayout": "./src/layouts/default.astro"
},
"main": "index.ts",
- "types": "index.d.ts",
+ "types": "src/api/ghosttypes.ts",
"files": [
"src",
- "index.ts",
- "index.d.ts"
+ "index.ts"
],
"keywords": [
"astro-component",
@@ -29,16 +36,33 @@
"ghost",
"ghostcms"
],
- "scripts": {},
- "devDependencies": {
- "astro": "^4.1.1"
- },
- "peerDependencies": {
- "astro": "^4.0.0"
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "typecheck": "astro check && tsc --noEmit",
+ "preview": "astro preview",
+ "format": "prettier --write .",
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint ."
},
"dependencies": {
"@astrojs/check": "^0.3.4",
"typescript": "^5.3.3",
- "axios": "^1.0.0"
+ "axios": "^1.0.0",
+ "astro-font": "^0.0.72",
+ "@astrojs/renderer-svelte": "0.5.2",
+ "@astrojs/rss": "^4.0.2",
+ "@astrojs/sitemap": "^3.0.4",
+ "@snowpack/plugin-dotenv": "^2.2.0",
+ "@typescript-eslint/eslint-plugin": "^6.5.0",
+ "@typescript-eslint/parser": "^6.5.0",
+ "astro": "^4.1.2",
+ "eslint": "^8.48.0",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-astro": "^0.29.0",
+ "prettier": "^3.0.3",
+ "prettier-plugin-astro": "^0.12.0",
+ "sass": "^1.66.1",
+ "tiny-invariant": "^1.3.1",
+ "vite": "^4.4.9"
}
}
diff --git a/src/functions.ts b/src/api/functions.ts
similarity index 99%
rename from src/functions.ts
rename to src/api/functions.ts
index f9500485..021b83ca 100644
--- a/src/functions.ts
+++ b/src/api/functions.ts
@@ -1,5 +1,5 @@
// IMPORT Ghost Types
-import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from '..';
+import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from './ghosttypes';
// IMPORT Ghost API Client
import api from './interface';
diff --git a/index.d.ts b/src/api/ghosttypes.ts
similarity index 100%
rename from index.d.ts
rename to src/api/ghosttypes.ts
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 00000000..1621bb3a
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1,5 @@
+// FUNCTION EXPORTS
+export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './functions';
+
+// TYPE EXPORTS
+export type { PostOrPage, ArrayOrValue, Author, Authors, BrowseFunction, CodeInjection, Excerpt, Facebook, FieldParam, FilterParam, FormatParam, GhostAPI, GhostContentAPIOptions, GhostData, GhostError, Identification, IncludeParam, LimitParam, Metadata, Nullable, OrderParam, PageParam, Pagination, Params, PostsOrPages, ReadFunction, Settings, SettingsResponse, SocialMedia, Tag, TagVisibility, Tags, Twitter } from './ghosttypes';
\ No newline at end of file
diff --git a/src/interface.js b/src/api/interface.js
similarity index 100%
rename from src/interface.js
rename to src/api/interface.js
diff --git a/src/tryghost-content-api.js b/src/api/tryghost-content-api.js
similarity index 100%
rename from src/tryghost-content-api.js
rename to src/api/tryghost-content-api.js
diff --git a/src/components/AuthorCard.astro b/src/components/AuthorCard.astro
new file mode 100644
index 00000000..23e4ab86
--- /dev/null
+++ b/src/components/AuthorCard.astro
@@ -0,0 +1,80 @@
+---
+import { getGhostImgPath } from "../utils";
+import type { Settings, Author } from "../api";
+export type Props = {
+ author: Author;
+ wide?: boolean;
+ addClass?: string;
+ settings: Settings;
+ showCover?: boolean;
+};
+const {
+ author,
+ wide = false,
+ settings,
+ showCover = true,
+} = Astro.props as Props;
+---
+
+
+
+
+ {author.profile_image && (
+
+
+
+ )}
+
+
+
+ {author.bio &&
{author.bio}
}
+
+
+ {author.count && author.count.posts && (
+
+ {author.count.posts > 0 ? `${author.count.posts} posts` : "No posts"}
+
+ )}
+
+
+
+
+
diff --git a/src/components/AuthorList.astro b/src/components/AuthorList.astro
new file mode 100644
index 00000000..ef4125e7
--- /dev/null
+++ b/src/components/AuthorList.astro
@@ -0,0 +1,75 @@
+---
+import { getGhostImgPath } from "../utils";
+import type { Settings, PostOrPage } from "../api";
+export type Props = {
+ post: PostOrPage;
+ settings: Settings;
+};
+const { post, settings } = Astro.props as Props;
+---
+
+
+ {post.authors && post.authors.map((author) => (
+ -
+ {author.profile_image ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ ))}
+
+
+
diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro
new file mode 100644
index 00000000..e484bc02
--- /dev/null
+++ b/src/components/BaseHead.astro
@@ -0,0 +1,86 @@
+---
+import { AstroFont } from "astro-font";
+import { ViewTransitions } from 'astro:transitions';
+import type { Settings } from "../api";
+
+export type Props = {
+ title: string;
+ description: string;
+ permalink?: string;
+ image?: string;
+ settings: Settings;
+};
+
+const { description, permalink, image, settings, title } = Astro.props as Props;
+---
+
+
+
+
+
+
+{title}
+
+
+{description && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+{permalink && }
+{description && }
+{image && }
+
+
+
+
+{permalink && }
+{description && }
+{image && }
+
+
+
+
+
+
diff --git a/src/components/FeatureImage.astro b/src/components/FeatureImage.astro
new file mode 100644
index 00000000..ddbddaa2
--- /dev/null
+++ b/src/components/FeatureImage.astro
@@ -0,0 +1,32 @@
+---
+import { getGhostImgPath } from "../utils";
+import type { Settings } from "../api";
+export type Props = {
+ image: string;
+ alt?: string;
+ caption?: string;
+ settings: Settings;
+ transitionName?: string;
+};
+const { image, alt, caption = "", settings, transitionName } = Astro.props as Props;
+---
+
+
+
+
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
new file mode 100644
index 00000000..2f4d2939
--- /dev/null
+++ b/src/components/Footer.astro
@@ -0,0 +1,112 @@
+---
+import type { Settings } from "../api";
+export type Props = {
+ settings: Settings;
+};
+const { settings } = Astro.props as Props;
+---
+
+
+
+
diff --git a/src/components/Header.astro b/src/components/Header.astro
new file mode 100644
index 00000000..996f8add
--- /dev/null
+++ b/src/components/Header.astro
@@ -0,0 +1,437 @@
+---
+import type { Settings } from "../api";
+export type Props = {
+ settings: Settings;
+};
+const { settings } = Astro.props as Props;
+---
+
+
+
+
+
+
+
+
diff --git a/src/components/HeroContent.astro b/src/components/HeroContent.astro
new file mode 100644
index 00000000..c52a8788
--- /dev/null
+++ b/src/components/HeroContent.astro
@@ -0,0 +1,131 @@
+---
+import { getGhostImgPath } from "../utils";
+import type { Settings } from "../api";
+export type Props = {
+ featureImg?: string;
+ mainTitle?: string;
+ settings: Settings;
+ description?: string;
+ addClass?: string;
+};
+const {
+ featureImg = "https://static.ghost.org/v4.0.0/images/publication-cover.jpg",
+ mainTitle = "",
+ settings,
+ description = "",
+} = Astro.props as Props;
+---
+
+
+
+
diff --git a/src/components/MainLayout.astro b/src/components/MainLayout.astro
new file mode 100644
index 00000000..d5ca8ab7
--- /dev/null
+++ b/src/components/MainLayout.astro
@@ -0,0 +1,23 @@
+---
+import Header from "./Header.astro";
+import Footer from "./Footer.astro";
+import type { Settings } from "../api";
+export type Props = {
+ settings: Settings;
+};
+const { settings } = Astro.props as Props;
+---
+
+
+
+
diff --git a/src/components/Page.astro b/src/components/Page.astro
new file mode 100644
index 00000000..857f8d92
--- /dev/null
+++ b/src/components/Page.astro
@@ -0,0 +1,569 @@
+---
+import FeatureImage from "./FeatureImage.astro";
+import type { Settings, PostOrPage } from "../api";
+export type Props = {
+ page: PostOrPage;
+ settings: Settings;
+ pageClass?: string;
+};
+const { page, settings, pageClass } = Astro.props as Props;
+---
+
+
+
+
+ {page.feature_image && (
+
+ )}
+
+
+
+
+
+
+
diff --git a/src/components/Paginator.astro b/src/components/Paginator.astro
new file mode 100644
index 00000000..cf0e572f
--- /dev/null
+++ b/src/components/Paginator.astro
@@ -0,0 +1,26 @@
+---
+import type { Page } from 'astro';
+const { page } = Astro.props as {page: Page};
+---
+
+
+ {page.url.prev && (
+
+ ← Prev
+
+ )}
+ {page.url.next && (
+
+ Next →
+
+ )}
+
+
+
diff --git a/src/components/Post.astro b/src/components/Post.astro
new file mode 100644
index 00000000..1554ef89
--- /dev/null
+++ b/src/components/Post.astro
@@ -0,0 +1,570 @@
+---
+import PostHero from "./PostHero.astro";
+import PostFooter from "./PostFooter.astro";
+import invariant from "tiny-invariant";
+import type {PostOrPage, PostsOrPages, Settings } from "../api";
+export type Props = {
+ post: PostOrPage;
+ settings: Settings;
+ postClass?: string;
+ posts: PostsOrPages;
+};
+const { post, settings, postClass, posts } = Astro.props as Props;
+invariant(settings, "Settings not found");
+---
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/PostFooter.astro b/src/components/PostFooter.astro
new file mode 100644
index 00000000..7203f4be
--- /dev/null
+++ b/src/components/PostFooter.astro
@@ -0,0 +1,125 @@
+---
+import PostPreview from "./PostPreview.astro";
+import type { Settings, PostOrPage, PostsOrPages } from "../api";
+export type Props = {
+ post: PostOrPage;
+ settings: Settings;
+ posts: PostsOrPages;
+};
+const { post, settings, posts } = Astro.props as Props;
+---
+
+
+
+
+
+
diff --git a/src/components/PostHero.astro b/src/components/PostHero.astro
new file mode 100644
index 00000000..49bb2c47
--- /dev/null
+++ b/src/components/PostHero.astro
@@ -0,0 +1,110 @@
+---
+import FeatureImage from "./FeatureImage.astro";
+import AuthorList from "./AuthorList.astro";
+import { formatDate } from "../utils";
+import type { Settings, PostOrPage } from "../api";
+export type Props = {
+ post: PostOrPage;
+ settings: Settings;
+};
+const { post, settings } = Astro.props as Props;
+---
+
+
+ {post.primary_tag && (
+
+ )}
+ {post.title}
+
+ {post.custom_excerpt && {post.custom_excerpt}
}
+
+
+
+
+ { post.authors && post.authors.length > 1 && (
+
+ {post.authors.map((author) => author.name).join(", ")}
+
+ )}
+
+ {post.created_at && (
+
+ )}
+ •
+ {post.reading_time} min read
+
+
+
+
+
+ {post.feature_image && (
+
+ )}
+
+
+
diff --git a/src/components/PostPreview.astro b/src/components/PostPreview.astro
new file mode 100644
index 00000000..527b1c1a
--- /dev/null
+++ b/src/components/PostPreview.astro
@@ -0,0 +1,301 @@
+---
+import { getGhostImgPath, formatDate } from "../utils";
+import AuthorList from "./AuthorList.astro";
+import type { Settings, PostOrPage, Tag } from "../api";
+export type Props = {
+ post: PostOrPage;
+ settings: Settings;
+ index?: number;
+ isHome?: boolean;
+};
+const { post, settings, index, isHome = false } = Astro.props as Props;
+---
+
+ `tag-${tag.slug}`)
+ .join(" ")} ${
+ isHome && post.feature_image && index == 0 ? "post-card-large" : ""
+ }`}
+>
+
+
+
+
+
+
+
diff --git a/src/components/PostPreviewList.astro b/src/components/PostPreviewList.astro
new file mode 100644
index 00000000..126908a7
--- /dev/null
+++ b/src/components/PostPreviewList.astro
@@ -0,0 +1,43 @@
+---
+import PostPreview from "./PostPreview.astro";
+import type { Settings, PostOrPage } from "../api";
+export type Props = {
+ posts: PostOrPage[];
+ settings: Settings;
+ isHome?: boolean;
+};
+const { posts, settings, isHome = false } = Astro.props as Props;
+---
+
+
+ {posts.map((post: PostOrPage, index: number) => (
+
+ ))}
+
+
+
diff --git a/src/components/TagCard.astro b/src/components/TagCard.astro
new file mode 100644
index 00000000..f200ad6f
--- /dev/null
+++ b/src/components/TagCard.astro
@@ -0,0 +1,51 @@
+---
+import { getGhostImgPath } from "../utils";
+import type { Settings, Tag } from "../api";
+export type Props = {
+ tag: Tag;
+ addClass?: string;
+ settings: Settings;
+};
+const { tag, addClass = "", settings } = Astro.props;
+---
+
+
+ {
+ tag.feature_image && (
+
+ )
+ }
+
+
{tag.name}
+ {tag.count && {tag.count.posts}+ Posts}
+
+
+
+
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 00000000..8c34fb45
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/src/layouts/default.astro b/src/layouts/default.astro
new file mode 100644
index 00000000..93bd703d
--- /dev/null
+++ b/src/layouts/default.astro
@@ -0,0 +1,60 @@
+---
+import type { Settings } from "../api";
+import BaseHead from "../components/BaseHead.astro";
+import MainLayout from "../components/MainLayout.astro";
+export type Props = {
+ content?: {
+ title: string|undefined;
+ description: string|undefined;
+ };
+ bodyClass?: string;
+ settings: Settings;
+};
+
+const { content, settings, bodyClass = "" } = Astro.props as Props;
+const ghostAccentColor = settings.accent_color;
+console.log(settings.accent_color)
+---
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/[slug].astro b/src/routes/[slug].astro
new file mode 100644
index 00000000..51fff808
--- /dev/null
+++ b/src/routes/[slug].astro
@@ -0,0 +1,46 @@
+---
+import type { InferGetStaticPropsType } from 'astro';
+import DefaultPageLayout from "../layouts/default.astro";
+import Page from "../components/Page.astro";
+import Post from "../components/Post.astro";
+import { getGhostSettings, getGhostPages, getGhostPosts } from "../api";
+import invariant from 'tiny-invariant';
+
+export async function getStaticPaths() {
+ const [posts, pages, settings] = await Promise.all([getGhostPosts(), await getGhostPages(), await getGhostSettings()]);
+ const allPosts = [...posts, ...pages];
+ return allPosts.map((post) => ({
+ params: { slug: post.slug },
+ props: { post, posts, settings },
+ }));
+}
+
+export type Props = InferGetStaticPropsType;
+
+const {post, posts, settings} = Astro.props as Props;
+invariant(settings, "Settings are required");
+const postClass = post.tags?.map((tag) => "tag-" + tag.slug).join(" ");
+const bodyClass = `post-template ${postClass}`;
+---
+
+
+ {
+ post.primary_author ? (
+
+
+ ) : (
+
+ )
+ }
+
+
+
diff --git a/src/routes/archives/[...page].astro b/src/routes/archives/[...page].astro
new file mode 100644
index 00000000..270cc410
--- /dev/null
+++ b/src/routes/archives/[...page].astro
@@ -0,0 +1,51 @@
+---
+import type { GetStaticPathsOptions, Page } from 'astro';
+import invariant from "tiny-invariant";
+import DefaultPageLayout from "../../layouts/default.astro";
+import PostPreviewList from "../../components/PostPreviewList.astro";
+import HeroContent from "../../components/HeroContent.astro";
+import Paginator from "../../components/Paginator.astro";
+import { getGhostSettings, getGhostPosts } from "../../api";
+import type { PostOrPage } from '../../api';
+
+export async function getStaticPaths({ paginate }:GetStaticPathsOptions) {
+ const posts = await getGhostPosts();
+ return paginate(posts, {
+ pageSize: 5,
+ });
+}
+
+export type Props = {
+ page: Page
+};
+
+const settings = await getGhostSettings();
+invariant(settings, "Settings are required");
+
+const title = settings.title;
+const description = settings.description;
+const { page } = Astro.props as Props;
+---
+
+
+
+ Archives
+
+
+
+
+
+
diff --git a/src/routes/author/[slug].astro b/src/routes/author/[slug].astro
new file mode 100644
index 00000000..34eb85e5
--- /dev/null
+++ b/src/routes/author/[slug].astro
@@ -0,0 +1,121 @@
+---
+import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro';
+import DefaultPageLayout from "../../layouts/default.astro";
+import PostPreviewList from "../../components/PostPreviewList.astro";
+import { getGhostPosts, getGhostAuthors, getGhostSettings } from "../../api";
+import invariant from "tiny-invariant";
+
+export async function getStaticPaths() {
+ const posts = await getGhostPosts();
+ const authors = await getGhostAuthors();
+ const settings = await getGhostSettings();
+
+ return authors.map((author) => {
+ const filteredPosts = posts.filter((post) =>
+ post.authors?.map((author) => author.slug).includes(author.slug)
+ );
+ return {
+ params: { slug: author.slug },
+ props: {
+ posts: filteredPosts,
+ settings,
+ author,
+ },
+ };
+ });
+}
+
+export type Params = InferGetStaticParamsType;
+export type Props = InferGetStaticPropsType;
+
+const { posts, settings, author } = Astro.props;
+invariant(settings, "Settings are required");
+const title = `Posts by author: ${author.name}`;
+const description = `All of the articles we've posted and linked so far under the author: ${author.name}`;
+---
+
+
+
+
diff --git a/src/routes/authors.astro b/src/routes/authors.astro
new file mode 100644
index 00000000..5b7dd7da
--- /dev/null
+++ b/src/routes/authors.astro
@@ -0,0 +1,44 @@
+---
+import DefaultPageLayout from "../layouts/default.astro";
+import AuthorCard from "../components/AuthorCard.astro";
+import { getGhostAuthors, getGhostSettings } from "../api";
+import invariant from "tiny-invariant";
+
+let title = "All Authors";
+let description = "All the authors";
+const authors = await getGhostAuthors();
+const settings = await getGhostSettings();
+invariant(settings, 'Settings not found');
+---
+
+
+
+
+
+ {settings.title}
+
+
+ Collection of Tags
+
+
+ {authors.map((author) => (
+
+
+
+ ))}
+
+
+
+
+
+
diff --git a/src/routes/index.astro b/src/routes/index.astro
new file mode 100644
index 00000000..a2c43d39
--- /dev/null
+++ b/src/routes/index.astro
@@ -0,0 +1,33 @@
+---
+import DefaultPageLayout from "../layouts/default.astro";
+import PostPreviewList from "../components/PostPreviewList.astro";
+import HeroContent from "../components/HeroContent.astro";
+import { getGhostPosts, getGhostSettings } from "../api";
+import invariant from "tiny-invariant";
+const posts = await getGhostPosts();
+const settings = await getGhostSettings();
+invariant(settings, "Settings not found");
+
+const title = settings.title;
+const description = settings.description;
+---
+
+
+
+
+
+
+
+
diff --git a/src/routes/tag/[slug].astro b/src/routes/tag/[slug].astro
new file mode 100644
index 00000000..e32c2a9d
--- /dev/null
+++ b/src/routes/tag/[slug].astro
@@ -0,0 +1,102 @@
+---
+import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro';
+import DefaultPageLayout from "../../layouts/default.astro";
+import PostPreview from "../../components/PostPreview.astro";
+import { getGhostPosts, getGhostTags, getGhostSettings } from "../../api";
+import { getGhostImgPath } from "../../utils";
+import invariant from "tiny-invariant";
+
+export async function getStaticPaths() {
+ const posts = await getGhostPosts();
+ const tags = await getGhostTags();
+ const settings = await getGhostSettings();
+
+ return tags.map((tag) => {
+ const filteredPosts = posts.filter((post) =>
+ post.tags?.map((tag) => tag.slug).includes(tag.slug)
+ );
+ return {
+ params: { slug: tag.slug },
+ props: {
+ posts: filteredPosts,
+ settings,
+ tag,
+ },
+ };
+ });
+}
+
+export type Params = InferGetStaticParamsType;
+export type Props = InferGetStaticPropsType;
+
+const { posts, settings, tag } = Astro.props;
+invariant(settings, 'Settings not found');
+const title = `Posts by Tag: ${tag.name}`;
+const description = `all of the articles we've posted and linked so far under the tag: ${tag.name}`;
+---
+
+
+
+
+
+
+ {tag.feature_image && (
+
+
+
+ )}
+
+
+
+
+
+ {tag.description
+ ? tag.description
+ : `A collection of ${tag.count?.posts || 0 } Post${
+ tag.count?.posts ?? 0 > 1 ? "s" : ""
+ }`}
+
+
+
+
+
+ {posts.map((post, index) => (
+
+ ))}
+
+
+
+
diff --git a/src/routes/tags.astro b/src/routes/tags.astro
new file mode 100644
index 00000000..e6a8547d
--- /dev/null
+++ b/src/routes/tags.astro
@@ -0,0 +1,48 @@
+---
+import DefaultPageLayout from "../layouts/default.astro";
+import TagCard from "../components/TagCard.astro";
+import { getGhostSettings, getGhostTags } from "../api";
+import invariant from 'tiny-invariant';
+
+
+let title = "All Tags";
+let description = "All the tags used so far...";
+
+const tags = await getGhostTags();
+const settings = await getGhostSettings();
+invariant(settings, "Settings not found");
+---
+
+
+
+
+
+ {settings.title}
+
+
+ Collection of Tags
+
+
+ {
+ tags.map((tag) => (
+
+
+
+ ))
+ }
+
+
+
+
+
+
diff --git a/src/styles/app.scss b/src/styles/app.scss
new file mode 100644
index 00000000..dcc688ee
--- /dev/null
+++ b/src/styles/app.scss
@@ -0,0 +1,390 @@
+/* Reset
+/* ---------------------------------------------------------- */
+
+@import "./reset";
+@import "./variables";
+
+/* 1. Global - Set up the things
+/* ---------------------------------------------------------- */
+/* Import CSS reset and base styles */
+:root {
+ /* Colours */
+ --color-green: #a4d037;
+ --color-yellow: #fecd35;
+ --color-red: #f05230;
+ --color-darkgrey: #15171a;
+ --color-midgrey: #738a94;
+ --color-lightgrey: #c5d2d9;
+ --color-wash: #e5eff5;
+ --color-darkmode: #151719;
+
+ /*
+ An accent color is also set by Ghost itself in
+ Ghost Admin > Settings > Brand
+
+ --ghost-accent-color: {value};
+
+ You can use this variale throughout your styles
+ */
+
+ /* Fonts */
+ --font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+ --font-serif: Georgia, Times, serif;
+ --font-mono: Menlo, Courier, monospace;
+}
+
+/* 2. Layout - Page building blocks
+/* ---------------------------------------------------------- */
+
+.viewport {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.site-content {
+ flex-grow: 1;
+}
+
+/* Full width page blocks */
+.outer {
+ position: relative;
+ padding: 0 4vmin;
+}
+
+/* Centered content container blocks */
+.inner {
+ margin: 0 auto;
+ max-width: 1200px;
+ width: 100%;
+}
+
+/* 9. Error Template
+/* ---------------------------------------------------------- */
+
+.error-content {
+ padding: 14vw 4vw 6vw;
+}
+
+.error-message {
+ padding-bottom: 10vw;
+ text-align: center;
+}
+
+.error-code {
+ margin: 0;
+ color: var(--ghost-accent-color);
+ font-size: 12vw;
+ line-height: 1em;
+ letter-spacing: -5px;
+}
+
+.error-description {
+ margin: 0;
+ color: var(--color-midgrey);
+ font-size: 3.2rem;
+ line-height: 1.3em;
+ font-weight: 400;
+}
+
+.error-link {
+ display: inline-block;
+ margin-top: 5px;
+}
+
+@media (min-width: 940px) {
+ .error-content .post-card {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+ }
+}
+
+@media (max-width: 800px) {
+ .error-content {
+ padding-top: 24vw;
+ }
+ .error-code {
+ font-size: 11.2rem;
+ }
+ .error-message {
+ padding-bottom: 16vw;
+ }
+ .error-description {
+ margin: 5px 0 0 0;
+ font-size: 1.8rem;
+ }
+}
+
+@media (max-width: 500px) {
+ .error-content {
+ padding-top: 28vw;
+ }
+ .error-message {
+ padding-bottom: 14vw;
+ }
+}
+
+.author-template .posts {
+ position: relative;
+ height: 100%;
+ display: grid;
+ grid-template-columns: 200px 1fr 1fr;
+ grid-gap: 4vmin;
+}
+
+.author-template .posts .post-feed {
+ grid-column: 2 / 4;
+ grid-template-columns: 1fr 1fr;
+}
+@media (max-width: 900px) {
+ .author-template .posts .post-feed {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 650px) {
+ .author-template .posts {
+ grid-template-columns: 1fr;
+ grid-gap: 0;
+ }
+ .author-template .posts .post-feed {
+ grid-column: 1 / auto;
+ }
+ .author-profile {
+ padding-right: 0;
+ }
+ .author-profile-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ }
+}
+
+/* 11. Site Footer
+/* ---------------------------------------------------------- */
+
+/* 12. Dark Mode
+/* ---------------------------------------------------------- */
+
+/* If you prefer a dark color scheme, you can enable dark mode
+by adding the following code to the Head section of "Code Injection"
+settings inside: Ghost Admin > Settings > Advanced
+
+
+
+Or you can just edit default.hbs and add the .dark-mode class directly
+to the html tag on the very first line of the file.
+
+ */
+
+html.dark-mode body {
+ color: rgba(255, 255, 255, 0.75);
+ background: var(--color-darkmode);
+}
+
+html.dark-mode img {
+ opacity: 0.9;
+}
+
+html.dark-mode .post-card,
+html.dark-mode .post-card:hover {
+ border-bottom-color: color-mod(var(--color-darkmode) l(+8%));
+}
+
+html.dark-mode .post-card-byline-content a {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+html.dark-mode .post-card-byline-content a:hover {
+ color: #fff;
+}
+
+html.dark-mode .post-card-image {
+ background: var(--color-darkmode);
+}
+
+html.dark-mode .post-card-title {
+ color: rgba(255, 255, 255, 0.85);
+}
+
+html.dark-mode .post-card-excerpt {
+ color: color-mod(var(--color-midgrey) l(+10%));
+}
+
+html.dark-mode .post-full-content {
+ background: var(--color-darkmode);
+}
+
+html.dark-mode .article-title {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+html.dark-mode .article-excerpt {
+ color: color-mod(var(--color-midgrey) l(+10%));
+}
+
+html.dark-mode .post-full-image {
+ background-color: color-mod(var(--color-darkmode) l(+8%));
+}
+
+html.dark-mode .article-byline {
+ border-top-color: color-mod(var(--color-darkmode) l(+15%));
+}
+
+html.dark-mode .article-byline-meta h4 a {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+html.dark-mode .article-byline-meta h4 a:hover {
+ color: #fff;
+}
+
+html.dark-mode .no-image .author-social-link a {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+html.dark-mode .gh-content h1,
+html.dark-mode .gh-content h2,
+html.dark-mode .gh-content h3,
+html.dark-mode .gh-content h4,
+html.dark-mode .gh-content h5,
+html.dark-mode .gh-content h6 {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+html.dark-mode .gh-content pre {
+ background: color-mod(var(--color-darkgrey) l(-8%));
+}
+
+html.dark-mode .gh-content :not(pre) > code {
+ background: color-mod(var(--color-darkgrey) l(+6%));
+ border-color: color-mod(var(--color-darkmode) l(+8%));
+ color: var(--color-wash);
+}
+
+html.dark-mode .post-full-content a {
+ color: #fff;
+ box-shadow: inset 0 -1px 0 #fff;
+}
+
+html.dark-mode .post-full-content strong {
+ color: #fff;
+}
+
+html.dark-mode .post-full-content em {
+ color: #fff;
+}
+
+html.dark-mode .post-full-content code {
+ color: #fff;
+ background: #000;
+}
+
+html.dark-mode hr {
+ border-top-color: color-mod(var(--color-darkmode) l(+8%));
+}
+
+html.dark-mode .post-full-content hr:after {
+ background: color-mod(var(--color-darkmode) l(+8%));
+ box-shadow: var(--color-darkmode) 0 0 0 5px;
+}
+
+html.dark-mode .gh-content figcaption {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+html.dark-mode .post-full-content table td:first-child {
+ background-image: linear-gradient(
+ to right,
+ var(--color-darkmode) 50%,
+ color-mod(var(--color-darkmode) a(0%)) 100%
+ );
+}
+
+html.dark-mode .post-full-content table td:last-child {
+ background-image: linear-gradient(
+ to left,
+ var(--color-darkmode) 50%,
+ color-mod(var(--color-darkmode) a(0%)) 100%
+ );
+}
+
+html.dark-mode .post-full-content table th {
+ color: rgba(255, 255, 255, 0.85);
+ background-color: color-mod(var(--color-darkmode) l(+8%));
+}
+
+html.dark-mode .post-full-content table th,
+html.dark-mode .post-full-content table td {
+ border: color-mod(var(--color-darkmode) l(+8%)) 1px solid;
+}
+
+html.dark-mode .post-full-content .kg-bookmark-container,
+html.dark-mode .post-full-content .kg-bookmark-container:hover {
+ color: rgba(255, 255, 255, 0.75);
+ box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
+}
+
+html.dark-mode .post-full-content input {
+ color: color-mod(var(--color-midgrey) l(-30%));
+}
+
+html.dark-mode .kg-bookmark-title {
+ color: #fff;
+}
+
+html.dark-mode .kg-bookmark-description {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+html.dark-mode .kg-bookmark-metadata {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+html.dark-mode .site-archive-header .no-image {
+ color: rgba(255, 255, 255, 0.9);
+ background: var(--color-darkmode);
+}
+
+html.dark-mode .subscribe-form {
+ border: none;
+ background: linear-gradient(
+ color-mod(var(--color-darkmode) l(-6%)),
+ color-mod(var(--color-darkmode) l(-3%))
+ );
+}
+
+html.dark-mode .subscribe-form-title {
+ color: rgba(255, 255, 255, 0.9);
+}
+
+html.dark-mode .subscribe-form p {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+html.dark-mode .subscribe-email {
+ border-color: color-mod(var(--color-darkmode) l(+6%));
+ color: rgba(255, 255, 255, 0.9);
+ background: color-mod(var(--color-darkmode) l(+3%));
+}
+
+html.dark-mode .subscribe-email:focus {
+ border-color: color-mod(var(--color-darkmode) l(+25%));
+}
+
+html.dark-mode .subscribe-form button {
+ opacity: 0.9;
+}
+
+html.dark-mode .subscribe-form .invalid .message-error,
+html.dark-mode .subscribe-form .error .message-error {
+ color: color-mod(var(--color-red) l(+5%) s(-5%));
+}
+
+html.dark-mode .subscribe-form .success .message-success {
+ color: color-mod(var(--color-green) l(+5%) s(-5%));
+}
diff --git a/src/styles/reset.scss b/src/styles/reset.scss
new file mode 100644
index 00000000..d911f735
--- /dev/null
+++ b/src/styles/reset.scss
@@ -0,0 +1,462 @@
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font: inherit;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+body {
+ line-height: 1;
+}
+ol,
+ul {
+ list-style: none;
+}
+blockquote,
+q {
+ quotes: none;
+}
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: "";
+ content: none;
+}
+table {
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+html {
+ box-sizing: border-box;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+}
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+a {
+ background-color: transparent;
+}
+a:active,
+a:hover {
+ outline: 0;
+}
+b,
+strong {
+ font-weight: bold;
+}
+i,
+em,
+dfn {
+ font-style: italic;
+}
+h1 {
+ margin: 0.67em 0;
+ font-size: 2em;
+}
+small {
+ font-size: 80%;
+}
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sup {
+ top: -0.5em;
+}
+sub {
+ bottom: -0.25em;
+}
+img {
+ border: 0;
+}
+svg:not(:root) {
+ overflow: hidden;
+}
+mark {
+ background-color: #fdffb6;
+}
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+ margin: 0; /* 3 */
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+}
+button {
+ overflow: visible;
+ border: none;
+}
+button,
+select {
+ text-transform: none;
+}
+button,
+html input[type="button"],
+/* 1 */
+input[type="reset"],
+input[type="submit"] {
+ cursor: pointer; /* 3 */
+
+ -webkit-appearance: button; /* 2 */
+}
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+input {
+ line-height: normal;
+}
+input:focus {
+ outline: none;
+}
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+input[type="search"] {
+ box-sizing: content-box; /* 2 */
+
+ -webkit-appearance: textfield; /* 1 */
+}
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+legend {
+ padding: 0; /* 2 */
+ border: 0; /* 1 */
+}
+textarea {
+ overflow: auto;
+}
+table {
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+td,
+th {
+ padding: 0;
+}
+
+/* ==========================================================================
+ Base styles: opinionated defaults
+ ========================================================================== */
+
+html {
+ font-size: 62.5%;
+
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+body {
+ color: #35373a;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 1.6rem;
+ line-height: 1.6em;
+ font-weight: 400;
+ font-style: normal;
+ letter-spacing: 0;
+ text-rendering: optimizeLegibility;
+ background: #fff;
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -moz-font-feature-settings: "liga" on;
+}
+
+::selection {
+ text-shadow: none;
+ background: #daf2fd;
+}
+
+hr {
+ position: relative;
+ display: block;
+ width: 100%;
+ margin: 2.5em 0 3.5em;
+ padding: 0;
+ height: 1px;
+ border: 0;
+ border-top: 1px solid #f0f0f0;
+}
+
+audio,
+canvas,
+iframe,
+img,
+svg,
+video {
+ vertical-align: middle;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+textarea {
+ resize: vertical;
+}
+
+::not(.gh-content) p,
+::not(.gh-content) ul,
+::not(.gh-content) ol,
+::not(.gh-content) dl,
+::not(.gh-content) blockquote {
+ margin: 0 0 1.5em 0;
+}
+
+ol,
+ul {
+ padding-left: 1.3em;
+ padding-right: 1.5em;
+}
+
+ol ol,
+ul ul,
+ul ol,
+ol ul {
+ margin: 0.5em 0 1em;
+}
+
+ul {
+ list-style: disc;
+}
+
+ol {
+ list-style: decimal;
+}
+
+ul,
+ol {
+ max-width: 100%;
+}
+
+li {
+ padding-left: 0.3em;
+ line-height: 1.6em;
+}
+
+li + li {
+ margin-top: 0.5em;
+}
+
+dt {
+ float: left;
+ margin: 0 20px 0 0;
+ width: 120px;
+ color: #daf2fd;
+ font-weight: 500;
+ text-align: right;
+}
+
+dd {
+ margin: 0 0 5px 0;
+ text-align: left;
+}
+
+blockquote {
+ margin: 1.5em 0;
+ padding: 0 1.6em 0 1.6em;
+ border-left: #daf2fd;
+}
+
+blockquote p {
+ margin: 0.8em 0;
+ font-size: 1.2em;
+ font-weight: 300;
+}
+
+blockquote small {
+ display: inline-block;
+ margin: 0.8em 0 0.8em 1.5em;
+ font-size: 0.9em;
+ opacity: 0.8;
+}
+/* Quotation marks */
+blockquote small:before {
+ content: "\2014 \00A0";
+}
+
+blockquote cite {
+ font-weight: bold;
+}
+blockquote cite a {
+ font-weight: normal;
+}
+
+a {
+ color: #15171a;
+ text-decoration: none;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin-top: 0;
+ line-height: 1.15;
+ font-weight: 600;
+ text-rendering: optimizeLegibility;
+ letter-spacing: -0.01em;
+}
+
+h1 {
+ margin: 0 0 0.5em 0;
+ font-size: 4.8rem;
+ font-weight: 700;
+ letter-spacing: -0.015em;
+}
+@media (max-width: 600px) {
+ h1 {
+ font-size: 2.8rem;
+ }
+}
+
+h2 {
+ margin: 1.5em 0 0.5em 0;
+ font-size: 2.8rem;
+ font-weight: 700;
+}
+@media (max-width: 600px) {
+ h2 {
+ font-size: 2.3rem;
+ }
+}
+
+h3 {
+ margin: 1.5em 0 0.5em 0;
+ font-size: 2.4rem;
+ font-weight: 600;
+}
+@media (max-width: 600px) {
+ h3 {
+ font-size: 1.7rem;
+ }
+}
+
+h4 {
+ margin: 1.5em 0 0.5em 0;
+ font-size: 2.2rem;
+}
+
+h5 {
+ margin: 1.5em 0 0.5em 0;
+ font-size: 2rem;
+}
+
+h6 {
+ margin: 1.5em 0 0.5em 0;
+ font-size: 1.8rem;
+}
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
new file mode 100644
index 00000000..b0774680
--- /dev/null
+++ b/src/styles/variables.scss
@@ -0,0 +1,8 @@
+$color-green: #a4d037;
+$color-yellow: #fecd35;
+$color-red: #f05230;
+$color-darkgrey: #15171a;
+$color-midgrey: #738a94;
+$color-lightgrey: #c5d2d9;
+$color-wash: #e5eff5;
+$color-darkmode: #151719;
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 00000000..145b2801
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,32 @@
+export const getGhostImgPath = (
+ baseUrl: string,
+ imgUrl: string,
+ width = 0
+): string => {
+ if (!imgUrl) return "";
+ if (!imgUrl.startsWith(baseUrl)) {
+ return imgUrl;
+ }
+ const relativePath = imgUrl.substring(`${baseUrl}/content/images`.length);
+ const cleanedBaseUrl = baseUrl.replace(/\/~/, "");
+ if (width && width > 0) {
+ return `${cleanedBaseUrl}/content/images/size/w${width}/${relativePath}`;
+ }
+ return `${cleanedBaseUrl}/content/images/${width}${relativePath}`;
+};
+
+export const truncate = (input: string, size: number): string =>
+ input.length > size ? `${input.substring(0, size)}...` : input;
+
+export const formatDate = (dateInput: string): string => {
+ const dateObject = new Date(dateInput);
+ return dateObject.toDateString();
+};
+
+export const uniqWith = (
+ arr: Array,
+ fn: (element: T, step: T) => number
+): Array =>
+ arr.filter(
+ (element, index) => arr.findIndex((step) => fn(element, step)) === index
+ );