Merge Branch create-util (#19)
This merges the branch containing the create-astro-ghostcms utility.
This commit is contained in:
commit
cddb8a89e0
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -18,9 +18,10 @@ In this Repo you will also find the Following:
|
|||
- `www`: [Public Site](https://astro-ghostcms.xyz)
|
||||
- `playground`: Development and Testing
|
||||
- `packages/`:
|
||||
- `create-astro-ghostcms`: CLI Utility to quickly deploy new Astro-GhostCMS projects.
|
||||
- `astro-ghostcms`: The main Integration!
|
||||
- `astro-ghostcms-theme-default`: The Default theme in integration mode
|
||||
- `tsconfig`: *LOCAL* Development package for `@ts-ghost/core-api`
|
||||
- `astro-ghostcms-theme-default`: The Default theme in integration mode.
|
||||
- `tsconfig`: *LOCAL* Development package for `@ts-ghost/core-api`.
|
||||
|
||||
### Notices
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
"node": ">=18.19.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --apply .",
|
||||
"playground:dev": "pnpm --filter playground dev",
|
||||
"www:dev": "pnpm --filter www dev",
|
||||
"demo:dev": "pnpm --filter demo dev",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --apply .",
|
||||
"base": "pnpm i --frozen-lockfile",
|
||||
"api:test": "pnpm --filter astro-ghostcms test",
|
||||
"api:test:watch": "pnpm --filter astro-ghostcms test:watch",
|
||||
"api:test:coverage": "pnpm --filter astro-ghostcms test:coverage",
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1,33 @@
|
|||
# `@matthiesenxyz/create-astro-ghostcms`
|
||||
|
||||
Utility to quickly get started with our Integration and astro.
|
||||
|
||||

|
||||
|
||||
The Default install method requires `pnpm` if you dont have `pnpm` installed you can change this by using the command below with the following argument `--pkg-manager <pkg-mngr>` with the Package manager of your choice(i.e npm, yarn).
|
||||
|
||||
```sh
|
||||
npx @matthiesenxyz/create-astro-ghostcms
|
||||
|
||||
# OR
|
||||
|
||||
npx @matthiesenxyz/create-astro-ghostcms <template> <project_directory>
|
||||
```
|
||||
|
||||
## Available command args:
|
||||
|
||||
```sh
|
||||
npx @matthiesenxyz/create-astro-ghostcms --<arg>
|
||||
# `--help` : Calls internal getHelp Function
|
||||
# `--install` : Sets Install Dependencies to 'true'
|
||||
# `--git` : Initiates git Repo
|
||||
# `--dry` : Shows you what the command will do. (NO CHANGES WILL BE MADE)
|
||||
# `--pkg-manager` : Specify your Package manager(i.e. npm, yarn | DEFAULT: pnpm)
|
||||
```
|
||||
|
||||
## Available templates
|
||||
|
||||
| Template | Description |
|
||||
| ------------ | ----------------------------------------------------- |
|
||||
| `basic` | Basic Setup with astro-ghostcms and theme-default |
|
||||
| `starterkit` | Integration in API-Only Mode with customizable theme |
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
// biome-ignore lint/suspicious/noRedundantUseStrict: <explanation>
|
||||
'use strict';
|
||||
|
||||
const currentVersion = process.versions.node;
|
||||
const requiredMajorVersion = parseInt(currentVersion.split('.')[0], 10);
|
||||
const minimumMajorVersion = 18;
|
||||
|
||||
if (requiredMajorVersion < minimumMajorVersion) {
|
||||
console.error(`Node.js v${currentVersion} is out of date and unsupported!`);
|
||||
console.error(`Please use Node.js v${minimumMajorVersion} or higher.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import('./src/main.js').then(({ main }) => main());
|
|
@ -0,0 +1,9 @@
|
|||
export interface Context {
|
||||
dryRun: boolean;
|
||||
installDeps: boolean;
|
||||
initGitRepo: boolean;
|
||||
pkgManager: "npm" | "yarn" | "pnpm" | null;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export type Template = ["basic","starterkit"];
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "@matthiesenxyz/create-astro-ghostcms",
|
||||
"version": "0.0.1-dev42",
|
||||
"description": "Utility to quickly get started with our Integration and astro",
|
||||
"type": "module",
|
||||
"main": "./create-astro-ghostcms.mjs",
|
||||
"bin": {
|
||||
"create-astro-ghostcms": "./create-astro-ghostcms.mjs"
|
||||
},
|
||||
"exports": {
|
||||
".": "./create-astro-ghostcms.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
},
|
||||
"sideEffects": false,
|
||||
"author": {
|
||||
"email": "adam@matthiesen.xyz",
|
||||
"name": "Adam Matthiesen - MatthiesenXYZ",
|
||||
"url": "https://matthiesen.xyz"
|
||||
},
|
||||
"homepage": "https://astro-ghostcms.xyz/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MatthiesenXYZ/astro-ghostcms.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/MatthiesenXYZ/astro-ghostcms/issues",
|
||||
"email": "issues@astro-ghostcms.xyz"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src",
|
||||
"create-astro-ghostcms",
|
||||
"index.d.ts",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"types.d.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"arg": "^5.0.2",
|
||||
"execa": "^7.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"read-pkg": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/gunzip-maybe": "^1.4.0",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/tar-fs": "^2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.7",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"prettier": "^2.8.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.1.0",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.2.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,61 @@
|
|||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import * as p from "@clack/prompts";
|
||||
import c from "picocolors";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
*/
|
||||
export function getModulePaths(url) {
|
||||
const pathname = fileURLToPath(url);
|
||||
const parts = pathname.split("/");
|
||||
const basename = parts.pop();
|
||||
const dirname = parts.join(path.sep);
|
||||
return { pathname, dirname, basename };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {never}
|
||||
*/
|
||||
export function exitPrompt() {
|
||||
p.cancel(c.red("Operation Cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
export function isPathname(str) {
|
||||
return str.includes(path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
export function normalizePath(pathname) {
|
||||
if (os.platform() === "win32") {
|
||||
return path.win32.normalize(pathname);
|
||||
}
|
||||
return path.normalize(pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} ms
|
||||
*/
|
||||
export async function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {str is PackageManager}
|
||||
*/
|
||||
export function isPackageManager(str) {
|
||||
return str === "npm" || str === "yarn" || str === "pnpm";
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import("../../types").PackageManager} PackageManager
|
||||
*/
|
|
@ -0,0 +1,128 @@
|
|||
import path from "node:path";
|
||||
import arg from "arg";
|
||||
import fse from "fs-extra";
|
||||
import * as p from "@clack/prompts";
|
||||
import c from 'picocolors';
|
||||
import { exitPrompt, getModulePaths, isPackageManager } from "./lib/utils.js";
|
||||
import { createBasic } from "./runners/basic.js";
|
||||
import { createStarterKit } from "./runners/starterkit.js";
|
||||
|
||||
|
||||
export async function main() {
|
||||
const exit = () => process.exit(0);
|
||||
process.on("SIGINT", exit);
|
||||
process.on("SIGTERM", exit);
|
||||
|
||||
console.clear();
|
||||
|
||||
const argv = process.argv.slice(2).filter((arg) => arg !== "--");
|
||||
|
||||
const flags = arg(
|
||||
{
|
||||
"--help": Boolean,
|
||||
"--install": Boolean,
|
||||
"--git": Boolean,
|
||||
"--dry": Boolean,
|
||||
"--pkg-manager": String,
|
||||
"-h": "--help",
|
||||
"-i": "--install",
|
||||
"-g": "--git",
|
||||
"-p": "--pkg-manager",
|
||||
},
|
||||
{ argv, permissive: true }
|
||||
);
|
||||
const {
|
||||
"--help": help,
|
||||
"--install": installDeps,
|
||||
"--git": initGitRepo,
|
||||
"--dry": dryRun,
|
||||
"--pkg-manager": pkgManager,
|
||||
} = flags;
|
||||
|
||||
// 0. Show help text and bail
|
||||
if (help) {
|
||||
console.log(getHelp());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Package Version for Intro
|
||||
const { pathname } = getModulePaths(import.meta.url);
|
||||
const iJSON = path.resolve(pathname, "..", "..", 'package.json');
|
||||
const pJSON = await fse.readJson(iJSON);
|
||||
const pkgVer = pJSON.version;
|
||||
|
||||
// 1. Say hello!
|
||||
p.intro(c.bgMagenta(c.black(` ${c.bold("Astro-GhostCMS Create Utility - By MatthiesenXYZ")} ${c.underline(c.bold(c.blue(`( v${pkgVer} )`)))} ${c.italic(dryRun ? "[Dry Run] ":" ")}`)))
|
||||
|
||||
const gettingStarted = `${c.white(c.bold('Want to Initiate a git repo at the same time as deploying your project?'))} \n - ${c.white(`Use ${c.yellow('--git')} at the end of the command`)} \n ${c.white(c.bold(`Using a package manager other than ${c.cyan(c.bold('pnpm'))}?`))} \n - ${c.white(`Use ${c.yellow('--pkg-manager npm')} or ${c.yellow('--pkg-manager yarn')}.`)}`
|
||||
|
||||
p.note(gettingStarted)
|
||||
|
||||
// 2. Get template to set up
|
||||
let [template, ...args] = flags._;
|
||||
if (template && !isValidTemplate(template)) {
|
||||
p.log.warning(c.red(`"${template}" isn't a valid template`));
|
||||
template = null;
|
||||
}
|
||||
if (!template) {
|
||||
const answer = await p.select({
|
||||
message: `${c.cyan('Which template would you like to use?')}`,
|
||||
options: [
|
||||
{
|
||||
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",
|
||||
});
|
||||
if (p.isCancel(answer)) exitPrompt();
|
||||
template = answer;
|
||||
}
|
||||
|
||||
// 2. Construct context to pass to template functions
|
||||
/** @type {Context} */
|
||||
const ctx = {
|
||||
dryRun,
|
||||
installDeps,
|
||||
initGitRepo,
|
||||
pkgManager: isPackageManager(pkgManager) ? pkgManager : null,
|
||||
args,
|
||||
};
|
||||
|
||||
// 3. Call template functions
|
||||
switch (template) {
|
||||
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}`));
|
||||
}
|
||||
|
||||
// 4. Huzzah!
|
||||
p.outro(c.reset(`Problems? ${c.underline(c.cyan('https://github.com/MatthiesenXYZ/astro-ghostcms/issues'))}`));
|
||||
}
|
||||
|
||||
function getHelp() {
|
||||
return `${c.yellow('Need Help? Check the Docs!')} ${c.underline(c.cyan('https://astro-ghostcms.xyz/docs'))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} template
|
||||
* @returns {template is Template}
|
||||
*/
|
||||
function isValidTemplate(template) {
|
||||
return ["basic","starterkit"].includes(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").Template} Template
|
||||
* @typedef {import("../types").PackageManager} PackageManager
|
||||
* @typedef {import("../types").Context} Context
|
||||
*/
|
|
@ -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 = "basic";
|
||||
|
||||
/** @param {Context} ctx */
|
||||
export async function createBasic(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
|
||||
*/
|
|
@ -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
|
||||
*/
|
|
@ -0,0 +1,2 @@
|
|||
CONTENT_API_KEY=a33da3965a3a9fb2c6b3f63b48
|
||||
CONTENT_API_URL=https://ghostdemo.matthiesen.xyz
|
|
@ -0,0 +1,21 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Matthiesen XYZ
|
||||
|
||||
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.
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import GhostCMS from "@matthiesenxyz/astro-ghostcms";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://example.com/",
|
||||
integrations: [GhostCMS({
|
||||
ghostURL: 'https://ghostdemo.matthiesen.xyz'
|
||||
})]
|
||||
});
|
|
@ -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": {
|
||||
"astro": "^4.2.4",
|
||||
"@matthiesenxyz/astro-ghostcms": "^3.1.5",
|
||||
"@matthiesenxyz/astro-ghostcms-theme-default": "^0.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"sass": "^1.70.0"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="astro/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly CONTENT_API_KEY: string
|
||||
readonly CONTENT_API_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
|
@ -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
|
4
packages/create-astro-ghostcms/src/templates/starterkit/.vscode/extensions.json
vendored
Normal file
4
packages/create-astro-ghostcms/src/templates/starterkit/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,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.
|
|
@ -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!
|
|
@ -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
|
||||
})
|
||||
]
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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> © 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
← Prev
|
||||
</a>
|
||||
)}
|
||||
{page.url.next && (
|
||||
<a class="action__go-to-x" href={page.url.next} title="Go to Next">
|
||||
Next →
|
||||
</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">•</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>
|
|
@ -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">•</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>
|
|
@ -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>
|
|
@ -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 |
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Default",
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules", "templates/*"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"checkJs": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"noEmit": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"inlineSources": false,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
// Non-null assertions are a PITA when checking JS files
|
||||
"strictNullChecks": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./index";
|
||||
export type PackageManager = "npm" | "yarn" | "pnpm";
|
||||
export type Serializable = string | object | number | boolean | bigint;
|
667
pnpm-lock.yaml
667
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue