new template test

This commit is contained in:
Adam Matthiesen 2024-01-27 22:29:13 -08:00
parent 3fb13d14c5
commit c95af3948d
44 changed files with 4640 additions and 5 deletions

View File

@ -6,4 +6,4 @@ export interface Context {
args: string[];
}
export type Template = "basic";
export type Template = ["basic","starterkit"];

View File

@ -1,6 +1,6 @@
{
"name": "@matthiesenxyz/create-astro-ghostcms",
"version": "0.0.1-dev25",
"version": "0.0.1-dev26",
"description": "Utility to quickly get started with our Integration and astro",
"type": "module",
"main": "./create-astro-ghostcms.mjs",

View File

@ -1,8 +1,9 @@
import arg from "arg";
import * as p from "@clack/prompts";
import c from 'picocolors';
import { exitPrompt, isPackageManager } from "./lib/utils.js";
import { createBasic } from "./runners/basic.js";
import c from 'picocolors';
import { createStarterKit } from "./runners/starterkit.js";
export async function main() {
@ -63,6 +64,10 @@ export async function main() {
value: "basic",
label: `${c.magenta('Basic')} - ${c.cyan(c.italic('Integration w/ Default Theme'))}`
},
{
value: "starterkit",
label: `${c.magenta('Starter Kit')} - ${c.cyan(c.italic('Integration in API-Only Mode with customizable theme'))}`
}
],
initialValue: "basic",
});
@ -85,6 +90,9 @@ export async function main() {
case "basic":
await createBasic(ctx).catch(console.error);
break;
case "starterkit":
await createStarterKit(ctx).catch(console.error);
break;
default:
throw new Error(c.red(`Unknown template: ${template}`));
}
@ -102,7 +110,7 @@ function getHelp() {
* @returns {template is Template}
*/
function isValidTemplate(template) {
return ["basic"].includes(template);
return ["basic","starterkit"].includes(template);
}
/**

View File

@ -1,8 +1,8 @@
import path from "node:path";
import fse from "fs-extra";
import c from 'picocolors';
import { execa } from "execa";
import * as p from "@clack/prompts";
import { execa } from "execa";
import { exitPrompt, getModulePaths, isPathname,
normalizePath, wait } from "../lib/utils.js";

View File

@ -0,0 +1,245 @@
import path from "node:path";
import fse from "fs-extra";
import c from 'picocolors';
import * as p from "@clack/prompts";
import { execa } from "execa";
import { exitPrompt, getModulePaths, isPathname,
normalizePath, wait } from "../lib/utils.js";
const runnerName = "starterkit";
/** @param {Context} ctx */
export async function createStarterKit(ctx) {
let { args, dryRun, initGitRepo, installDeps } = ctx;
const s = p.spinner();
let cwd = process.cwd();
// 1. Set up project directory
const project = await getProjectDetails(args[0], { cwd });
if (dryRun) {
await wait(1);
} else {
await fse.ensureDir(project.pathname);
}
// 2. Create the damned thing
cwd = project.pathname;
const relativePath = path.relative(process.cwd(), project.pathname);
s.start(`${c.yellow(`Creating a new Astro-GhostCMS project in ${relativePath}`)}`);
if (dryRun) {
await wait(2000);
} else {
await createApp(project.name, project.pathname, {
onError(error) {
s.stop(`${c.red('Failed to create new project')}`);
p.cancel();
console.error(error);
process.exit(1);
},
});
}
s.stop(`${c.green('New Astro-GhostCMS project')} '${project.name}' ${c.green('created')} 🚀`);
const fCheck = await p.group({
installDeps: () => p.confirm({
message: `${c.cyan('Install dependencies? (Recommended)')}`,
initialValue: false,
}),
//GitRepo: () => p.confirm({
// message: `${c.cyan('Initialize a Git repository?')} ${c.italic(c.gray("( Tip: This Option gets Stuck Press Enter Twice if you get no reponse! )"))}`,
// initialValue: false,
//}),
readyCheck: () => p.confirm({
message: `${c.bgYellow(c.black(c.bold(' CONFIRM: Press Enter Twice to continue or `Ctrl+C` to Cancel. ')))}`,
initialValue: true,
}),
},
{ onCancel: () => { exitPrompt(); } });
if(fCheck.readyCheck){;
// 3. Initialize git repo
if (initGitRepo) {
if (dryRun) {
await wait(1);
} else {
await exec("git", ["init"], { cwd });
}
p.log.success(c.green("Initialized Git repository"));
} else {
p.log.info(`${c.gray("Skipped Git initialization")}`);
}
const nextSteps = `If you didnt opt to install Dependencies dont forget to run: \n ${c.yellow('npm install')} / ${c.yellow('pnpm install')} / ${c.yellow('yarn install')} inside your project directory! \n \n ${c.bgYellow(c.black(c.bold(" Dont forget to modify your .env file for YOUR ghost install! ")))} `
// 4. Install dependencies
installDeps = installDeps ?? fCheck.installDeps;
const pm = ctx.pkgManager ?? "pnpm";
if (installDeps) {
s.start(`${c.cyan(`Installing dependencies with ${pm}`)} `);
if (dryRun) {
await wait(1);
} else {
await installDependencies(pm, { cwd });
}
s.stop(`${c.green(`Dependencies installed with ${pm}`)}`);
success()
} else {
p.log.info(`${c.gray('Skipped dependency installation')}`);
success()
}
async function success() {
p.note(nextSteps);
p.outro(c.green("Deployment Complete!"));
}
} else {
exitPrompt();
}
}
/**
*
* @param {string} projectName
* @param {string} projectPathname
* @param {{ onError: (err: unknown) => any }} opts
*/
async function createApp(projectName, projectPathname, { onError }) {
const { pathname } = getModulePaths(import.meta.url);
const templatesDir = path.resolve(pathname, "..", "..", "templates");
const sharedTemplateDir = path.join(templatesDir, "_shared");
const runnerTemplateDir = path.join(templatesDir, runnerName);
await fse.ensureDir(projectPathname);
// TODO: Detect if project directory is empty, otherwise we
// can't create a new project here.
await fse.copy(runnerTemplateDir, projectPathname);
// Copy misc files from shared
const filesToCopy = [
{
src: path.join(sharedTemplateDir, ".env"),
dest: path.join(projectPathname, ".env"),
},
];
await Promise.all(
filesToCopy.map(async ({ src, dest }) => await fse.copy(src, dest))
);
/** @type {Array<{ pathname: string; getUpdates: (contents: string) => string }>} */
const filesToUpdate = [
{
pathname: path.join(projectPathname, "package.json"),
getUpdates: updateProjectName,
},
];
await Promise.all(
filesToUpdate.map(async ({ pathname, getUpdates }) => {
const contents = await fse.readFile(pathname, "utf-8");
const updatedContents = getUpdates(contents);
await fse.writeFile(pathname, updatedContents, "utf-8");
})
);
/** @param {string} contents */
function updateProjectName(contents) {
return contents.replace(/{{PROJECT_NAME}}/g, projectName);
}
}
/**
* @param {string|undefined} projectNameInput
* @param {{ cwd: string }} opts
*/
async function getProjectDetails(projectNameInput, opts) {
let projectName = projectNameInput;
if (!projectName) {
const defaultProjectName = "my-astro-ghost";
let answer = await p.text({
message: `${c.cyan("Where would you like to create your project?")}`,
placeholder: `.${path.sep}${defaultProjectName}`,
});
if (p.isCancel(answer)) exitPrompt();
answer = answer?.trim();
projectName = answer || defaultProjectName;
}
/** @type {string} */
let pathname;
if (isPathname(projectName)) {
const dir = path.resolve(opts.cwd, path.dirname(normalizePath(projectName)));
projectName = toValidProjectName(path.basename(projectName));
pathname = path.join(dir, projectName);
} else {
projectName = toValidProjectName(projectName);
pathname = path.resolve(opts.cwd, projectName);
}
return {
name: projectName,
pathname,
};
}
/**
* @param {string} command
* @param {readonly string[]} args
* @param {{ cwd: string }} options
* @returns {Promise<{ code: number | null; signal: NodeJS.Signals | null }>}
*/
async function exec(command, args, options) {
const installExec = execa(command, ["init"], { ...options, stdio: "ignore" });
return new Promise((resolve, reject) => {
installExec.on("error", (error) => reject(error));
installExec.on("close", (code, signal) => resolve({ code, signal }));
});
}
/**
* @param {"npm" | "yarn" | "pnpm"} packageManager
* @param {{ cwd: string }} opts
* @returns
*/
async function installDependencies(packageManager, { cwd }) {
const installExec = execa(packageManager, ["install"], { cwd });
return new Promise((resolve, reject) => {
installExec.on("error", (error) => reject(error));
installExec.on("close", () => resolve());
});
}
/**
* @param {string} projectName
*/
function toValidProjectName(projectName) {
if (isValidProjectName(projectName)) {
return projectName;
}
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/^[._]/, "")
.replace(/[^a-z\d\-~]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "");
}
/**
* @param {string} projectName
*/
function isValidProjectName(projectName) {
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
projectName
);
}
/**
* @typedef {import("../../types.js").Template} Template
* @typedef {import("../../types.js").PackageManager} PackageManager
* @typedef {import("../../types.js").Context} Context
* @typedef {import("../../types.js").Serializable} Serializable
*/

View File

@ -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

View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Adam Matthiesen
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.

View File

@ -0,0 +1,15 @@
# Astro Starter Kit: Astro-GhostCMS Integration
To get started fork or clone this repo, and use one of the following commands:
```
npm install
pnpm install
yarn install
```
Then make sure you set your .env variables for DEV and environment variables on your server.
Its just astro! At this point you are free to start modifying what content/what your website will do, want to make it blog features only? go ahead!

View File

@ -0,0 +1,17 @@
import GhostCMS from '@matthiesenxyz/astro-ghostcms';
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// CHANGE THIS TO MATCH YOUR EXTERNAL DOMAIN
site: "http://localhost:4321",
integrations: [
// Includes GhostCMS API, @astrojs/rss, @astrojs/sitemap, and astro-robots-txt
GhostCMS({
// This Option Disables all default theme injection and allows DIY mode.
disableRouteInjection: true,
// Enable this to disable the extra console logs
disableConsoleOutput: false
})
]
});

View File

@ -0,0 +1,22 @@
{
"name": "{{PROJECT_NAME}}",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.4.1",
"@matthiesenxyz/astro-ghostcms": "^3.1.4",
"astro": "^4.2.4",
"typescript": "^5.3.3",
"astro-font": "^0.0.77"
},
"devDependencies": {
"sass": "^1.70.0"
}
}

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@ -0,0 +1,80 @@
---
import { getGhostImgPath } from "../utils";
import type { Settings, Author } from "@matthiesenxyz/astro-ghostcms/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;
---
<div class={`card author-card author-${author.slug} ${wide ? "wide" : ""}`}>
<figure class="author-card-cover">
{author.cover_image && showCover && (
<img
class="lazyautosizes lazyloaded"
data-srcset={`
${getGhostImgPath(settings.url, author.cover_image, 300)} 300w,
${getGhostImgPath(settings.url, author.cover_image, 600)} 600w
`}
srcset={`
${getGhostImgPath(settings.url, author.cover_image, 300)} 300w,
${getGhostImgPath(settings.url, author.cover_image, 600)} 600w
`}
data-sizes="auto"
data-src={getGhostImgPath(settings.url, author.cover_image, 300)}
src={getGhostImgPath(settings.url, author.cover_image, 300)}
alt={author.name}
sizes="316px"
/>
)}
</figure>
{author.profile_image && (
<a href={`/author/${author.slug}`} class="author-card-media">
<img
class="author-card-img"
data-srcset={`
${getGhostImgPath(settings.url, author.profile_image, 100)} 100w,
${getGhostImgPath(settings.url, author.profile_image, 300)} 300w
`}
srcset={`
${getGhostImgPath(settings.url, author.profile_image, 100)} 100w,
${getGhostImgPath(settings.url, author.profile_image, 300)} 300w
`}
data-sizes="auto"
data-src={getGhostImgPath(settings.url, author.profile_image, 300)}
src={getGhostImgPath(settings.url, author.profile_image, 300)}
alt={author.name}
sizes="316px"
/>
</a>
)}
<div class="author-card-content">
<div class="author-card-name">
<a href={`/author/${author.slug}`}>{author.name}</a>
</div>
{author.bio && <div class="author-card-descr">{author.bio}</div>}
<div class="author-card-details">
{author.count && author.count.posts && (
<div>
{author.count.posts > 0 ? `${author.count.posts} posts` : "No posts"}
</div>
)}
</div>
</div>
</div>
<style lang="scss">
</style>

View File

@ -0,0 +1,76 @@
---
import { getGhostImgPath } from "../utils";
import type { Settings, Post } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
post: Post;
settings: Settings;
};
const { post, settings } = Astro.props as Props;
---
<ul class="author-list">
{post.authors && post.authors.map((author) => (
<li class="author-list-item">
{author.profile_image ? (
<a href={`/author/${author.slug}`} class="author-avatar">
<img
class="author-profile-image"
src={getGhostImgPath(settings.url, author.profile_image, 100)}
alt={author.name}
/>
</a>
) : (
<a href={`/author/${author.slug}`} class="author-avatar">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M3.513 18.998C4.749 15.504 8.082 13 12 13s7.251 2.504 8.487 5.998C18.47 21.442 15.417 23 12 23s-6.47-1.558-8.487-4.002zM12 12c2.21 0 4-2.79 4-5s-1.79-4-4-4-4 1.79-4 4 1.79 5 4 5z"
fill="#FFF"
/>
</g>
</svg>
</a>
)}
</li>
))}
</ul>
<style lang="scss">
@use "sass:color";
@import "../styles/variables";
.author-avatar {
display: block;
overflow: hidden;
margin: 0 -4px;
width: 50px;
height: 50px;
border: #fff 2px solid;
border-radius: 100%;
transition: all 0.5s cubic-bezier(0.4, 0.01, 0.165, 0.99) 700ms;
}
.author-list {
display: flex;
flex-wrap: wrap;
margin: 0 0 0 4px;
padding: 0;
padding-inline-end: 1rem;
list-style: none;
}
.author-list-item {
position: relative;
flex-shrink: 0;
margin: 0;
padding: 0;
}
.author-profile-image {
display: block;
width: 100%;
height: 100%;
background: color.scale($color-lightgrey, $lightness: +10%);
border-radius: 100%;
object-fit: cover;
}
</style>

View File

@ -0,0 +1,86 @@
---
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
import { ViewTransitions } from 'astro:transitions';
import { AstroFont } from "astro-font";
export type Props = {
title: string;
description: string;
permalink?: string;
image?: string;
settings: Settings;
};
const { description, permalink, image, settings, title } = Astro.props as Props;
---
<AstroFont
config={[
{
src: [],
name: "Inter",
preload: false,
display: "swap",
selector: "html",
fallback: "sans-serif",
googleFontsURL: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
},
]}
/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<ViewTransitions />
<meta name="title" content={title} />
{description && <meta name="description" content={description} />}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href={settings.icon} />
<link rel="shortcut icon" type="image/png" sizes="16x16" href={settings.icon} />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
<!-- Open Graph Tags (Facebook) -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
{permalink && <meta property="og:url" content={permalink} />}
{description && <meta property="og:description" content={description} />}
{image && <meta property="og:image" content={image} />}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
{permalink && <meta property="twitter:url" content={permalink} />}
{description && <meta property="twitter:description" content={description} />}
{image && <meta property="twitter:image" content={image} />}
<!-- Link to the global style, or the file that imports constructs -->
<link
rel="preload"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css"
as="style"
/>
<link
rel="preload"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"
as="script"
/>
<link
rel="preload stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
crossorigin
/>
<script
async
defer
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"
>
</script>

View File

@ -0,0 +1,32 @@
---
import { getGhostImgPath } from "../utils";
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
image: string;
alt?: string;
caption?: string;
settings: Settings;
transitionName?: string;
};
const { image, alt, caption = "", settings, transitionName } = Astro.props as Props;
---
<figure class="article-image">
<img
srcset={`
${getGhostImgPath(settings.url, image, 300)} 300w,
${getGhostImgPath(settings.url, image, 600)} 600w,
${getGhostImgPath(settings.url, image, 1000)} 1000w,
${getGhostImgPath(settings.url, image, 2000)} 2000w
`}
sizes="(min-width: 1400px) 1400px, 92vw"
src={getGhostImgPath(settings.url, image, 2000)}
alt={alt}
transition:name={transitionName}
/>
{caption && <figcaption><Fragment set:html={caption}></figcaption>}
</figure>
<style lang="scss">
</style>

View File

@ -0,0 +1,112 @@
---
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
settings: Settings;
};
const { settings } = Astro.props as Props;
---
<footer class="site-footer outer">
<div class="inner">
<section class="copyright">
<a href="/">{settings.title}</a> &copy; 2021
</section>
<nav class="site-footer-nav">
{settings.secondary_navigation.length > 0 && (
<ul class="nav secondary">
{settings.secondary_navigation.map(({ label, url }) => (
<li class="">
<a href={url}>{label}</a>
</li>
))}
</ul>
)}
</nav>
<div>
<a href="https://ghost.org/" target="_blank" rel="noopener"
>Powered by Ghost
</a>
</div>
</div>
</footer>
<style lang="scss">
.site-footer {
position: relative;
margin: 40px 0 0 0;
padding: 40px 4vmin 140px;
color: #fff;
background: var(--color-darkgrey);
}
.site-footer .inner {
display: grid;
grid-gap: 40px;
grid-template-columns: auto 1fr auto;
color: rgba(255, 255, 255, 0.7);
font-size: 1.3rem;
}
.site-footer .copyright a {
color: #fff;
letter-spacing: -0.015em;
font-weight: 500;
}
.site-footer a {
color: rgba(255, 255, 255, 0.7);
}
.site-footer a:hover {
color: rgba(255, 255, 255, 1);
text-decoration: none;
}
.site-footer-nav ul {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 0 0 20px;
padding: 0;
list-style: none;
}
.site-footer-nav li {
display: inline-flex;
align-items: center;
padding: 0;
margin: 0;
line-height: 2em;
}
.site-footer-nav a {
position: relative;
display: inline-flex;
align-items: center;
margin-left: 10px;
}
.site-footer-nav li:not(:first-child) a:before {
content: "";
display: block;
width: 2px;
height: 2px;
margin: 0 10px 0 0;
background: #fff;
border-radius: 100%;
}
@media (max-width: 800px) {
.site-footer .inner {
max-width: 500px;
grid-template-columns: 1fr;
grid-gap: 0;
text-align: center;
}
.site-footer .copyright,
.site-footer .copyright a {
color: #fff;
font-size: 1.5rem;
}
}
</style>

View File

@ -0,0 +1,438 @@
---
import { twitter, facebook } from "@matthiesenxyz/astro-ghostcms/api";
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
settings: Settings;
};
const { settings } = Astro.props as Props;
---
<header
id="gh-head"
class={`gh-head has-cover js-header`}
>
<nav class="gh-head-inner inner gh-container">
<div class="gh-head-brand">
<a class="gh-head-logo" href="/">
{settings.logo && <img src={settings.logo} alt={settings.title} />}
{!settings.logo && settings.title}
</a>
<a class="gh-burger" role="button">
<div class="gh-burger-box">
<div class="gh-burger-inner"></div>
</div>
</a>
</div>
<div class="gh-head-menu">
<ul class="nav">
{settings.navigation.map(({ label, url }) => (
<li class="nav__item">
<a href={url}>{label}</a>
</li>
))}
</ul>
</div>
<div class="gh-head-actions">
<div class="gh-social">
{settings.facebook && (
<a
class="gh-social-facebook"
href={facebook(settings.facebook)}
title="Facebook"
target="_blank"
rel="noopener"
>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M16 0c8.837 0 16 7.163 16 16s-7.163 16-16 16S0 24.837 0 16 7.163 0 16 0zm5.204 4.911h-3.546c-2.103 0-4.443.885-4.443 3.934.01 1.062 0 2.08 0 3.225h-2.433v3.872h2.509v11.147h4.61v-11.22h3.042l.275-3.81h-3.397s.007-1.695 0-2.187c0-1.205 1.253-1.136 1.329-1.136h2.054V4.911z" />
</svg>
</a>
)}
{settings.twitter && (
<a
class="gh-social-twitter"
href={twitter(settings.twitter)}
title="Twitter"
target="_blank"
rel="noopener"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M30.063 7.313c-.813 1.125-1.75 2.125-2.875 2.938v.75c0 1.563-.188 3.125-.688 4.625a15.088 15.088 0 0 1-2.063 4.438c-.875 1.438-2 2.688-3.25 3.813a15.015 15.015 0 0 1-4.625 2.563c-1.813.688-3.75 1-5.75 1-3.25 0-6.188-.875-8.875-2.625.438.063.875.125 1.375.125 2.688 0 5.063-.875 7.188-2.5-1.25 0-2.375-.375-3.375-1.125s-1.688-1.688-2.063-2.875c.438.063.813.125 1.125.125.5 0 1-.063 1.5-.25-1.313-.25-2.438-.938-3.313-1.938a5.673 5.673 0 0 1-1.313-3.688v-.063c.813.438 1.688.688 2.625.688a5.228 5.228 0 0 1-1.875-2c-.5-.875-.688-1.813-.688-2.75 0-1.063.25-2.063.75-2.938 1.438 1.75 3.188 3.188 5.25 4.25s4.313 1.688 6.688 1.813a5.579 5.579 0 0 1 1.5-5.438c1.125-1.125 2.5-1.688 4.125-1.688s3.063.625 4.188 1.813a11.48 11.48 0 0 0 3.688-1.375c-.438 1.375-1.313 2.438-2.563 3.188 1.125-.125 2.188-.438 3.313-.875z" />
</svg>
</a>
)}
</div>
</div>
</nav>
</header>
<style lang="scss" is:global>
.gh-head {
padding: 1vmin 4vmin;
font-size: 1.6rem;
line-height: 1.3em;
color: #fff;
background: var(--ghost-accent-color);
}
.gh-head a {
color: inherit;
text-decoration: none;
}
.gh-head-inner {
display: grid;
grid-gap: 2.5vmin;
grid-template-columns: auto auto 1fr;
grid-auto-flow: row dense;
}
/* Brand
/* ---------------------------------------------------------- */
.gh-head-brand {
display: flex;
align-items: center;
height: 40px;
max-width: 200px;
text-align: center;
word-break: break-all;
}
.gh-head-logo {
display: block;
padding: 10px 0;
font-weight: 700;
font-size: 2rem;
line-height: 1.2em;
letter-spacing: -0.02em;
}
.gh-head-logo img {
max-height: 26px;
}
/* Primary Navigation
/* ---------------------------------------------------------- */
.gh-head-menu {
display: flex;
align-items: center;
font-weight: 500;
}
.gh-head-menu .nav {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.gh-head-menu .nav li {
margin: 0 2.5vmin 0 0;
padding: 0;
}
.gh-head-menu .nav a {
display: inline-block;
padding: 5px 0;
opacity: 0.8;
}
.gh-head-menu .nav a:hover {
opacity: 1;
}
/* Secondary Navigation
/* ---------------------------------------------------------- */
.gh-head-actions {
display: flex;
justify-content: flex-end;
align-items: center;
list-style: none;
text-align: right;
}
.gh-head-actions-list {
display: inline-flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
.gh-head-actions-list a:not([class]) {
display: inline-block;
margin: 0 0 0 1.5vmin;
padding: 5px 0;
}
.gh-social {
margin: 0 1.5vmin 0 0;
}
.gh-social a {
opacity: 0.8;
}
.gh-social a + a {
margin-left: 0.8rem;
}
.gh-social a:hover {
opacity: 1;
}
.gh-social svg {
height: 22px;
width: 22px;
fill: #fff;
}
.gh-social-facebook svg {
height: 20px;
width: 20px;
}
a.gh-head-button {
display: block;
padding: 8px 15px;
color: var(--color-darkgrey);
font-weight: 500;
letter-spacing: -0.015em;
font-size: 1.5rem;
line-height: 1em;
background: #fff;
border-radius: 30px;
}
/* Mobile Menu Trigger
/* ---------------------------------------------------------- */
.gh-burger {
position: relative;
display: none;
cursor: pointer;
}
.gh-burger-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 33px;
height: 33px;
}
.gh-burger-inner {
width: 100%;
height: 100%;
}
.gh-burger-box::before {
position: absolute;
display: block;
top: 0;
left: 0;
bottom: 0;
margin: auto;
content: "";
width: 100%;
height: 1px;
background: currentcolor;
transition: transform 300ms cubic-bezier(0.2, 0.6, 0.3, 1),
width 300ms cubic-bezier(0.2, 0.6, 0.3, 1);
will-change: transform, width;
}
.gh-burger-inner::before,
.gh-burger-inner::after {
position: absolute;
display: block;
top: 0;
left: 0;
bottom: 0;
margin: auto;
content: "";
width: 100%;
height: 1px;
background: currentcolor;
transition: transform 250ms cubic-bezier(0.2, 0.7, 0.3, 1),
width 250ms cubic-bezier(0.2, 0.7, 0.3, 1);
will-change: transform, width;
}
.gh-burger-inner::before {
transform: translatey(-6px);
}
.gh-burger-inner::after {
transform: translatey(6px);
}
body:not(.gh-head-open) .gh-burger:hover .gh-burger-inner::before {
transform: translatey(-8px);
}
body:not(.gh-head-open) .gh-burger:hover .gh-burger-inner::after {
transform: translatey(8px);
}
.gh-head-open .gh-burger-box::before {
width: 0;
transform: translatex(19px);
transition: transform 200ms cubic-bezier(0.2, 0.7, 0.3, 1),
width 200ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.gh-head-open .gh-burger-inner::before {
width: 26px;
transform: translatex(6px) rotate(135deg);
}
.gh-head-open .gh-burger-inner::after {
width: 26px;
transform: translatex(6px) rotate(-135deg);
}
/* Mobile Menu
/* ---------------------------------------------------------- */
/* IDs needed to ensure sufficient specificity */
@media (max-width: 900px) {
.gh-burger {
display: inline-block;
}
#gh-head {
transition: all 0.4s ease-out;
overflow: hidden;
}
#gh-head .gh-head-inner {
height: 100%;
grid-template-columns: 1fr;
}
#gh-head .gh-head-brand {
position: relative;
z-index: 10;
grid-column-start: auto;
max-width: none;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.home-template #gh-head .gh-head-brand {
justify-content: flex-end;
}
#gh-head .gh-head-menu {
align-self: center;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin: 0 0 10vh 0;
font-weight: 300;
font-size: 3.6rem;
line-height: 1.1em;
}
#gh-head .gh-head-menu .nav li {
margin: 5px 0;
}
#gh-head .gh-head-menu .nav a {
padding: 8px 0;
}
#gh-head .gh-head-menu .nav {
display: flex;
flex-direction: column;
align-items: center;
}
#gh-head .gh-head-actions {
padding: 20px 0;
justify-content: center;
text-align: left;
}
#gh-head .gh-head-actions a {
margin: 0 10px;
}
/* Hide collapsed content */
#gh-head .gh-head-actions,
#gh-head .gh-head-menu {
display: none;
}
/* Open the menu */
.gh-head-open {
overflow: hidden;
height: 100vh;
}
.gh-head-open #gh-head {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 3999999;
overflow-y: scroll;
}
.gh-head-open #gh-head .gh-head-inner {
grid-template-rows: auto 1fr auto;
}
.gh-head-open #gh-head .gh-head-actions,
.gh-head-open #gh-head .gh-head-menu {
display: flex;
}
}
@media (max-width: 600px) {
#gh-head .gh-head-menu {
font-size: 6vmin;
}
}
.home-template .gh-head {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 2000;
}
.home-template .gh-head.has-cover {
background: transparent;
}
.home-template.gh-head-open .gh-head {
background: var(--ghost-accent-color);
}
.home-template .gh-head-logo {
display: none;
}
.home-template .gh-head-menu {
margin-left: -2.5vmin;
}
</style>
<script lang="ts">
if (!window.handleMenu) {
window.handleMenu = () => {
// menu
const burgerButton = document.querySelector(".gh-burger");
if (burgerButton) {
burgerButton.addEventListener("click", () => {
const body = document.querySelector("body");
body.classList.toggle("gh-head-open");
});
}
};
const callback = () => {
window.handleMenu();
};
if (document.readyState === "complete") {
callback();
} else {
document.addEventListener("DOMContentLoaded", callback);
}
}
</script>

View File

@ -0,0 +1,154 @@
---
import { getGhostImgPath } from "../utils";
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
import cover from "./cover.jpg";
export type Props = {
featureImg: string;
mainTitle?: string;
settings: Settings;
description?: string;
addClass?: string;
};
const {
featureImg = cover.src,
mainTitle = "",
settings,
description = "",
} = Astro.props as Props;
---
<div class="site-header-content">
{featureImg && (
<img
class="site-header-cover"
data-srcset={`
${getGhostImgPath(settings.url, featureImg, 300)} 300w,
${getGhostImgPath(settings.url, featureImg, 600)} 600w
${getGhostImgPath(settings.url, featureImg, 1000)} 1000w
${getGhostImgPath(settings.url, featureImg, 2000)} 2000w
`}
srcset={`
${getGhostImgPath(settings.url, featureImg, 300)} 300w,
${getGhostImgPath(settings.url, featureImg, 600)} 600w
${getGhostImgPath(settings.url, featureImg, 1000)} 1000w
${getGhostImgPath(settings.url, featureImg, 2000)} 2000w
`}
data-sizes="auto"
data-src={getGhostImgPath(settings.url, featureImg, 2000)}
src={getGhostImgPath(settings.url, featureImg, 2000)}
alt={mainTitle}
sizes="100vw"
/>
)}
{!featureImg && (
<img
class="site-header-cover"
data-srcset={`
${cover.src} 300w,
${cover.src} 600w,
${cover.src} 1000w,
${cover.src} 2000w
`}
srcset={`
${cover.src} 300w,
${cover.src} 600w
${cover.src} 1000w
${cover.src} 2000w
`}
data-sizes="auto"
data-src={cover.src}
src={cover.src}
alt={mainTitle}
sizes="100vw"
/>
)}
<slot name="title">
<h1 class="site-title">
{settings.logo ? (
<img
class="site-logo"
src={getGhostImgPath(settings.url, settings.logo, 300)}
alt={settings.title}
/>
) : (
settings.title
)}
</h1>
</slot>
<p>{description}</p>
</div>
<style lang="scss">
.site-header {
position: relative;
color: #fff;
background: var(--ghost-accent-color);
}
.site-header-cover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.site-header-content {
position: relative;
z-index: 100;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 6vw 3vw;
min-height: 200px;
max-height: 340px;
text-align: center;
}
.site-title {
z-index: 10;
margin: 0 0 0.15em;
padding: 0;
}
.site-logo {
max-height: 55px;
}
.site-header-content p {
z-index: 10;
max-width: 600px;
margin: 0 auto;
line-height: 1.2em;
opacity: 0.8;
}
@media (max-width: 600px) {
.site-header-content p {
max-width: 80vmin;
font-size: 1.8rem;
}
}
/* 4.1 Home header
/* ---------------------------------------------------------- */
.site-home-header {
position: relative;
z-index: 1000;
overflow: hidden;
}
.site-header-content {
padding: 18vmin 4vmin;
font-size: 2.5rem;
font-weight: 400;
color: #fff;
background: var(--ghost-accent-color);
}
</style>

View File

@ -0,0 +1,23 @@
---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
settings: Settings;
};
const { settings } = Astro.props as Props;
---
<div class="viewport">
<Header settings={settings} />
<div class="site-content">
<slot></slot>
</div>
<Footer settings={settings} />
</div>
<style>
.content {
min-height: 580px;
}
</style>

View File

@ -0,0 +1,569 @@
---
import FeatureImage from "../components/FeatureImage.astro";
import type { Settings, Page } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
page: Page;
settings: Settings;
pageClass?: string;
};
const { page, settings, pageClass } = Astro.props as Props;
---
<main id="site-main" class="site-main">
<article class={`article page ${pageClass}`}>
<header class="article-header gh-canvas">
{page.feature_image && (
<FeatureImage
image={page.feature_image}
alt={page.feature_image_alt ? page.feature_image_alt : page.title}
caption={page.feature_image_caption || ""}
settings={settings}
/>
)}
</header>
<section class="gh-content gh-canvas">
<h1 class="article-title">{page.title}</h1>
<Fragment set:html={page.html} />
</section>
</article>
</main>
<style lang="scss" is:global>
.article {
padding: 8vmin 0;
word-break: break-word;
}
.article-header {
padding: 0 0 6vmin 0;
}
.article-tag {
display: flex;
justify-content: flex-start;
align-items: center;
margin: 0 0 0.5rem;
color: var(--color-midgrey);
font-size: 1.3rem;
line-height: 1.4em;
letter-spacing: 0.02em;
font-weight: 600;
text-transform: uppercase;
}
.article-tag a {
color: var(--ghost-accent-color);
}
.article-title {
color: color-mod(var(--color-darkgrey) l(-5%));
}
.article-excerpt {
margin: 0 0 1rem;
font-size: 2rem;
line-height: 1.4em;
opacity: 0.6;
}
.gh-canvas .article-image {
grid-column: wide-start / wide-end;
width: 100%;
margin: 6vmin 0 0;
}
.gh-canvas .article-image img {
display: block;
margin-left: auto;
margin-right: auto;
width: 100%;
}
@media (max-width: 600px) {
.article-excerpt {
font-size: 1.8rem;
}
}
/* -------- */
/* Content grid
/* ---------------------------------------------------------- */
/* Canvas creates a multi-column, centered grid which the post
is laid out on top of. Canvas just defines the grid, we don't
use it for applying any other styles. */
.gh-canvas {
display: grid;
grid-template-columns:
[full-start]
minmax(4vmin, auto)
[wide-start]
minmax(auto, 240px)
[main-start]
min(720px, calc(100% - 8vw))
[main-end]
minmax(auto, 240px)
[wide-end]
minmax(4vmin, auto)
[full-end];
}
.gh-canvas > * {
grid-column: main-start / main-end;
}
.kg-width-wide {
grid-column: wide-start / wide-end;
}
.kg-width-full {
grid-column: full-start / full-end;
}
.kg-width-full img {
width: 100%;
}
/* Default vertical spacing */
.gh-content > * + * {
margin-top: 4vmin;
margin-bottom: 0;
}
/* [id] represents all headings h1-h6, reset all margins */
.gh-content > [id] {
margin: 0;
color: var(--color-darkgrey);
}
/* Add back a top margin to all headings, unless a heading
is the very first element in the post content */
.gh-content > [id]:not(:first-child) {
margin: 2em 0 0;
}
/* Add a small margin between a heading and anything after it */
.gh-content > [id] + * {
margin-top: 1.5rem !important;
}
/* A larger margin before/after HRs and blockquotes */
.gh-content > hr,
.gh-content > blockquote {
position: relative;
margin-top: 6vmin;
}
.gh-content > hr + *,
.gh-content > blockquote + * {
margin-top: 6vmin !important;
}
/* Now the content typography styles */
.gh-content a {
color: var(--ghost-accent-color);
text-decoration: underline;
word-break: break-word;
}
.gh-content > blockquote,
.gh-content > ol,
.gh-content > ul,
.gh-content > dl,
.gh-content > p {
font-family: var(--font-serif);
font-weight: 400;
font-size: 2.1rem;
line-height: 1.6em;
}
.gh-content > ul,
.gh-content > ol,
.gh-content > dl {
padding-left: 1.9em;
}
.gh-content > blockquote {
position: relative;
font-style: italic;
padding: 0;
}
.gh-content > blockquote::before {
content: "";
position: absolute;
left: -1.5em;
top: 0;
bottom: 0;
width: 0.3rem;
background: var(--ghost-accent-color);
}
.gh-content :not(pre) > code {
vertical-align: middle;
padding: 0.15em 0.4em 0.15em;
border: #e1eaef 1px solid;
font-weight: 400 !important;
font-size: 0.9em;
line-height: 1em;
color: #15171a;
background: #f0f6f9;
border-radius: 0.25em;
}
.gh-content pre {
overflow: auto;
padding: 16px 20px;
color: var(--color-wash);
font-size: 1.4rem;
line-height: 1.5em;
background: var(--color-darkgrey);
border-radius: 5px;
box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.4);
}
@media (max-width: 650px) {
.gh-content blockquote,
.gh-content ol,
.gh-content ul,
.gh-content dl,
.gh-content p {
font-size: 1.7rem;
}
.gh-content blockquote::before {
left: -4vmin;
}
}
/* Cards
/* ---------------------------------------------------------- */
/* Cards are dynamic blocks of content which appear within Ghost
posts, for example: embedded videos, tweets, galleries, or
specially styled bookmark links. We add extra styling here to
make sure they look good, and are given a bit of extra spacing. */
/* Add extra margin before/after any cards,
except for when immediately preceeded by a heading */
.gh-content :not(.kg-card):not([id]) + .kg-card {
margin-top: 6vmin;
margin-bottom: 0;
}
.gh-content .kg-card + :not(.kg-card) {
margin-top: 6vmin;
margin-bottom: 0;
}
/* This keeps small embeds centered */
.kg-embed-card {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
/* This keeps small iamges centered */
.kg-image-card img {
margin: auto;
}
/* Captions */
figcaption {
padding: 1.5rem 1.5rem 0;
text-align: center;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
font-size: 1.3rem;
line-height: 1.4em;
}
figcaption strong {
color: rgba(0, 0, 0, 0.8);
}
figcaption a {
color: var(--ghost-accent-color);
}
/* Highly specific styles for traditional Instagram embeds */
iframe.instagram-media {
margin-top: 6vmin !important;
margin-left: auto !important;
margin-right: auto !important;
margin-bottom: 0 !important;
}
iframe.instagram-media + script + :not([id]) {
margin-top: 6vmin;
}
/* Galleries
/* ---------------------------------------------------------- */
/* When there galleries are mixed with images, reduce margin
between them, so it looks like 1 big gallery */
.kg-image-card + .kg-gallery-card,
.kg-gallery-card + .kg-image-card,
.kg-gallery-card + .kg-gallery-card {
margin-top: 0.75em;
}
.kg-image-card.kg-card-hascaption + .kg-gallery-card,
.kg-gallery-card.kg-card-hascaption + .kg-image-card,
.kg-gallery-card.kg-card-hascaption + .kg-gallery-card {
margin-top: 1.75em;
}
.kg-gallery-container {
position: relative;
}
.kg-gallery-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.kg-gallery-image img {
display: block;
margin: 0;
width: 100%;
height: 100%;
}
.kg-gallery-row:not(:first-of-type) {
margin: 0.75em 0 0 0;
}
.kg-gallery-image:not(:first-of-type) {
margin: 0 0 0 0.75em;
}
/* Bookmark Cards
/* ---------------------------------------------------------- */
/* These are styled links with structured data, similar to a
Twitter card. These styles roughly match what you see in the
Ghost editor. */
.kg-bookmark-card,
.kg-bookmark-publisher {
position: relative;
width: 100%;
}
.kg-bookmark-container,
.kg-bookmark-container:hover {
display: flex;
color: currentColor;
font-family: var(--font-sans-serif);
text-decoration: none !important;
background: rgba(255, 255, 255, 0.6);
border-radius: 5px;
box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.kg-bookmark-content {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-basis: 100%;
align-items: flex-start;
justify-content: flex-start;
padding: 20px;
}
.kg-bookmark-title {
font-size: 1.5rem;
line-height: 1.4em;
font-weight: 600;
color: #15171a;
}
.kg-bookmark-description {
display: -webkit-box;
font-size: 1.4rem;
line-height: 1.5em;
margin-top: 3px;
color: #626d79;
font-weight: 400;
max-height: 44px;
overflow-y: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.kg-bookmark-metadata {
display: flex;
align-items: center;
margin-top: 22px;
width: 100%;
color: #394047;
font-size: 1.4rem;
font-weight: 500;
}
.kg-bookmark-icon {
width: 20px;
height: 20px;
margin-right: 6px;
}
.kg-bookmark-author,
.kg-bookmark-publisher {
display: inline;
}
.kg-bookmark-publisher {
text-overflow: ellipsis;
overflow: hidden;
max-width: 240px;
white-space: nowrap;
display: block;
line-height: 1.65em;
}
.kg-bookmark-metadata > span:nth-of-type(2) {
color: #626d79;
font-weight: 400;
}
.kg-bookmark-metadata > span:nth-of-type(2):before {
content: "•";
color: #394047;
margin: 0 6px;
}
.kg-bookmark-thumbnail {
position: relative;
flex-grow: 1;
min-width: 33%;
}
.kg-bookmark-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
border-radius: 0 4px 4px 0;
}
/* Card captions
/* ---------------------------------------------------------- */
.kg-width-full.kg-card-hascaption {
display: grid;
grid-template-columns: inherit;
}
.kg-width-wide.kg-card-hascaption img {
grid-column: wide-start / wide-end;
}
.kg-width-full.kg-card-hascaption img {
grid-column: 1 / -1;
}
.kg-width-full.kg-card-hascaption figcaption {
grid-column: main-start / main-end;
}
.article-comments {
margin: 6vmin 0 0 0;
}
/* -----old------ */
.footnotes-sep {
margin-bottom: 30px;
}
.footnotes {
font-size: 1.5rem;
}
.footnotes p {
margin: 0;
}
.footnote-backref {
font-size: 1.2rem;
font-weight: bold;
text-decoration: none !important;
box-shadow: none !important;
}
/* Tables */
.post-full-content table {
display: inline-block;
overflow-x: auto;
margin: 0.5em 0 2.5em;
max-width: 100%;
width: auto;
border-spacing: 0;
border-collapse: collapse;
font-family: var(--font-sans-serif);
font-size: 1.6rem;
white-space: nowrap;
vertical-align: top;
}
.post-full-content table {
-webkit-overflow-scrolling: touch;
background: radial-gradient(
ellipse at left,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0) 75%
)
0 center,
radial-gradient(
ellipse at right,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0) 75%
)
100% center;
background-attachment: scroll, scroll;
background-size: 10px 100%, 10px 100%;
background-repeat: no-repeat;
}
.post-full-content table td:first-child {
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 20px 100%;
background-repeat: no-repeat;
}
.post-full-content table td:last-child {
background-image: linear-gradient(
to left,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-position: 100% 0;
background-size: 20px 100%;
background-repeat: no-repeat;
}
.post-full-content table th {
color: var(--color-darkgrey);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.2px;
text-align: left;
text-transform: uppercase;
background-color: color-mod(var(--color-wash) l(+4%));
}
.post-full-content table th,
.post-full-content table td {
padding: 6px 12px;
border: color-mod(var(--color-wash) l(-1%) s(-5%)) 1px solid;
}
</style>

View File

@ -0,0 +1,26 @@
---
import type { Page } from 'astro';
const { page } = Astro.props as {page: Page};
---
<div class="page__actions">
{page.url.prev && (
<a class="action__go-to-x" href={page.url.prev} title="Go to Previous">
&larr; Prev
</a>
)}
{page.url.next && (
<a class="action__go-to-x" href={page.url.next} title="Go to Next">
Next &rarr;
</a>
)}
</div>
<style>
/* .page__actions {
@apply flex justify-center md:justify-end py-6 gap-2;
}
.action__go-to-x {
@apply text-base uppercase text-gray-500 dark:text-gray-400 hover:underline;
} */
</style>

View File

@ -0,0 +1,569 @@
---
import PostHero from "../components/PostHero.astro";
import PostFooter from "../components/PostFooter.astro";
import {invariant, type Post, type Settings } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
post: Post;
settings: Settings;
postClass?: string;
posts: Post[];
};
const { post, settings, postClass, posts } = Astro.props as Props;
invariant(settings, "Settings not found");
---
<main id="site-main" class="site-main">
<article class={`article post ${postClass}`}>
<PostHero post={post} settings={settings} />
<section class="gh-content gh-canvas">
<Fragment set:html={post.html} />
</section>
</article>
<PostFooter post={post} settings={settings} posts={posts} />
</main>
<style lang="scss" is:global>
.article {
padding: 8vmin 0;
word-break: break-word;
}
.article-header {
padding: 0 0 6vmin 0;
}
.article-tag {
display: flex;
justify-content: flex-start;
align-items: center;
margin: 0 0 0.5rem;
color: var(--color-midgrey);
font-size: 1.3rem;
line-height: 1.4em;
letter-spacing: 0.02em;
font-weight: 600;
text-transform: uppercase;
}
.article-tag a {
color: var(--ghost-accent-color);
}
.article-title {
color: color-mod(var(--color-darkgrey) l(-5%));
}
.article-excerpt {
margin: 0 0 1rem;
font-size: 2rem;
line-height: 1.4em;
opacity: 0.6;
}
.gh-canvas .article-image {
grid-column: wide-start / wide-end;
width: 100%;
margin: 6vmin 0 0;
}
.gh-canvas .article-image img {
display: block;
margin-left: auto;
margin-right: auto;
width: 100%;
}
@media (max-width: 600px) {
.article-excerpt {
font-size: 1.8rem;
}
}
/* -------- */
/* Content grid
/* ---------------------------------------------------------- */
/* Canvas creates a multi-column, centered grid which the post
is laid out on top of. Canvas just defines the grid, we don't
use it for applying any other styles. */
.gh-canvas {
display: grid;
grid-template-columns:
[full-start]
minmax(4vmin, auto)
[wide-start]
minmax(auto, 240px)
[main-start]
min(720px, calc(100% - 8vw))
[main-end]
minmax(auto, 240px)
[wide-end]
minmax(4vmin, auto)
[full-end];
}
.gh-canvas > * {
grid-column: main-start / main-end;
}
.kg-width-wide {
grid-column: wide-start / wide-end;
}
.kg-width-full {
grid-column: full-start / full-end;
}
.kg-width-full img {
width: 100%;
}
/* Content
/* ---------------------------------------------------------- */
/* Content refers to styling all page and post content that is
created within the Ghost editor. The main content handles
headings, text, images and lists. We deal with cards lower down. */
/* Default vertical spacing */
.gh-content > * + * {
margin-top: 4vmin;
margin-bottom: 0;
}
/* [id] represents all headings h1-h6, reset all margins */
.gh-content > [id] {
margin: 0;
color: var(--color-darkgrey);
}
/* Add back a top margin to all headings, unless a heading
is the very first element in the post content */
.gh-content > [id]:not(:first-child) {
margin: 2em 0 0;
}
/* Add a small margin between a heading and anything after it */
.gh-content > [id] + * {
margin-top: 1.5rem !important;
}
/* A larger margin before/after HRs and blockquotes */
.gh-content > hr,
.gh-content > blockquote {
position: relative;
margin-top: 6vmin;
}
.gh-content > hr + *,
.gh-content > blockquote + * {
margin-top: 6vmin !important;
}
/* Now the content typography styles */
.gh-content a {
color: var(--ghost-accent-color);
text-decoration: underline;
word-break: break-word;
}
.gh-content > blockquote,
.gh-content > ol,
.gh-content > ul,
.gh-content > dl,
.gh-content > p {
font-family: var(--font-serif);
font-weight: 400;
font-size: 2.1rem;
line-height: 1.6em;
}
.gh-content > ul,
.gh-content > ol,
.gh-content > dl {
padding-left: 1.9em;
}
.gh-content > blockquote {
position: relative;
font-style: italic;
padding: 0;
}
.gh-content > blockquote::before {
content: "";
position: absolute;
left: -1.5em;
top: 0;
bottom: 0;
width: 0.3rem;
background: var(--ghost-accent-color);
}
.gh-content :not(pre) > code {
vertical-align: middle;
padding: 0.15em 0.4em 0.15em;
border: #e1eaef 1px solid;
font-weight: 400 !important;
font-size: 0.9em;
line-height: 1em;
color: #15171a;
background: #f0f6f9;
border-radius: 0.25em;
}
.gh-content pre {
overflow: auto;
padding: 16px 20px;
color: var(--color-wash);
font-size: 1.4rem;
line-height: 1.5em;
background: var(--color-darkgrey);
border-radius: 5px;
box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.4);
}
@media (max-width: 650px) {
.gh-content blockquote,
.gh-content ol,
.gh-content ul,
.gh-content dl,
.gh-content p {
font-size: 1.7rem;
}
.gh-content blockquote::before {
left: -4vmin;
}
}
/* Cards
/* ---------------------------------------------------------- */
/* Cards are dynamic blocks of content which appear within Ghost
posts, for example: embedded videos, tweets, galleries, or
specially styled bookmark links. We add extra styling here to
make sure they look good, and are given a bit of extra spacing. */
/* Add extra margin before/after any cards,
except for when immediately preceeded by a heading */
.gh-content :not(.kg-card):not([id]) + .kg-card {
margin-top: 6vmin;
margin-bottom: 0;
}
.gh-content .kg-card + :not(.kg-card) {
margin-top: 6vmin;
margin-bottom: 0;
}
/* This keeps small embeds centered */
.kg-embed-card {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
/* This keeps small iamges centered */
.kg-image-card img {
margin: auto;
}
/* Captions */
figcaption {
padding: 1.5rem 1.5rem 0;
text-align: center;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
font-size: 1.3rem;
line-height: 1.4em;
}
figcaption strong {
color: rgba(0, 0, 0, 0.8);
}
figcaption a {
color: var(--ghost-accent-color);
}
/* Highly specific styles for traditional Instagram embeds */
iframe.instagram-media {
margin-top: 6vmin !important;
margin-left: auto !important;
margin-right: auto !important;
margin-bottom: 0 !important;
}
iframe.instagram-media + script + :not([id]) {
margin-top: 6vmin;
}
/* Galleries
/* ---------------------------------------------------------- */
/* When there galleries are mixed with images, reduce margin
between them, so it looks like 1 big gallery */
.kg-image-card + .kg-gallery-card,
.kg-gallery-card + .kg-image-card,
.kg-gallery-card + .kg-gallery-card {
margin-top: 0.75em;
}
.kg-image-card.kg-card-hascaption + .kg-gallery-card,
.kg-gallery-card.kg-card-hascaption + .kg-image-card,
.kg-gallery-card.kg-card-hascaption + .kg-gallery-card {
margin-top: 1.75em;
}
.kg-gallery-container {
position: relative;
}
.kg-gallery-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.kg-gallery-image img {
display: block;
margin: 0;
width: 100%;
height: 100%;
}
.kg-gallery-row:not(:first-of-type) {
margin: 0.75em 0 0 0;
}
.kg-gallery-image:not(:first-of-type) {
margin: 0 0 0 0.75em;
}
/* Bookmark Cards
/* ---------------------------------------------------------- */
/* These are styled links with structured data, similar to a
Twitter card. These styles roughly match what you see in the
Ghost editor. */
.kg-bookmark-card,
.kg-bookmark-publisher {
position: relative;
width: 100%;
}
.kg-bookmark-container,
.kg-bookmark-container:hover {
display: flex;
color: currentColor;
font-family: var(--font-sans-serif);
text-decoration: none !important;
background: rgba(255, 255, 255, 0.6);
border-radius: 5px;
box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.kg-bookmark-content {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-basis: 100%;
align-items: flex-start;
justify-content: flex-start;
padding: 20px;
}
.kg-bookmark-title {
font-size: 1.5rem;
line-height: 1.4em;
font-weight: 600;
color: #15171a;
}
.kg-bookmark-description {
display: -webkit-box;
font-size: 1.4rem;
line-height: 1.5em;
margin-top: 3px;
color: #626d79;
font-weight: 400;
max-height: 44px;
overflow-y: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.kg-bookmark-metadata {
display: flex;
align-items: center;
margin-top: 22px;
width: 100%;
color: #394047;
font-size: 1.4rem;
font-weight: 500;
}
.kg-bookmark-icon {
width: 20px;
height: 20px;
margin-right: 6px;
}
.kg-bookmark-author,
.kg-bookmark-publisher {
display: inline;
}
.kg-bookmark-publisher {
text-overflow: ellipsis;
overflow: hidden;
max-width: 240px;
white-space: nowrap;
display: block;
line-height: 1.65em;
}
.kg-bookmark-metadata > span:nth-of-type(2) {
color: #626d79;
font-weight: 400;
}
.kg-bookmark-metadata > span:nth-of-type(2):before {
content: "•";
color: #394047;
margin: 0 6px;
}
.kg-bookmark-thumbnail {
position: relative;
flex-grow: 1;
min-width: 33%;
}
.kg-bookmark-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
border-radius: 0 4px 4px 0;
}
/* Card captions
/* ---------------------------------------------------------- */
.kg-width-full.kg-card-hascaption {
display: grid;
grid-template-columns: inherit;
}
.kg-width-wide.kg-card-hascaption img {
grid-column: wide-start / wide-end;
}
.kg-width-full.kg-card-hascaption img {
grid-column: 1 / -1;
}
.kg-width-full.kg-card-hascaption figcaption {
grid-column: main-start / main-end;
}
.article-comments {
margin: 6vmin 0 0 0;
}
/* -----old------ */
.footnotes-sep {
margin-bottom: 30px;
}
.footnotes {
font-size: 1.5rem;
}
.footnotes p {
margin: 0;
}
.footnote-backref {
font-size: 1.2rem;
font-weight: bold;
text-decoration: none !important;
box-shadow: none !important;
}
/* Tables */
.post-full-content table {
display: inline-block;
overflow-x: auto;
margin: 0.5em 0 2.5em;
max-width: 100%;
width: auto;
border-spacing: 0;
border-collapse: collapse;
font-family: var(--font-sans-serif);
font-size: 1.6rem;
white-space: nowrap;
vertical-align: top;
}
.post-full-content table {
-webkit-overflow-scrolling: touch;
background: radial-gradient(
ellipse at left,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0) 75%
)
0 center,
radial-gradient(
ellipse at right,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0) 75%
)
100% center;
background-attachment: scroll, scroll;
background-size: 10px 100%, 10px 100%;
background-repeat: no-repeat;
}
.post-full-content table td:first-child {
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 20px 100%;
background-repeat: no-repeat;
}
.post-full-content table td:last-child {
background-image: linear-gradient(
to left,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 0) 100%
);
background-position: 100% 0;
background-size: 20px 100%;
background-repeat: no-repeat;
}
.post-full-content table th {
color: var(--color-darkgrey);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.2px;
text-align: left;
text-transform: uppercase;
background-color: color-mod(var(--color-wash) l(+4%));
}
.post-full-content table th,
.post-full-content table td {
padding: 6px 12px;
border: color-mod(var(--color-wash) l(-1%) s(-5%)) 1px solid;
}
</style>

View File

@ -0,0 +1,125 @@
---
import PostPreview from "../components/PostPreview.astro";
import type { Settings, Post } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
post: Post;
settings: Settings;
posts: Post[];
};
const { post, settings, posts } = Astro.props as Props;
---
<!--section class="footer-cta">
<div class="inner">
<h2>Sign up for more like this.</h2>
<a class="footer-cta-button" href="#/portal">
<div>Enter your email</div>
<span>Subscribe</span>
</a>
</div>
</section-->
<aside class="read-more-wrap">
<div class="read-more inner">
{posts
.filter((p: Post) => p.id !== post.id)
.slice(0, 3)
.map((post: Post) => <PostPreview post={post} settings={settings} />)}
</div>
</aside>
<style lang="scss">
/* 7.3. Subscribe
/* ---------------------------------------------------------- */
.footer-cta {
position: relative;
padding: 9vmin 4vmin 10vmin;
color: #fff;
text-align: center;
background: var(--color-darkgrey);
}
/* Increases the default h2 size by 15%, for small and large screens */
.footer-cta h2 {
margin: 0 0 30px;
font-size: 3.2rem;
}
@media (max-width: 600px) {
.footer-cta h2 {
font-size: 2.65rem;
}
}
.footer-cta-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 500px;
padding: 5px 5px 5px 15px;
font-size: 1.8rem;
color: var(--color-midgrey);
background: #fff;
border-radius: 8px;
}
.footer-cta-button span {
display: inline-block;
padding: 10px 20px;
color: #fff;
font-weight: 500;
background: var(--ghost-accent-color);
border-radius: 5px;
}
/* 7.4. Read more
/* ---------------------------------------------------------- */
.read-more-wrap {
width: 100%;
padding: 4vmin;
margin: 0 auto -40px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: color-mod(var(--color-darkgrey) l(-5%));
}
.read-more {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 4vmin;
}
.read-more .post-card-title {
color: #fff;
opacity: 0.8;
}
.read-more .post-card-excerpt {
color: rgba(255, 255, 255, 0.6);
}
.read-more .post-card-byline-content a {
color: #fff;
}
@media (max-width: 1000px) {
.read-more {
grid-template-columns: 1fr 1fr;
}
.read-more article:nth-child(3) {
display: none;
}
}
@media (max-width: 700px) {
.read-more {
grid-template-columns: 1fr;
}
.read-more article:nth-child(2) {
display: none;
}
}
</style>

View File

@ -0,0 +1,108 @@
---
import FeatureImage from "../components/FeatureImage.astro";
import AuthorList from "../components/AuthorList.astro";
import { formatDate } from "../utils";
import type { Settings, Post } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
post: Post;
settings: Settings;
};
const { post, settings } = Astro.props as Props;
---
<header class="article-header gh-canvas">
{post.primary_tag && (
<section class="article-tag">
<a href={`/tag/${post.primary_tag.slug}`}>{post.primary_tag.name}</a>
</section>
)}
<h1 class="article-title" transition:name={post.title}>{post.title}</h1>
{post.custom_excerpt && <p class="article-excerpt">{post.custom_excerpt}</p>}
<div class="article-byline">
<section class="article-byline-content">
<AuthorList post={post} settings={settings} />
<div class="article-byline-meta">
{ post.authors && post.authors.length > 1 && (
<h4 class="author-name">
{post.authors.map((author) => author.name).join(", ")}
</h4>
)}
<div class="byline-meta-content">
<time class="byline-meta-date" datetime={formatDate(post.created_at)}
>{formatDate(post.created_at)}
</time>
<span class="byline-reading-time"
><span class="bull">&bull;</span>
{post.reading_time} min read
</span>
</div>
</div>
</section>
</div>
{post.feature_image && (
<FeatureImage
image={post.feature_image}
alt={post.feature_image_alt ? post.feature_image_alt : post.title}
caption={post.feature_image_caption || "" }
settings={settings}
transitionName={`img-${post.title}`}
/>
)}
</header>
<style lang="scss">
.article-byline {
display: flex;
justify-content: space-between;
margin: 20px 0 0;
}
.article-byline-content {
flex-grow: 1;
display: flex;
align-items: center;
}
.article-byline-content .author-list {
justify-content: flex-start;
padding: 0 12px 0 0;
}
.article-byline-meta {
color: color-mod(var(--color-midgrey));
font-size: 1.4rem;
line-height: 1.2em;
}
.article-byline-meta h4 {
margin: 0 0 3px;
font-size: 1.6rem;
}
.article-byline-meta .bull {
display: inline-block;
margin: 0 2px;
opacity: 0.6;
}
.basic-info .avatar-wrapper {
position: relative;
margin: 0;
width: 60px;
height: 60px;
border: none;
background: rgba(229, 239, 245, 0.1);
}
.basic-info .avatar-wrapper svg {
margin: 0;
width: 60px;
height: 60px;
opacity: 0.15;
}
.page-template .article-title {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,301 @@
---
import { getGhostImgPath, formatDate } from "../utils";
import AuthorList from "../components/AuthorList.astro";
import type { Settings, Post, Tag } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
post: Post;
settings: Settings;
index?: number;
isHome?: boolean;
};
const { post, settings, index, isHome = false } = Astro.props as Props;
---
<article
class={`post-card ${post.tags && post.tags
.map((tag: Tag) => `tag-${tag.slug}`)
.join(" ")} ${
isHome && post.feature_image && index == 0 ? "post-card-large" : ""
}`}
>
<a class="post-card-image-link" href={`/${post.slug}`} data-astro-reload>
<img
class="post-card-image"
srcset={`
${getGhostImgPath(settings.url, post.feature_image || "", 300)} 300w,
${getGhostImgPath(settings.url, post.feature_image || "", 600)} 600w
${getGhostImgPath(settings.url, post.feature_image || "", 1000)} 1000w
${getGhostImgPath(settings.url, post.feature_image || "", 2000)} 2000w
`}
src={`${getGhostImgPath(settings.url, post.feature_image || "", 600)}`}
alt={post.title}
sizes="(max-width: 1000px) 400px, 800px"
loading="lazy"
transition:name={`img-${post.title}`}
/>
</a>
<div class="post-card-content">
<a class="post-card-content-link" href={`/${post.slug}`} data-astro-reload>
<header class="post-card-header">
{post.primary_tag && (
<div class="post-card-primary-tag">{post.primary_tag.name}</div>
)}
<h2 class="post-card-title" transition:name={post.title}>{post.title}</h2>
</header>
<div class="post-card-excerpt">
<p>{post.excerpt}</p>
</div>
</a>
<footer class="post-card-meta">
<AuthorList post={post} settings={settings} />
<div class="post-card-byline-content">
<span>{post.primary_author?.name ?? ""}</span>
<span class="post-card-byline-date"
><time datetime={formatDate(post.created_at)}
>{formatDate(post.created_at)}
</time>
<span class="bull">&bull;</span>
{post.reading_time} min read
</span>
</div>
</footer>
</div>
</article>
<style lang="scss" is:global>
@use "sass:color";
@import "../styles/variables";
.post-card {
position: relative;
flex: 1 1 301px;
display: flex;
flex-direction: column;
min-height: 220px;
background-size: cover;
word-break: break-word;
}
.post-card-excerpt {
max-width: 56em;
color: color.scale($color-midgrey, $lightness: -8%);
}
.post-card-image-link {
position: relative;
display: block;
overflow: hidden;
border-radius: 3px;
}
.post-card-image {
width: 100%;
height: 200px;
background: $color-lightgrey no-repeat center center;
object-fit: cover;
}
.post-card-content-link {
position: relative;
display: block;
color: $color-darkgrey;
}
.post-card-content-link:hover {
text-decoration: none;
}
.post-card-header {
margin: 20px 0 0;
}
.post-feed .no-image .post-card-content-link {
padding: 0;
}
.no-image .post-card-header {
margin-top: 0;
}
.post-card-primary-tag {
margin: 0 0 0.2em;
color: var(--ghost-accent-color);
font-size: 1.2rem;
font-weight: 500;
letter-spacing: 0.2px;
text-transform: uppercase;
}
.post-card-title {
margin: 0 0 0.4em;
font-size: 2.4rem;
transition: color 0.2s ease-in-out;
}
.no-image .post-card-title {
margin-top: 0;
}
.post-card-content {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.post-card-excerpt p {
margin-bottom: 1em;
display: -webkit-box;
overflow-y: hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
}
.post-card-meta {
display: flex;
align-items: center;
padding: 0;
}
.avatar-wrapper {
display: block;
width: 100%;
height: 100%;
background: color.scale($color-lightgrey, $lightness: +8%);
border-radius: 100%;
object-fit: cover;
}
.post-card-meta .profile-image-wrapper,
.post-card-meta .avatar-wrapper {
position: relative;
}
.static-avatar {
display: block;
overflow: hidden;
margin: 0 0 0 -6px;
width: 36px;
height: 36px;
border-radius: 100%;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.post-card-byline-content {
flex: 1 1 50%;
display: flex;
flex-direction: column;
margin: 0 0 0 8px;
color: color.scale($color-midgrey, $lightness: +10%);
font-size: 1.4rem;
line-height: 1.2em;
font-weight: 400;
}
.post-card-byline-content span {
margin: 0;
}
.post-card-byline-content a {
color: color.scale($color-darkgrey, $lightness: +15%);
font-weight: 600;
}
.post-card-byline-date {
font-size: 1.3rem;
line-height: 1.5em;
}
.post-card-byline-date .bull {
display: inline-block;
margin: 0 2px;
opacity: 0.6;
}
.single-author-byline {
display: flex;
flex-direction: column;
margin-left: 5px;
color: color.scale($color-midgrey, $lightness: -10%);
font-size: 1.3rem;
line-height: 1.4em;
font-weight: 500;
}
.single-author {
display: flex;
align-items: center;
}
.single-author .static-avatar {
margin-left: -2px;
}
.single-author-name {
display: inline-block;
}
/* Special Styling for home page grid (below):
The first post in the list is styled to be bigger than the others and take over
the full width of the grid to give it more emphasis. Wrapped in a media query to
make sure this only happens on large viewports / desktop-ish devices.
*/
@media (min-width: 1001px) {
.post-card-large {
grid-column: 1 / span 3;
display: grid;
grid-gap: 4vmin;
grid-template-columns: 1fr 1fr 1fr;
min-height: 280px;
border-top: 0;
}
.post-card-large:not(.no-image) .post-card-header {
margin-top: 0;
}
.post-card-large .post-card-image-link {
position: relative;
grid-column: 1 / span 2;
margin-bottom: 0;
min-height: 380px;
}
.post-card-large .post-card-image {
position: absolute;
width: 100%;
height: 100%;
}
.post-card-large .post-card-content {
justify-content: center;
}
.post-card-large .post-card-title {
margin-top: 0;
font-size: 3.2rem;
}
.post-card-large .post-card-excerpt p {
margin-bottom: 1.5em;
font-size: 1.7rem;
line-height: 1.55em;
-webkit-line-clamp: 8;
}
}
@media (max-width: 500px) {
.post-card-title {
font-size: 1.9rem;
}
.post-card-excerpt {
font-size: 1.6rem;
}
}
</style>

View File

@ -0,0 +1,43 @@
---
import PostPreview from "../components/PostPreview.astro";
import type { Settings, Post } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
posts: Post[];
settings: Settings;
isHome?: boolean;
};
const { posts, settings, isHome = false } = Astro.props as Props;
---
<div class="post-feed">
{posts.map((post: Post, index: number) => (
<PostPreview
post={post}
index={index}
settings={settings}
isHome={isHome}
/>
))}
</div>
<style lang="scss">
.post-feed {
position: relative;
display: grid;
grid-gap: 4vmin;
grid-template-columns: 1fr 1fr 1fr;
padding: 4vmin 0;
}
@media (max-width: 1000px) {
.post-feed {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 700px) {
.post-feed {
grid-template-columns: 1fr;
grid-gap: 40px;
}
}
</style>

View File

@ -0,0 +1,51 @@
---
import { getGhostImgPath } from "../utils";
import type { Settings, Tag } from "@matthiesenxyz/astro-ghostcms/api";
export type Props = {
tag: Tag;
addClass?: string;
settings: Settings;
};
const { tag, addClass = "", settings } = Astro.props;
---
<a
href={`/tag/${tag.slug}`}
title={tag.name}
aria-label={tag.name}
class={`tag-card ${addClass ? addClass : ""}`}
style={tag.accent_color ? `--color-accent:${tag.accent_color}` : ""}
>
{
tag.feature_image && (
<div class="tag-card-media">
<img
class="tag-card-img"
data-srcset={`
${getGhostImgPath(settings.url, tag.feature_image, 100)} 100w,
${getGhostImgPath(settings.url, tag.feature_image, 300)} 300w
`}
srcset={`
${getGhostImgPath(settings.url, tag.feature_image, 100)} 100w,
${getGhostImgPath(settings.url, tag.feature_image, 300)} 300w
`}
data-sizes="auto"
data-src={getGhostImgPath(settings.url, tag.feature_image, 300)}
src={getGhostImgPath(settings.url, tag.feature_image, 300)}
alt={tag.name}
sizes="200px"
/>
</div>
)
}
<div class="tag-card-content">
<h4 class="tag-card-name">{tag.name}</h4>
{tag.count && <span class="tag-card-count">{tag.count.posts}+ Posts</span>}
</div>
</a>
<style lang="scss">
.tag-card {
text-align: center;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

View File

@ -0,0 +1 @@
/// <reference types="astro/client" />

View File

@ -0,0 +1,128 @@
---
import type { Settings } from "@matthiesenxyz/astro-ghostcms/api";
import { AstroFont } from "astro-font";
import MainLayout from "../components/MainLayout.astro";
import { ViewTransitions } from "astro:transitions";
export type Props = {
content?: {
title: string;
description: string;
};
bodyClass?: string;
settings: Settings;
image?: string;
permalink?: string;
};
const { content, permalink, image, settings, bodyClass = "" } = Astro.props as Props;
const ghostAccentColor = settings.accent_color;
---
<html lang="en">
<head>
<Fragment set:html={settings.codeinjection_head}>
<AstroFont
config={[
{
src: [],
name: "Inter",
preload: false,
display: "swap",
selector: "html",
fallback: "sans-serif",
googleFontsURL: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
},
]}
/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{content ? content.title + "|" + settings.title : settings.title}</title>
<ViewTransitions />
<meta name="title" content={content ? content.title : settings.title} />
{content?.description && <meta name="description" content={content.description} />}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href={settings.icon} />
<link rel="shortcut icon" type="image/png" sizes="16x16" href={settings.icon} />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
<!-- Open Graph Tags (Facebook) -->
<meta property="og:type" content="website" />
<meta property="og:title" content={content?.title} />
{permalink && <meta property="og:url" content={permalink} />}
{content?.description && <meta property="og:description" content={content.description} />}
{image && <meta property="og:image" content={image} />}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={content?.title} />
{permalink && <meta property="twitter:url" content={permalink} />}
{content?.description && <meta property="twitter:description" content={content.description} />}
{image && <meta property="twitter:image" content={image} />}
<!-- Link to the global style, or the file that imports constructs -->
<link
rel="preload"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css"
as="style"
/>
<link
rel="preload"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"
as="script"
/>
<link
rel="preload stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
crossorigin
/>
<script
async
defer
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"
>
</script>
</head>
<body class={bodyClass}>
<MainLayout settings={settings}>
<slot />
</MainLayout>
<style
lang="scss"
is:global
define:vars={{ "ghost-accent-color": ghostAccentColor }}
>
@import "../styles/reset.scss";
@import "../styles/app.scss";
@mixin mq-sm {
@media only screen and (min-width: 36em) {
@content;
}
}
@mixin mq-md {
@media only screen and (min-width: 48em) {
@content;
}
}
@mixin mq-lg {
@media only screen and (min-width: 62em) {
@content;
}
}
@mixin mq-xl {
@media only screen and (min-width: 75em) {
@content;
}
}
</style>
</body>
</html>

View File

@ -0,0 +1,45 @@
---
import type { InferGetStaticPropsType } from 'astro';
import DefaultPageLayout from "../layouts/default.astro";
import Page from "../components/Page.astro";
import Post from "../components/Post.astro";
import { getSettings, getAllPages, getAllPosts, invariant } from "@matthiesenxyz/astro-ghostcms/api";
export async function getStaticPaths() {
const [posts, pages, settings] = await Promise.all([getAllPosts(), await getAllPages(), await getSettings()]);
const allPosts = [...posts, ...pages];
return allPosts.map((post) => ({
params: { slug: post.slug },
props: { post, posts, settings },
}));
}
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
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}`;
---
<DefaultPageLayout
content={{ title: post.title, description: post.excerpt }}
settings={settings}
bodyClass={bodyClass}
>
{
post.primary_author ? (
<Post
post={post}
settings={settings}
postClass={postClass}
posts={posts}
/>
) : (
<Page page={post} settings={settings} pageClass={postClass} />
)
}
</DefaultPageLayout>
<style lang="scss"></style>

View File

@ -0,0 +1,49 @@
---
import type { GetStaticPathsOptions, Page } from 'astro';
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 { getSettings, getAllPosts, invariant, type Post } from "@matthiesenxyz/astro-ghostcms/api";
export async function getStaticPaths({ paginate }:GetStaticPathsOptions) {
const posts = await getAllPosts();
return paginate(posts, {
pageSize: 5,
});
}
export type Props = {
page: Page<Post>
};
const settings = await getSettings();
invariant(settings, "Settings are required");
const title = settings.title;
const description = settings.description;
const { page } = Astro.props as Props;
---
<DefaultPageLayout
content={{ title, description }}
settings={settings}
bodyClass={"home-template"}
>
<HeroContent
mainTitle={"Archives"}
description={"All the posts"}
featureImg={settings.cover_image || ""}
settings={settings}
addClass={"hero-cta bg-gradient"}
>
<h1 class="site-title" slot="title">Archives</h1>
</HeroContent>
<main id="site-main" class="site-main outer">
<div class="inner posts">
<PostPreviewList posts={page.data} settings={settings} />
<Paginator {page} />
</div>
</main>
</DefaultPageLayout>

View File

@ -0,0 +1,120 @@
---
import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro';
import DefaultPageLayout from "../../layouts/default.astro";
import PostPreviewList from "../../components/PostPreviewList.astro";
import { getAllPosts, getAllAuthors, getSettings, twitter, facebook, invariant } from "@matthiesenxyz/astro-ghostcms/api";
export async function getStaticPaths() {
const posts = await getAllPosts();
const { authors } = await getAllAuthors();
const settings = await getSettings();
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<typeof getStaticPaths>;
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
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}`;
---
<DefaultPageLayout
bodyClass={`author-template author-${author.slug}`}
content={{ title, description }}
settings={settings}
>
<section class="outer author-template">
<div class="inner posts">
<header class="author-profile">
<div class="author-profile-content">
{author.profile_image ? (
<img
class="author-profile-pic"
src={author.profile_image}
alt={author.name}
/>
) : (
<span class="author-profile-pic">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M3.513 18.998C4.749 15.504 8.082 13 12 13s7.251 2.504 8.487 5.998C18.47 21.442 15.417 23 12 23s-6.47-1.558-8.487-4.002zM12 12c2.21 0 4-2.79 4-5s-1.79-4-4-4-4 1.79-4 4 1.79 5 4 5z"
fill="#FFF"
/>
</g>
</svg>
</span>
)}
<h1>{author.name}</h1>
<p>
{author.bio
? author.bio
: author.count?.posts || 0 > 0
? `${author.count?.posts} Posts`
: "No posts"}
</p>
<div class="author-profile-meta">
{author.location && (
<div class="author-profile-location">📍 {author.location}</div>
)}
{author.website && (
<span>
<a
class="author-profile-social-link"
href={author.website}
target="_blank"
rel="noopener"
>
{author.website}
</a>
</span>
)}
{author.twitter && (
<span>
<a
class="author-profile-social-link"
href={twitter(author.twitter)}
target="_blank"
rel="noopener"
>
{author.twitter}
</a>
</span>
)}
{author.facebook && (
<span>
<a
class="author-profile-social-link"
href={facebook(author.facebook)}
target="_blank"
rel="noopener"
>
{author.facebook}
</a>
</span>
)}
</div>
</div>
</header>
<PostPreviewList posts={posts} settings={settings} />
</div>
</section>
</DefaultPageLayout>

View File

@ -0,0 +1,43 @@
---
import DefaultPageLayout from "../layouts/default.astro";
import AuthorCard from "../components/AuthorCard.astro";
import { getAllAuthors, getSettings, invariant } from "@matthiesenxyz/astro-ghostcms/api";
let title = "All Authors";
let description = "All the authors";
const { authors } = await getAllAuthors();
const settings = await getSettings();
invariant(settings, 'Settings not found');
---
<DefaultPageLayout content={{ title, description }} settings={settings}>
<section class="outer">
<div class="inner posts">
<h1>
{settings.title}
</h1>
<div class="page__excerpt m-t text-acc-3 text-center text-lg">
Collection of Tags
</div>
<div class="author-feed">
{authors.map((author) => (
<article class="post-card ">
<AuthorCard author={author} settings={settings} />
</article>
))}
</div>
</div>
</section>
</DefaultPageLayout>
<style lang="scss">
.author-feed {
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-auto-rows: 1fr;
grid-column-gap: 9vh;
grid-row-gap: 10vh;
padding: 4vmin 0;
}
</style>

View File

@ -0,0 +1,31 @@
---
import DefaultPageLayout from "../layouts/default.astro";
import PostPreviewList from "../components/PostPreviewList.astro";
import HeroContent from "../components/HeroContent.astro";
import { getPosts, getSettings, invariant } from "@matthiesenxyz/astro-ghostcms/api";
const { posts } = await getPosts();
const settings = await getSettings();
invariant(settings, 'Settings not found');
const title = settings.title;
const description = settings.description;
---
<DefaultPageLayout
content={{ title, description }}
settings={settings}
bodyClass={"home-template"}
>
<HeroContent
mainTitle={settings.title}
description={settings.description}
featureImg={settings?.cover_image || ""}
settings={settings}
addClass={"hero-cta bg-gradient"}
/>
<main id="site-main" class="site-main outer">
<div class="inner posts">
<PostPreviewList posts={posts} settings={settings} isHome={true} />
</div>
</main>
</DefaultPageLayout>

View File

@ -0,0 +1,101 @@
---
import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro';
import DefaultPageLayout from "../../layouts/default.astro";
import PostPreview from "../../components/PostPreview.astro";
import { getAllPosts, getAllTags, getSettings, invariant } from "@matthiesenxyz/astro-ghostcms/api";
import { getGhostImgPath } from "../../utils";
export async function getStaticPaths() {
const posts = await getAllPosts();
const { tags } = await getAllTags();
const settings = await getSettings();
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<typeof getStaticPaths>;
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
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}`;
---
<DefaultPageLayout
bodyClass={`tag-template tag-${tag.slug}`}
content={{ title, description }}
settings={settings}
>
<main id="site-main" class="site-main outer">
<div class="inner posts">
<div class="post-feed">
<section class="post-card post-card-large">
{tag.feature_image && (
<div class="post-card-image-link">
<img
class="post-card-image"
srcset={`${getGhostImgPath(
settings.url,
tag.feature_image,
300
)} 300w,
${getGhostImgPath(
settings.url,
tag.feature_image,
600
)} 600w,
${getGhostImgPath(
settings.url,
tag.feature_image,
1000
)} 1000w,
${getGhostImgPath(
settings.url,
tag.feature_image,
2000
)} 2000w`}
sizes="(max-width: 1000px) 400px, 800px"
src={getGhostImgPath(settings.url, tag.feature_image, 600)}
alt={tag.name}
loading="lazy"
/>
</div>
)}
<div class="post-card-content">
<div class="post-card-content-link">
<header class="post-card-header">
<div class="post-card-primary-tag">Tagged</div>
<h2 class="post-card-title">{tag.name}</h2>
</header>
<div class="post-card-excerpt">
<p>
{tag.description
? tag.description
: `A collection of ${tag.count?.posts || 0 } Post${
tag.count?.posts ?? 0 > 1 ? "s" : ""
}`}
</p>
</div>
</div>
</div>
</section>
{posts.map((post, index) => (
<PostPreview post={post} index={index} settings={settings} />
))}
</div>
</div>
</main>
</DefaultPageLayout>

View File

@ -0,0 +1,49 @@
---
import DefaultPageLayout from "../layouts/default.astro";
import TagCard from "../components/TagCard.astro";
import { getSettings, getAllTags, invariant } from "@matthiesenxyz/astro-ghostcms/api";
let title = "All Tags";
let description = "All the tags used so far...";
const { tags } = await getAllTags();
const settings = await getSettings();
invariant(settings, "Settings not found");
---
<DefaultPageLayout content={{ title, description }} settings={settings}>
<section class="outer">
<div class="inner posts">
<h1>
{settings.title}
</h1>
<div class="page__excerpt m-t text-acc-3 text-center text-lg">
Collection of Tags
</div>
<div class="tag-feed">
{
tags
.filter((tag) => tag.slug && !tag.slug.startsWith("hash-"))
.map((tag) => (
<article class="post-card ">
<TagCard tag={tag} settings={settings} />
</article>
))
}
</div>
</div>
</section>
</DefaultPageLayout>
<style lang="scss">
.tag-feed {
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-auto-rows: 1fr;
grid-column-gap: 9vh;
grid-row-gap: 10vh;
padding: 4vmin 0;
}
</style>

View File

@ -0,0 +1,395 @@
/* 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;
padding-right: auto;
}
}
/* 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
<script>document.documentElement.classList.add('dark-mode');</script>
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: var(--color-midgrey);
}
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%));
}
html.dark-mode figcaption {
color: rgba(255, 255, 255, 0.6);
}

View File

@ -0,0 +1,464 @@
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;
font-family: sans-serif;
-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;
}

View File

@ -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;

View File

@ -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 = <T>(
arr: Array<T>,
fn: (element: T, step: T) => number
): Array<T> =>
arr.filter(
(element, index) => arr.findIndex((step) => fn(element, step)) === index
);

View File

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}