init
This commit is contained in:
commit
307e3a913a
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
src/testPosts.json
|
||||
/result
|
||||
/todo.txt
|
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1682669017,
|
||||
"narHash": "sha256-Vi+p4y3wnl0/4gcwTdmCO398kKlDaUrNROtf3GOD2NY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7449971a3ecf857b4a554cf79b1d9dcc1a4647d8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
description = "cohost-blogger";
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
package = builtins.fromJSON (builtins.readFile ./package.json);
|
||||
in
|
||||
rec {
|
||||
packages = flake-utils.lib.flattenTree rec {
|
||||
cohost-blogger = pkgs.buildNpmPackage {
|
||||
pname = "cohost-blogger";
|
||||
inherit (package) version;
|
||||
|
||||
npmDepsHash = "sha256-ixRfoMWVKPomqZJuvKfE2dDrqq7DrTryMCFT37MY/c8=";
|
||||
|
||||
doCheck = true;
|
||||
|
||||
postCheck = ''
|
||||
mkdir -p $out
|
||||
mv build $out/
|
||||
ln -s $out/lib/node_modules/cohost-blogger/package.json $out/build/package.json
|
||||
ln -s $out/lib/node_modules/cohost-blogger/node_modules $out/build/node_modules
|
||||
makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/cohost-blogger \
|
||||
--add-flags $out/build/index.js \
|
||||
--chdir $out/lib/node_modules/cohost-blogger/
|
||||
'';
|
||||
|
||||
src = ./.;
|
||||
};
|
||||
};
|
||||
|
||||
defaultPackage = packages.cohost-blogger;
|
||||
|
||||
nixosModule = { config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.cohost-blogger;
|
||||
in {
|
||||
options.services.cohost-blogger = {
|
||||
enable = mkEnableOption "Enables the cohost-blogger server";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Which domain to host the server under; if disabled, NGINX is not used";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3500;
|
||||
};
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = self.packages.${system}.default;
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services."cohost-blogger" = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
PORT = cfg.port;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
ExecStart = "${getExe cfg.package}";
|
||||
DynamicUser = "yes";
|
||||
RuntimeDirectory = "cohost-blogger";
|
||||
RuntimeDirectoryMode = "0755";
|
||||
StateDirectory = "cohost-blogger";
|
||||
StateDirectoryMode = "0700";
|
||||
CacheDirectory = "cohost-blogger";
|
||||
CacheDirectoryMode = "0750";
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx = mkIf cfg.domain {
|
||||
virtualHosts."${cfg.domain}" = {
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${cfg.port}/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "cohost-blogger",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^1.2.4",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.3.1",
|
||||
"highlight.js": "^11.7.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"insane": "^2.6.2",
|
||||
"marked": "^4.3.0",
|
||||
"minify-xml": "^3.4.0",
|
||||
"modern-normalize": "^1.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-sanitize": "^5.0.1",
|
||||
"rehype-stringify": "^9.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-parse": "^10.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"sass": "^1.62.0",
|
||||
"stringify-entities": "^4.0.3",
|
||||
"timeago.js": "^4.0.2",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^4.1.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
type ASTMap = RootAST | ElementAST | TextAST
|
||||
|
||||
interface RootAST {
|
||||
type: 'root',
|
||||
position?: any,
|
||||
children: ASTMap[]
|
||||
}
|
||||
interface ElementAST {
|
||||
type: 'element',
|
||||
position?: any,
|
||||
children: ASTMap[],
|
||||
properties: Record<string, any>,
|
||||
tagName: string
|
||||
}
|
||||
interface TextAST {
|
||||
type: 'text',
|
||||
position?: any,
|
||||
value: string
|
||||
}
|
||||
|
||||
interface MarkdownStorageBlock {
|
||||
type: 'markdown',
|
||||
markdown: {
|
||||
/** Raw markdown to be parsed at render-time. */
|
||||
content: string,
|
||||
},
|
||||
}
|
||||
|
||||
type AttachmentId = string
|
||||
|
||||
interface AttachmentStorageBlock {
|
||||
type: 'attachment',
|
||||
attachment: {
|
||||
/** ID for the [[`Attachment`]] to be rendered. */
|
||||
attachmentId: AttachmentId,
|
||||
altText: string?,
|
||||
previewURL: string,
|
||||
fileURL: string,
|
||||
width: number?,
|
||||
height: number?,
|
||||
},
|
||||
}
|
||||
|
||||
type StorageBlock = MarkdownStorageBlock | AttachmentStorageBlock
|
||||
|
||||
enum AccessResult {
|
||||
Allowed = "allowed",
|
||||
NotAllowed = "not-allowed",
|
||||
LogInFirst = "log-in-first",
|
||||
Blocked = "blocked",
|
||||
}
|
||||
|
||||
enum LimitedVisibilityReason {
|
||||
None = "none",
|
||||
LogInFirst = "log-in-first",
|
||||
}
|
||||
|
||||
type PostId = number
|
||||
|
||||
enum PostState {
|
||||
Unpublished = 0,
|
||||
Published,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
interface Post {
|
||||
postId: PostId,
|
||||
headline: string,
|
||||
publishedAt: string?,
|
||||
filename: string,
|
||||
transparentShareOfPostId: PostId?,
|
||||
shareOfPostId: PostId?,
|
||||
state: PostStateEnum,
|
||||
numComments: number,
|
||||
cws: string[],
|
||||
tags: tags[],
|
||||
hasCohostPlus: boolean,
|
||||
pinned: boolean,
|
||||
commentsLocked: boolean,
|
||||
sharesLocked: boolean,
|
||||
blocks: StorageBlock[], // TODO
|
||||
plainTextBody: string,
|
||||
postingProject: Project;
|
||||
shareTree: unknown[]; // TODO
|
||||
numSharedComments: number;
|
||||
relatedProjects: Project[];
|
||||
singlePostPageUrl: string;
|
||||
effectiveAdultContent: boolean;
|
||||
isEditor: boolean;
|
||||
hasAnyContributorMuted: boolean;
|
||||
contributorBlockIncomingOrOutgoing: boolean;
|
||||
postEditUrl: string;
|
||||
isLiked: boolean;
|
||||
canShare: boolean;
|
||||
canPublish: boolean;
|
||||
limitedVisibilityReason: LimitedVisibilityReason;
|
||||
astMap: {
|
||||
initial: string,
|
||||
initialLength: number,
|
||||
expanded: string,
|
||||
expandedLength: number,
|
||||
};
|
||||
}
|
||||
|
||||
type CommentId = string
|
||||
|
||||
interface Comment {
|
||||
comment: {
|
||||
commentId: CommentId,
|
||||
postedAtISO: string,
|
||||
deleted: boolean,
|
||||
body: string,
|
||||
children: Comment[],
|
||||
postId: PostId,
|
||||
inReplyTo: Comment?,
|
||||
hasCohostPlus: boolean,
|
||||
hidden: boolean
|
||||
},
|
||||
canInteract: AccessResult,
|
||||
canEdit: AccessResult,
|
||||
canHide: AccessResult,
|
||||
poster: Project
|
||||
}
|
||||
|
||||
type ProjectId = number
|
||||
type ProjectHandle = string
|
||||
|
||||
enum ProjectPrivacy {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
enum ProjectFlag {
|
||||
Staff = "staff",
|
||||
StaffMember = "staffMember",
|
||||
FriendOfTheSite = "friendOfTheSite",
|
||||
NoTransparentAvatar = "noTransparentAvatar",
|
||||
Suspended = "suspended",
|
||||
Automated = "automated", // used for the bot badge
|
||||
Parody = "parody", // used for the "un-verified" badge
|
||||
};
|
||||
|
||||
enum AvatarShape {
|
||||
Circle = "circle",
|
||||
RoundRect = "roundrect",
|
||||
Squircle = "squircle",
|
||||
CapsuleBig = "capsule-big",
|
||||
CapsuleSmall = "capsule-small",
|
||||
Egg = "egg",
|
||||
}
|
||||
|
||||
enum LoggedOutPostVisibility {
|
||||
Public = "public",
|
||||
None = "none"
|
||||
}
|
||||
|
||||
interface Project {
|
||||
projectId: ProjectId,
|
||||
handle: ProjectHandle,
|
||||
displayName: string,
|
||||
dek: string,
|
||||
description: string,
|
||||
avatarURL: string,
|
||||
avatarPreviewURL: string,
|
||||
headerURL: string?,
|
||||
headerPreviewURL: string?,
|
||||
privacy: ProjectPrivacyEnum,
|
||||
url: string?,
|
||||
pronouns: string?,
|
||||
flags: ProjectFlag[],
|
||||
avatarShape: AvatarShape,
|
||||
loggedOutPostVisibility: LoggedOutPostVisibility,
|
||||
frequentlyUsedTags: string[],
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,105 @@
|
|||
@use "sass:color";
|
||||
|
||||
$accent-color: #c177af;
|
||||
|
||||
:root {
|
||||
line-height: 1.5;
|
||||
font-size: 20px;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
--text-color: #111;
|
||||
--text-color-light: #444;
|
||||
--background-color: #eee;
|
||||
--card-overlay: rgba(255, 255, 255, 0.6);
|
||||
--line-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
--accent-color: #{$accent-color};
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
--text-color: #fff;
|
||||
--text-color-light: #aaa;
|
||||
--background-color: #111;
|
||||
--card-overlay: rgba(25, 25, 25, 0.6);
|
||||
--line-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
accent-color: var(--accent-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--accent-color);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
blockquote, dl, dd, h1, h2, h3, h4, h5, h6, figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--line-color);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: monospace;
|
||||
}
|
||||
pre code {
|
||||
white-space: pre-wrap;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
border-left: 0.25rem var(--line-color) solid;
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 1.6em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.post {
|
||||
isolation: isolate;
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
word-break: break-word;
|
||||
|
||||
&.preview {
|
||||
max-height: 40vh;
|
||||
overflow: hidden;
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--text-color-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.link-wrapper, .link-wrapper:hover {
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
<!-- svelte-ignore css-unused-selector -->
|
||||
|
||||
<style lang="scss">
|
||||
@font-face {
|
||||
font-family: ibm_vga;
|
||||
src: url(/WebPlus_IBM_VGA_8x16.woff) format("woff");
|
||||
src: url(/PxPlus_IBM_VGA_8x16.ttf) format("truetype");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ee64ae;
|
||||
text-shadow: 0 0 0.5px #ee64ae;
|
||||
transition: 0.1s color, 0.1s text-shadow;
|
||||
cursor: pointer;
|
||||
}
|
||||
a:hover {
|
||||
color: #f075b7;
|
||||
text-shadow: 0 0 2px #ee64ae;
|
||||
}
|
||||
b {
|
||||
font-weight: normal;
|
||||
text-shadow: 0 0 1px currentColor;
|
||||
transition: 0.1s color, 0.1s text-shadow;
|
||||
}
|
||||
|
||||
b {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #46465c;
|
||||
}
|
||||
b.black {
|
||||
color: #634560;
|
||||
}
|
||||
.red {
|
||||
color: #e95678;
|
||||
}
|
||||
b.red {
|
||||
color: #ec6a88;
|
||||
}
|
||||
.green {
|
||||
color: #29d398;
|
||||
}
|
||||
b.green {
|
||||
color: #3fdaa4;
|
||||
}
|
||||
.yellow {
|
||||
color: #fab795;
|
||||
}
|
||||
b.yellow {
|
||||
color: #fbc3a7;
|
||||
}
|
||||
.blue {
|
||||
color: #26bbd9;
|
||||
}
|
||||
b.blue {
|
||||
color: #3fc6de;
|
||||
}
|
||||
.magenta {
|
||||
color: #ee64ae;
|
||||
}
|
||||
b.magenta {
|
||||
color: #f075b7;
|
||||
}
|
||||
.cyan {
|
||||
color: #59e3e3;
|
||||
}
|
||||
b.cyan {
|
||||
color: #6be6e6;
|
||||
}
|
||||
.white {
|
||||
color: #fdf0ed;
|
||||
}
|
||||
b.white {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.black-bg {
|
||||
background-color: #46465c;
|
||||
}
|
||||
.red-bg {
|
||||
background-color: #e95678;
|
||||
}
|
||||
.green-bg {
|
||||
background-color: #29d398;
|
||||
}
|
||||
.yellow-bg {
|
||||
background-color: #fab795;
|
||||
}
|
||||
.blue-bg {
|
||||
background-color: #26bbd9;
|
||||
}
|
||||
.magenta-bg {
|
||||
background-color: #ee64ae;
|
||||
}
|
||||
.cyan-bg {
|
||||
background-color: #59e3e3;
|
||||
}
|
||||
.white-bg {
|
||||
background-color: #fdf0ed;
|
||||
}
|
||||
.bright-black-bg {
|
||||
background-color: #634560;
|
||||
}
|
||||
.bright-red-bg {
|
||||
background-color: #ec6a88;
|
||||
}
|
||||
.bright-green-bg {
|
||||
background-color: #3fdaa4;
|
||||
}
|
||||
.bright-yellow-bg {
|
||||
background-color: #fbc3a7;
|
||||
}
|
||||
.bright-blue-bg {
|
||||
background-color: #3fc6de;
|
||||
}
|
||||
.bright-magenta-bg {
|
||||
background-color: #f075b7;
|
||||
}
|
||||
.bright-cyan-bg {
|
||||
background-color: #6be6e6;
|
||||
}
|
||||
.bright-white-bg {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.red, .green, .yellow, .blue, .magenta, .cyan {
|
||||
text-shadow: 0 0 0.5px currentColor;
|
||||
}
|
||||
|
||||
.blinker {
|
||||
animation: blinker 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
49.9% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.command {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
.commandsymbol {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.silentlink {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.commandline {
|
||||
font-size: 16px;
|
||||
font-family: NotoEmoji-Regular, ibm_vga, monospace;
|
||||
background-color: #17101a;
|
||||
color: #e6dedc;
|
||||
padding: 2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="command commandline">
|
||||
<span>
|
||||
<a href="/" class="silentlink">blog.</a><a href="https://oat.zone/" class="silentlink">oat.zone</a>
|
||||
</span>
|
||||
<div class="green commandsymbol">λ</div>
|
||||
<span><slot/></span>
|
||||
<span class="blinker">█</span>
|
||||
</div>
|
|
@ -0,0 +1,150 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { renderCommentMarkdown } from "./markdown/rendering";
|
||||
import * as timeago from 'timeago.js';
|
||||
import ProfilePicture from "./ProfilePicture.svelte";
|
||||
|
||||
/**
|
||||
* @type {Comment}
|
||||
*/
|
||||
export let data;
|
||||
|
||||
let timestampElement;
|
||||
onMount(() => {
|
||||
timeago.render([timestampElement]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.comment {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
.pointer {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 0%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.display-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
flex-shrink: 1;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: block;
|
||||
color: var(--text-color-light);
|
||||
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a.timestamp {
|
||||
color: inherit;
|
||||
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
display: block;
|
||||
flex: 0 0 none;
|
||||
font-size: small;
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
:global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
:global(img) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.thread {
|
||||
padding-left: 1rem;
|
||||
margin-left: 2rem;
|
||||
border-left: 0.25rem solid var(--line-color);
|
||||
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<article class="comment">
|
||||
<a id="comment-{data.comment.commentId}" class="pointer"></a>
|
||||
<ProfilePicture url={data.poster.avatarURL} type={data.poster.avatarShape} handle={data.poster.handle} hideOnMobile={true}/>
|
||||
<div class="content">
|
||||
<div class="author">
|
||||
<ProfilePicture url={data.poster.avatarURL} type={data.poster.avatarShape} handle={data.poster.handle} hideOnMobile={false} tiny/>
|
||||
<a
|
||||
rel="author"
|
||||
href="https://cohost.org/{data.poster.handle}"
|
||||
class="display-name"
|
||||
title="{data.poster.displayName}">
|
||||
{data.poster.displayName}
|
||||
</a>
|
||||
<a href="https://cohost.org/{data.poster.handle}" class="handle">
|
||||
@{data.poster.handle}
|
||||
</a>
|
||||
<a href="#comment-{data.comment.commentId}" class="timestamp">
|
||||
<time
|
||||
bind:this={timestampElement}
|
||||
datetime="{data.comment.postedAtISO}"
|
||||
title="{(new Date(data.comment.postedAtISO)).toString()}"
|
||||
>
|
||||
{timeago.format(data.comment.postedAtISO)}
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
<div class="comment-text">
|
||||
{@html renderCommentMarkdown(data.comment.body)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{#each data.comment.children as child}
|
||||
<div class="thread">
|
||||
<svelte:self data={child}/>
|
||||
</div>
|
||||
{/each}
|
|
@ -0,0 +1,132 @@
|
|||
<script>
|
||||
/**
|
||||
* @type {AvatarShape}
|
||||
*/
|
||||
export let type;
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
export let url;
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
export let handle;
|
||||
/**
|
||||
* @type {boolean?}
|
||||
*/
|
||||
export let hideOnMobile;
|
||||
/**
|
||||
* @type {boolean?}
|
||||
*/
|
||||
export let tiny;
|
||||
|
||||
/**
|
||||
* @param {AvatarShape} avatarShape
|
||||
*/
|
||||
function avatarMaskClass(avatarShape) {
|
||||
switch (avatarShape) {
|
||||
case 'capsule-big':
|
||||
return 'mask-capsule-big';
|
||||
case 'capsule-small':
|
||||
return 'mask-capsule-small';
|
||||
case 'roundrect':
|
||||
return 'mask-roundrect';
|
||||
case 'squircle':
|
||||
return 'mask-squircle';
|
||||
case 'egg':
|
||||
return 'mask-egg';
|
||||
default:
|
||||
case 'circle':
|
||||
return 'mask-circle';
|
||||
}
|
||||
}
|
||||
|
||||
const avatarURL = `${url}?width=80&height=80&fit=cover&dpr=2&auto=webp`;
|
||||
const noTransparentAvatar = false;
|
||||
const maskName = avatarMaskClass(type);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-picture {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
&.tiny {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&.hide-on-mobile {
|
||||
display: none;
|
||||
@media (min-width: 1024px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.show-on-mobile {
|
||||
display: block;
|
||||
@media (min-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
&.no-transparent-avatar {
|
||||
background-image: var(--avatar);
|
||||
}
|
||||
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
|
||||
&.mask-squircle {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==);
|
||||
}
|
||||
|
||||
&.mask-roundrect {
|
||||
//@apply rounded-lg;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&.mask-circle {
|
||||
//@apply rounded-full;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&.mask-egg {
|
||||
mask-image: url('/masks/egg.svg');
|
||||
}
|
||||
|
||||
&.mask-capsule-big {
|
||||
mask-image: url('/masks/capsule-big.svg');
|
||||
}
|
||||
|
||||
&.mask-capsule-small {
|
||||
mask-image: url('/masks/capsule-small.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<a
|
||||
href="https://cohost.org/{handle}"
|
||||
class="profile-picture"
|
||||
class:hide-on-mobile={hideOnMobile === true}
|
||||
class:show-on-mobile={hideOnMobile === false}
|
||||
class:tiny={tiny}
|
||||
title="@{handle}"
|
||||
>
|
||||
<img
|
||||
class={maskName}
|
||||
class:no-transparent-avatar={noTransparentAvatar}
|
||||
style="--avatar:url('{avatarURL}')"
|
||||
src={avatarURL}
|
||||
alt={handle}
|
||||
/>
|
||||
</a>
|
|
@ -0,0 +1,146 @@
|
|||
import { stringifyEntities } from 'stringify-entities';
|
||||
import * as fs from 'fs/promises';
|
||||
import config from '$lib/config';
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} properties
|
||||
*/
|
||||
function renderProperties(properties) {
|
||||
if (Object.keys(properties).length === 0) return '';
|
||||
return ' ' + Object.entries(properties)
|
||||
.filter(([k, v]) => k !== '' && v !== '')
|
||||
.map(([k, v]) =>
|
||||
v ? `${k}=${typeof v === 'string' ? `"${stringifyEntities(v)}"` : v.toString()}` : k
|
||||
).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTMap} ast
|
||||
* @returns {string}
|
||||
*/
|
||||
// todo: obliterate from orbit
|
||||
export function renderASTMap(ast) {
|
||||
switch (ast.type) {
|
||||
case 'root':
|
||||
return ast.children.map(c => renderASTMap(c)).join('')
|
||||
case 'element':
|
||||
if (ast.tagName === 'a') {
|
||||
ast.properties.target = '_blank';
|
||||
ast.properties.rel = 'noreferrer noopener';
|
||||
}
|
||||
if (ast.properties.id && ast.properties.id.includes('cohost-blogger-ignore')) return '';
|
||||
return `<${ast.tagName}${renderProperties(ast.properties)}>${ast.children.map(c => renderASTMap(c)).join('')}</${ast.tagName}>`;
|
||||
case 'text':
|
||||
return stringifyEntities(ast.value);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const COHOST_API_URI = 'https://cohost.org/api/v1/trpc/';
|
||||
/**
|
||||
* @param {string} route
|
||||
* @param {Record<string, any>} input
|
||||
*/
|
||||
export async function trpcRequest(route, input) {
|
||||
const url = new URL(COHOST_API_URI + route);
|
||||
if (input) url.searchParams.set('input', JSON.stringify(input));
|
||||
const data = await (await fetch(url)).json();
|
||||
return data;
|
||||
}
|
||||
|
||||
const PAGES_PER_POST = 20;
|
||||
|
||||
/**
|
||||
* @param {number} page
|
||||
* @returns {Promise<Post[]>}
|
||||
*/
|
||||
export async function fetchAllPosts(page = 0) {
|
||||
const data = await trpcRequest('posts.getPostsTagged', {
|
||||
projectHandle: config.handle,
|
||||
tagSlug: config.tag,
|
||||
page: page
|
||||
});
|
||||
|
||||
let posts = data.result.data.items;
|
||||
|
||||
if (data.result.data.nItems >= PAGES_PER_POST) {
|
||||
posts = [...posts, ...(await fetchAllPosts(page + 1))]
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Post[]>}
|
||||
*/
|
||||
async function getPostsUncached() {
|
||||
return await fetchAllPosts();
|
||||
//return JSON.parse(await fs.readFile('src/testPosts.json', 'utf8')).filter(post => post.tags.includes('cohost-blogger'));
|
||||
}
|
||||
|
||||
// this technically only stores the preview data - the posts on the actual pages are always fetched
|
||||
// however there is no way to fetch a specified amount of info, so cache it is
|
||||
let postCache = {
|
||||
/** @type {Post[]} **/
|
||||
posts: [],
|
||||
refreshed: -1
|
||||
}
|
||||
const CACHE_INVALID_PERIOD = 60 * 1000;
|
||||
|
||||
/**
|
||||
* @returns {Promise<Post[]>}
|
||||
*/
|
||||
export async function getPosts() {
|
||||
const timeSinceCache = Date.now() - postCache.refreshed;
|
||||
if (timeSinceCache > CACHE_INVALID_PERIOD) {
|
||||
postCache.posts = await getPostsUncached();
|
||||
postCache.refreshed = Date.now();
|
||||
}
|
||||
|
||||
return postCache.posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Post} post
|
||||
*/
|
||||
export function getPostImages(post) {
|
||||
return post.blocks.filter(block => block.type === 'attachment').map(block => block.attachment);
|
||||
}
|
||||
const COMMENT_REGEX = /^\s*<!--\s*@cohost-blogger-meta\s+([\S\s]+)\s*-->\s*$/;
|
||||
/**
|
||||
* @param {Post} post
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function getPostMetadata(post) {
|
||||
return post.blocks
|
||||
.filter(block => block.type === 'markdown')
|
||||
.map(block => block.markdown.content)
|
||||
.map(text => COMMENT_REGEX.exec(text)).filter(res => res !== null).map(res => res[1])
|
||||
.reduce((lines, comment) => [...lines, ...comment.split('\n')], [])
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.reduce((properties, line) => {
|
||||
properties[line.split(':')[0].trim()] = line.split(':')[1].trim();
|
||||
return properties;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Post} post
|
||||
* @returns {Date | null}
|
||||
*/
|
||||
export function getPostPublishDate(post) {
|
||||
const meta = getPostMetadata(post);
|
||||
if (meta['published-at']) return new Date(meta['published-at']);
|
||||
if (post.publishedAt) return new Date(post.publishedAt);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Post} post
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getPostSlug(post) {
|
||||
return getPostMetadata(post).slug || post.filename;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
handle: 'oatmealine',
|
||||
tag: 'oatposting',
|
||||
blogName: 'breakfast oatmeal',
|
||||
blogDescription: 'cat /dev/urandom as a service - jill\'s tiny little blog place',
|
||||
siteURL: 'https://blog.oat.zone'
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
name: 'cohost-blogger',
|
||||
repo: 'https://git.oat.zone/oat/cohost-blogger'
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import { visit } from "unist-util-visit";
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
*/
|
||||
export function copyImgAltToTitle(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'img' }, (node) => {
|
||||
if (node.properties?.alt) {
|
||||
node.properties.title = node.properties.alt;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
*/
|
||||
export function lazyLoadImages(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'img' }, (node) => {
|
||||
node.properties.loading = 'lazy';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
*/
|
||||
export function dropCohostBloggerIgnoreBlocks(hast) {
|
||||
visit(hast, { type: 'element' }, (node, index, parent) => {
|
||||
if (parent === null || index === null) return;
|
||||
|
||||
// remove any elements that match the class
|
||||
if (node.properties.className && node.properties.className.includes('cohost-blogger-ignore')) {
|
||||
parent.children.splice(index, 1);
|
||||
|
||||
// if there's nothing else in the parent, then remove it aswell
|
||||
if (parent.children.length === 0 && parent.type === 'element') {
|
||||
// don't actually know how to remove it lol... display: none will do
|
||||
parent.properties.style = 'display:none';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// largely copied from makeIframelyEmbeds
|
||||
/**
|
||||
* @param {RootAST} hast
|
||||
*/
|
||||
export function makeLazyEmbeds(hast) {
|
||||
visit(hast, { type: 'element', tagName: 'a' }, (node, index, parent) => {
|
||||
if (parent === null || index === null) return;
|
||||
|
||||
// GFM autolink literals have the following two properties:
|
||||
// - they have exactly one child, and it's a text child;
|
||||
if (node.children.length != 1 || node.children[0].type != 'text')
|
||||
return;
|
||||
// - the starting offset of the text child matches the starting offset
|
||||
// of the node (angle-bracket autolinks and explicit links differ by 1
|
||||
// char)
|
||||
if (
|
||||
!node.position ||
|
||||
!node.children[0].position ||
|
||||
node.children[0].position.start.offset != node.position.start.offset
|
||||
)
|
||||
return;
|
||||
|
||||
// additionally, GFM autolink literals in their own paragraph are the
|
||||
// only child of their parent node.
|
||||
if (parent.children.length != 1) return;
|
||||
|
||||
// plain videos
|
||||
// todo: THIS IS LAZY!!!!
|
||||
if (node.properties?.href.endsWith('.mp4') || node.properties?.href.endsWith('.webm')) {
|
||||
// render the parent element to fit the video better
|
||||
if (parent.type === 'element') {
|
||||
parent.tagName = 'div';
|
||||
parent.properties.style = 'width:100%;display:flex;justify-content:center'
|
||||
}
|
||||
|
||||
parent.children.splice(index, 1, {
|
||||
type: 'element',
|
||||
tagName: 'video',
|
||||
properties: {
|
||||
src: node.properties?.href,
|
||||
autoplay: 'true',
|
||||
playsinline: 'true',
|
||||
loop: 'true',
|
||||
style: 'width: 100%;max-width: 600px',
|
||||
controls: 'true'
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
// youtube videos
|
||||
} else if (node.properties?.href.startsWith('https://www.youtube.com/')) {
|
||||
// <iframe src="https://www.youtube.com/embed/avNF21NRe10?feature=oembed" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="[NotITG Release] eyes in the water" name="fitvid0" frameborder="0"></iframe>
|
||||
parent.children.splice(index, 1, {
|
||||
type: 'element',
|
||||
tagName: 'iframe',
|
||||
properties: {
|
||||
src: node.properties?.href.replace('/watch?v=', '/embed/'),
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
frameborder: 0,
|
||||
style: 'width:100%;aspect-ratio:16/9',
|
||||
allowfullscreen: 'true'
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// most of this is borrowed from cohost's source code - i have no clue how it all works lmfao
|
||||
|
||||
import { unified } from 'unified';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import glsl from 'highlight.js/lib/languages/glsl'
|
||||
import deepmerge from 'deepmerge';
|
||||
import { compile } from 'html-to-text';
|
||||
import { copyImgAltToTitle, dropCohostBloggerIgnoreBlocks, lazyLoadImages, makeLazyEmbeds } from './processors';
|
||||
const convert = compile({
|
||||
wordwrap: false,
|
||||
});
|
||||
|
||||
const THIRD_AGE_SCHEMA = deepmerge(defaultSchema, {
|
||||
attributes: {
|
||||
"*": ["style"],
|
||||
},
|
||||
tagNames: ["video", "audio", "aside"], // consistency with current rules,
|
||||
});
|
||||
|
||||
const externalRel = ['nofollow', 'noopener', 'noreferrer'];
|
||||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @param {boolean} [xhtml]
|
||||
*/
|
||||
export function renderPostMarkdown(src, xhtml) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm, {
|
||||
singleTilde: false,
|
||||
})
|
||||
.use(remarkRehype, {
|
||||
allowDangerousHtml: true,
|
||||
})
|
||||
.use(rehypeHighlight, {
|
||||
ignoreMissing: true,
|
||||
languages: {glsl},
|
||||
})
|
||||
.use(() => copyImgAltToTitle)
|
||||
.use(() => lazyLoadImages)
|
||||
//.use(() => cleanUpFootnotes)
|
||||
.use(rehypeRaw)
|
||||
.use(() => dropCohostBloggerIgnoreBlocks)
|
||||
// sanitization on trusted posts isn't suuuper necessary
|
||||
// and prevents things like classes from being passed
|
||||
// along
|
||||
//.use(rehypeSanitize, COHOST_BLOGGER_SCHEMA)
|
||||
//.use(() => stripSecondAgeStyles(postDate))
|
||||
//.use(() => stripThirdAgeStyles(postDate));
|
||||
.use(() => makeLazyEmbeds)
|
||||
.use(rehypeExternalLinks, {
|
||||
rel: externalRel,
|
||||
target: '_blank',
|
||||
})
|
||||
.use(rehypeStringify, {
|
||||
closeSelfClosing: xhtml,
|
||||
upperDoctype: xhtml
|
||||
})
|
||||
//.use(() => convertMentions)
|
||||
//.use(parseEmoji, { cohostPlus: options.hasCohostPlus })
|
||||
.processSync(src)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StorageBlock[]} blocks
|
||||
*/
|
||||
export function renderPostSummaryMarkdown(blocks) {
|
||||
const origBlocks = blocks.filter(block => block.type === 'markdown');
|
||||
const readmoreIndex = origBlocks.findIndex(
|
||||
(block) => block.markdown.content === "---"
|
||||
);
|
||||
if (readmoreIndex > -1) {
|
||||
origBlocks.splice(readmoreIndex);
|
||||
}
|
||||
return renderPostMarkdown(origBlocks.map(b => b.markdown.content).join('\n\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @returns string
|
||||
*/
|
||||
export function renderCommentMarkdown(src) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm, {
|
||||
singleTilde: false,
|
||||
})
|
||||
.use(remarkRehype)
|
||||
.use(() => copyImgAltToTitle)
|
||||
.use(() => lazyLoadImages)
|
||||
//.use(() => cleanUpFootnotes)
|
||||
.use(rehypeSanitize, THIRD_AGE_SCHEMA)
|
||||
//.use(() => stripSecondAgeStyles(postDate))
|
||||
.use(rehypeExternalLinks, {
|
||||
rel: externalRel,
|
||||
target: '_blank',
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
//.use(() => convertMentions)
|
||||
//.use(parseEmoji, { cohostPlus: options.hasCohostPlus })
|
||||
.processSync(src)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} src
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPlaintext(src) {
|
||||
const renderedBody = renderCommentMarkdown(src);
|
||||
return convert(renderedBody);
|
||||
}
|
||||
/**
|
||||
* @param {string} src
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPostPlaintext(src) {
|
||||
const renderedBody = renderPostMarkdown(src);
|
||||
return convert(renderedBody);
|
||||
}
|
||||
/**
|
||||
* @param {StorageBlock[]} blocks
|
||||
* @returns string
|
||||
*/
|
||||
export function renderPostSummaryPlaintext(blocks) {
|
||||
const renderedBody = renderPostSummaryMarkdown(blocks);
|
||||
return convert(renderedBody);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
return `${['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('@sveltejs/kit').ParamMatcher} */
|
||||
export function match(param) {
|
||||
return /^[a-z0-9-]+$/.test(param);
|
||||
}
|
|
@ -0,0 +1,450 @@
|
|||
.prose {
|
||||
color: var(--tw-prose-body);
|
||||
}
|
||||
.prose :where(p) {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose :where([class~="lead"]) {
|
||||
color: var(--tw-prose-lead);
|
||||
font-size: 1.25em;
|
||||
line-height: 1.6;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
.prose :where(strong) {
|
||||
color: var(--tw-prose-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.prose :where(a strong) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(blockquote strong) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(thead th strong) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(ol) {
|
||||
list-style-type: decimal;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
.prose :where(ol[type="A"]) {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
.prose :where(ol[type="a"]) {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.prose :where(ol[type="A" s]) {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
.prose :where(ol[type="a" s]) {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.prose :where(ol[type="I"]) {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
.prose :where(ol[type="i"]) {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
.prose :where(ol[type="I" s]) {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
.prose :where(ol[type="i" s]) {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
.prose :where(ol[type="1"]) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.prose :where(ul) {
|
||||
list-style-type: disc;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
.prose :where(ol > li) ::marker{
|
||||
font-weight: 400;
|
||||
color: var(--tw-prose-counters);
|
||||
}
|
||||
.prose :where(ul > li) ::marker{
|
||||
color: var(--tw-prose-bullets);
|
||||
}
|
||||
.prose :where(hr) {
|
||||
border-color: var(--tw-prose-hr);
|
||||
border-top-width: 1px;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
.prose :where(blockquote) {
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
color: var(--tw-prose-quotes);
|
||||
border-left-width: 0.25rem;
|
||||
border-left-color: var(--tw-prose-quote-borders);
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 1.6em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.prose :where(blockquote p:first-of-type)::before {
|
||||
content: open-quote;
|
||||
}
|
||||
.prose :where(blockquote p:last-of-type)::after {
|
||||
content: close-quote;
|
||||
}
|
||||
.prose :where(h1) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 800;
|
||||
font-size: 2.25em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.8888889em;
|
||||
line-height: 1.1111111;
|
||||
}
|
||||
.prose :where(h1 strong) {
|
||||
font-weight: 900;
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(h2) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 700;
|
||||
font-size: 1.5em;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.3333333;
|
||||
}
|
||||
.prose :where(h2 strong) {
|
||||
font-weight: 800;
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(h3) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
font-size: 1.25em;
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 0.6em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.prose :where(h3 strong) {
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(h4) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.prose :where(h4 strong) {
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(img) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.prose :where(figure > *) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.prose :where(figcaption) {
|
||||
color: var(--tw-prose-captions);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.4285714;
|
||||
margin-top: 0.8571429em;
|
||||
}
|
||||
.prose :where(code) {
|
||||
color: var(--tw-prose-code);
|
||||
font-weight: 600;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.prose :where(code)::before {
|
||||
content: "`";
|
||||
}
|
||||
.prose :where(code)::after {
|
||||
content: "`";
|
||||
}
|
||||
.prose :where(a code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(h1 code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(h2 code) {
|
||||
color: inherit;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.prose :where(h3 code) {
|
||||
color: inherit;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.prose :where(h4 code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(blockquote code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(thead th code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(pre code) {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.prose :where(pre code)::before {
|
||||
content: none;
|
||||
}
|
||||
.prose :where(pre code)::after {
|
||||
content: none;
|
||||
}
|
||||
.prose :where(table) {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
text-align: left;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.7142857;
|
||||
}
|
||||
.prose :where(thead) {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--tw-prose-th-borders);
|
||||
}
|
||||
.prose :where(thead th) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
vertical-align: bottom;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
.prose :where(tbody tr) {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--tw-prose-td-borders);
|
||||
}
|
||||
.prose :where(tbody tr:last-child) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
.prose :where(tbody td) {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.prose :where(tfoot) {
|
||||
border-top-width: 1px;
|
||||
border-top-color: var(--tw-prose-th-borders);
|
||||
}
|
||||
.prose :where(tfoot td) {
|
||||
vertical-align: top;
|
||||
}
|
||||
.prose{
|
||||
--tw-prose-body: #374151;
|
||||
--tw-prose-headings: #111827;
|
||||
--tw-prose-lead: #4b5563;
|
||||
--tw-prose-links: #111827;
|
||||
--tw-prose-bold: #111827;
|
||||
--tw-prose-counters: #6b7280;
|
||||
--tw-prose-bullets: #d1d5db;
|
||||
--tw-prose-hr: #e5e7eb;
|
||||
--tw-prose-quotes: #111827;
|
||||
--tw-prose-quote-borders: #e5e7eb;
|
||||
--tw-prose-captions: #6b7280;
|
||||
--tw-prose-code: #111827;
|
||||
--tw-prose-pre-code: #e5e7eb;
|
||||
--tw-prose-pre-bg: #1f2937;
|
||||
--tw-prose-th-borders: #d1d5db;
|
||||
--tw-prose-td-borders: #e5e7eb;
|
||||
--tw-prose-invert-body: #d1d5db;
|
||||
--tw-prose-invert-headings: #fff;
|
||||
--tw-prose-invert-lead: #9ca3af;
|
||||
--tw-prose-invert-links: #fff;
|
||||
--tw-prose-invert-bold: #fff;
|
||||
--tw-prose-invert-counters: #9ca3af;
|
||||
--tw-prose-invert-bullets: #4b5563;
|
||||
--tw-prose-invert-hr: #374151;
|
||||
--tw-prose-invert-quotes: #f3f4f6;
|
||||
--tw-prose-invert-quote-borders: #374151;
|
||||
--tw-prose-invert-captions: #9ca3af;
|
||||
--tw-prose-invert-code: #fff;
|
||||
--tw-prose-invert-pre-code: #d1d5db;
|
||||
--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);
|
||||
--tw-prose-invert-th-borders: #4b5563;
|
||||
--tw-prose-invert-td-borders: #374151;
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
.prose :where(video) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.prose :where(figure) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.prose :where(li) {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.prose :where(ol > li) {
|
||||
padding-left: 0.375em;
|
||||
}
|
||||
.prose :where(ul > li) {
|
||||
padding-left: 0.375em;
|
||||
}
|
||||
.prose :where(.prose > ul > li p) {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.prose :where(.prose > ul > li > *:first-child) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
.prose :where(.prose > ul > li > *:last-child) {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose :where(.prose > ol > li > *:first-child) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
.prose :where(.prose > ol > li > *:last-child) {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose :where(ul ul, ul ol, ol ul, ol ol) {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.prose :where(hr + *) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose :where(h2 + *) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose :where(h3 + *) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose :where(h4 + *) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose :where(thead th:first-child) {
|
||||
padding-left: 0;
|
||||
}
|
||||
.prose :where(thead th:last-child) {
|
||||
padding-right: 0;
|
||||
}
|
||||
.prose :where(tbody td, tfoot td) {
|
||||
padding-top: 0.5714286em;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
.prose :where(tbody td:first-child, tfoot td:first-child) {
|
||||
padding-left: 0;
|
||||
}
|
||||
.prose :where(tbody td:last-child, tfoot td:last-child) {
|
||||
padding-right: 0;
|
||||
}
|
||||
.prose :where(.prose > :first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose :where(.prose > :last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.prose{
|
||||
color: #f9fafb;
|
||||
}
|
||||
.prose :where([class~="lead"]) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.prose :where(figcaption) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.prose :where(strong) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(ul > li)::before {
|
||||
background-color: #374151;
|
||||
}
|
||||
.prose :where(hr) {
|
||||
border-color: #1f2937;
|
||||
}
|
||||
.prose :where(blockquote) {
|
||||
color: #f3f4f6;
|
||||
border-left-color: #1f2937;
|
||||
}
|
||||
.prose :where(h1) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h2) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h3) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h4) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(thead) {
|
||||
color: #f3f4f6;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
.prose :where(tbody tr) {
|
||||
border-bottom-color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.prose{
|
||||
color: #f9fafb;
|
||||
}
|
||||
.prose :where([class~="lead"]) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.prose :where(figcaption) {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
.prose :where(strong) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(ul > li) ::before{
|
||||
background-color: #374151;
|
||||
}
|
||||
.prose :where(hr) {
|
||||
border-color: #1f2937;
|
||||
}
|
||||
.prose :where(blockquote) {
|
||||
color: #f3f4f6;
|
||||
border-left-color: #1f2937;
|
||||
}
|
||||
.prose :where(h1) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h2) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h3) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(h4) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(code) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.prose :where(a code) {
|
||||
color: inherit;
|
||||
}
|
||||
.prose :where(thead) {
|
||||
color: #f3f4f6;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
.prose :where(tbody tr) {
|
||||
border-bottom-color: #1f2937;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// hmm...
|
||||
//export const csr = false;
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import 'modern-normalize/modern-normalize.css';
|
||||
import '../app.scss';
|
||||
import CommandLine from '$lib/CommandLine.svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import config from '$lib/config';
|
||||
import constants from '$lib/constants';
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
footer {
|
||||
background: #0a0b0c;
|
||||
color: #fff;
|
||||
margin: max(12vmin,64px) 0 0;
|
||||
padding: 0 max(4vmin,20px);
|
||||
padding-bottom: 140px;
|
||||
padding-top: 48px;
|
||||
position: relative;
|
||||
|
||||
.inner {
|
||||
color: hsla(0,0%,100%,.7);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<CommandLine>
|
||||
cd {(new URL($page.url)).pathname}
|
||||
</CommandLine>
|
||||
</header>
|
||||
|
||||
<slot/>
|
||||
|
||||
<footer>
|
||||
<div class="inner">
|
||||
<a href="https://cohost.org/{config.handle}/tagged/{config.tag}" target="_blank" rel="noopener noreferrer">view on cohost</a>
|
||||
·
|
||||
<a href={constants.repo} target="_blank" rel="noopener noreferrer">powered by {constants.name}</a>
|
||||
</div>
|
||||
</footer>
|
|
@ -0,0 +1,9 @@
|
|||
import { getPosts } from '$lib/cohost';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ params }) {
|
||||
const posts = await getPosts();
|
||||
return {
|
||||
posts: posts
|
||||
};
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
<script>
|
||||
import { assets } from '$app/paths';
|
||||
import { renderPostPlaintext, renderPostSummaryMarkdown } from '$lib/markdown/rendering';
|
||||
import { formatDate } from '$lib/utils';
|
||||
import config from '$lib/config';
|
||||
import { getPostImages, getPostMetadata, getPostPublishDate, getPostSlug } from '../lib/cohost';
|
||||
import readingTime from 'reading-time/lib/reading-time';
|
||||
|
||||
import '../prose.scss';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{config.blogName}</title>
|
||||
<meta name="description" content={config.blogDescription}>
|
||||
<link rel="icon" href="{assets}/favicon.png" type="image/png">
|
||||
<link rel="canonical" href="{config.siteURL}/">
|
||||
|
||||
<meta property="og:site_name" content={config.blogName}>
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content={config.blogName}>
|
||||
<meta property="og:description" content={config.blogDescription}>
|
||||
<meta property="og:url" content="{config.siteURL}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content={config.blogName}>
|
||||
<meta name="twitter:description" content={config.blogDescription}>
|
||||
<meta name="twitter:url" content="{config.siteURL}/">
|
||||
<meta name="description" content={config.blogDescription}>
|
||||
|
||||
<link rel="alternate" title={config.blogName} type="application/rss+xml" href="{config.siteURL}/rss.xml">
|
||||
</svelte:head>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
margin: 10px auto;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.posts {
|
||||
display: grid;
|
||||
gap: 4.8vmin 4vmin;
|
||||
grid-template-columns: repeat(6,1fr);
|
||||
@media (max-width: 991px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
padding: max(4.8vmin,36px) 0 0;
|
||||
position: relative;
|
||||
|
||||
& > * {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.post-card {
|
||||
background-color: var(--card-overlay);
|
||||
border-radius: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
margin-bottom: 1.25em;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-bottom: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 0 0.75rem;
|
||||
.title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
display: -webkit-box;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
max-width: 720px;
|
||||
overflow-y: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.misc {
|
||||
padding: 0.75rem;
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
& > a {
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
}
|
||||
& > .misc {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
header {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 1em 1em;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 460px;
|
||||
max-height: 60vw;
|
||||
position: relative;
|
||||
|
||||
background:
|
||||
linear-gradient(to bottom, rgba(0,0,0,0), 85%, var(--background-color)),
|
||||
fixed center / cover no-repeat var(--background-image);
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
padding: 1em;
|
||||
|
||||
img {
|
||||
animation: 2s infinite ease-in-out alternate light-bob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-bob {
|
||||
from {
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="header" style="--background-image: url('{assets}/banner.png')">
|
||||
<div class="logo">
|
||||
<img src="{assets}/logo.png" alt="logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner">
|
||||
<div class="summary prose">
|
||||
<p>
|
||||
<b>Hi!</b> Welcome to the dumping grounds for my thoughts, experiments, ideas and whatever else I decide to write up.
|
||||
</p>
|
||||
<p>
|
||||
These aren't particularly well-organized, and I don't plan for them to be - I just write about whatever occupies my time or thoughts without
|
||||
much regard for how interesting it might be. The things I generally tend to think about, however, tend to be tech or programming related, so
|
||||
it's likely you'll see quite a bit of that here.
|
||||
</p>
|
||||
<p>
|
||||
This site is also actually a proxy of my <a href="https://cohost.org/{config.handle}/tagged/{config.tag}" target="_blank" rel="noopener noreferrer">cohost page</a>!
|
||||
The posts are just presented in a more convenient way, and are generally easier to embed and share.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="posts">
|
||||
{#each data.posts as post}
|
||||
<article class="post-card">
|
||||
<a href={'/' + getPostSlug(post)} class="link-wrapper">
|
||||
{#if getPostImages(post).length > 0}
|
||||
<div class="image">
|
||||
<!-- dumb hack for variable declaration -->
|
||||
{#each [getPostImages(post)[0]] as img}
|
||||
<img src={img.previewURL} alt={img.altText} width={img.width} height={img.height}>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<header>
|
||||
<h2 class="title">{post.headline}</h2>
|
||||
<div class="post preview excerpt prose">
|
||||
{@html renderPostSummaryMarkdown(post.blocks)}
|
||||
</div>
|
||||
</header>
|
||||
</a>
|
||||
<small class="misc">
|
||||
{#if getPostPublishDate(post)}
|
||||
<time datetime={getPostPublishDate(post)?.toISOString()}>{formatDate(getPostPublishDate(post))}</time>
|
||||
·
|
||||
{/if}
|
||||
{Math.ceil(readingTime(renderPostPlaintext(post.plainTextBody)).minutes)} minute read</small>
|
||||
<!--{JSON.stringify(post)}-->
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,24 @@
|
|||
import { getPostMetadata, getPosts, trpcRequest } from '$lib/cohost';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const trailingSlash = 'always';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ params }) {
|
||||
const posts = await getPosts();
|
||||
|
||||
let post = posts.find(post => getPostMetadata(post).slug === params.post);
|
||||
if (!post) post = posts.find(post => post.filename === params.post);
|
||||
if (!post) throw error(404, {message: 'Post not found'});
|
||||
|
||||
const postId = post.postId;
|
||||
const postFetched = await trpcRequest('posts.singlePost', {
|
||||
handle: post.postingProject.handle,
|
||||
postId: postId
|
||||
});
|
||||
|
||||
// uh oh! let's just serve from the emptier post cache
|
||||
if (!postFetched.result) return {post: post, comments: {[postId]: []}};
|
||||
|
||||
return postFetched.result.data;
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
<script>
|
||||
import Comment from '$lib/Comment.svelte';
|
||||
import { getPostImages, getPostPublishDate, getPostSlug, renderASTMap } from '$lib/cohost';
|
||||
import { renderPostMarkdown, renderPostSummaryPlaintext } from '$lib/markdown/rendering';
|
||||
import { formatDate } from '$lib/utils';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import '../../prose.scss';
|
||||
|
||||
import 'highlight.js/styles/github-dark-dimmed.css';
|
||||
import { onMount } from 'svelte';
|
||||
import config from '$lib/config';
|
||||
import constants from '$lib/constants';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
/**
|
||||
* @type {Post}
|
||||
*/
|
||||
const post = data.post;
|
||||
/**
|
||||
* @type {Record<string, Comment[]>}
|
||||
*/
|
||||
const comments = data.comments;
|
||||
|
||||
let publishTimestampElement;
|
||||
onMount(() => {
|
||||
if (publishTimestampElement) {
|
||||
timeago.render([publishTimestampElement]);
|
||||
}
|
||||
});
|
||||
|
||||
// metadata
|
||||
const canonicalURL = `${config.siteURL}/${getPostSlug(post)}/`;
|
||||
const summary = renderPostSummaryPlaintext(post.blocks);
|
||||
const image = getPostImages(post)[0];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{post.headline} · {config.blogName}</title>
|
||||
|
||||
<meta name="description" content={summary}>
|
||||
<link rel="canonical" href={canonicalURL}>
|
||||
|
||||
<meta property="og:site_name" content="{config.blogName}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content={post.headline}>
|
||||
<meta property="og:description" content={summary}>
|
||||
<meta property="og:url" content={canonicalURL}>
|
||||
{#if image}
|
||||
<meta property="og:image" content={image.fileURL}>
|
||||
<meta property="og:image:width" content={image.width}>
|
||||
<meta property="og:image:height" content={image.height}>
|
||||
{/if}
|
||||
{#if getPostPublishDate(post)}
|
||||
<meta property="article:published_time" content={getPostPublishDate(post)?.toISOString()}>
|
||||
{/if}
|
||||
|
||||
<meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'}>
|
||||
<meta name="twitter:title" content={post.headline}>
|
||||
<meta name="twitter:description" content={summary}>
|
||||
<meta name="twitter:url" content={canonicalURL}>
|
||||
{#if image}
|
||||
<meta name="twitter:image" content={image.fileURL}>
|
||||
{/if}
|
||||
|
||||
<meta name="generator" content={constants.name}>
|
||||
</svelte:head>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
.post-container {
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note, .note a {
|
||||
color: var(--text-color-light);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin: 1rem auto;
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.comments-label {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.comments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="inner">
|
||||
<div class="prose post-container">
|
||||
<h1 class="title">{post.headline}</h1>
|
||||
|
||||
<span class="note">
|
||||
<a rel="noreferrer nofollower" target="_blank" href={post.singlePostPageUrl}>
|
||||
{#if getPostPublishDate(post)}
|
||||
<time bind:this={publishTimestampElement} datetime={getPostPublishDate(post)?.toISOString()}>{timeago.format(getPostPublishDate(post))}</time>
|
||||
{:else}
|
||||
draft
|
||||
{/if}
|
||||
</a>
|
||||
·
|
||||
by <a rel="noreferrer nofollower" target="_blank" href={post.postingProject.url}>{post.postingProject.displayName}</a>
|
||||
</span>
|
||||
|
||||
{#if getPostImages(post).length > 0}
|
||||
<div class="image">
|
||||
<!-- dumb hack for variable declaration -->
|
||||
{#each [getPostImages(post)[0]] as img}
|
||||
<img src={img.previewURL} alt={img.altText} width={img.width} height={img.height}>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="post">
|
||||
{@html renderPostMarkdown(post.plainTextBody)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{#if getPostPublishDate(post)}
|
||||
<small>published <time datetime={getPostPublishDate(post)?.toISOString()}>{formatDate(getPostPublishDate(post))}</time></small>
|
||||
{:else}
|
||||
<small>draft</small>
|
||||
{/if}
|
||||
|
||||
<div class="comments-label">{post.numComments} comments · <a href={post.singlePostPageUrl} target="_blank" rel="noreferrer noopener">join the discussion on cohost</a></div>
|
||||
|
||||
<div class="comments">
|
||||
{#each comments[post.postId] as comment}
|
||||
<Comment data={comment}/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
import { getPostImages, getPostPublishDate, getPostSlug, getPosts } from '$lib/cohost';
|
||||
import { renderPostMarkdown, renderPostSummaryPlaintext } from '$lib/markdown/rendering';
|
||||
import { minify as minifyXML } from 'minify-xml';
|
||||
import config from '$lib/config';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET() {
|
||||
const posts = await getPosts();
|
||||
let response = new Response(xml(posts));
|
||||
response.headers.set('Cache-Control', 'max-age=0, s-maxage=3600');
|
||||
response.headers.set('Content-Type', 'application/xml');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const xml = (/** @type {Post[]} */ posts) => minifyXML(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<channel>
|
||||
<title>${config.blogName}</title>
|
||||
<description>${config.blogDescription}</description>
|
||||
<link>${config.siteURL}/</link>
|
||||
<link href="${config.siteURL}/rss.xml" rel="self" type="application/rss+xml" />
|
||||
<ttl>60</ttl>
|
||||
<image>
|
||||
<url>${config.siteURL}/favicon.png</url>
|
||||
<title>${config.blogName}</title>
|
||||
<link>${config.siteURL}/</link>
|
||||
</image>
|
||||
${posts
|
||||
.map(post => `
|
||||
<item>
|
||||
<title>${post.headline}</title>
|
||||
<description>${renderPostSummaryPlaintext(post.blocks)}</description>
|
||||
<link>${config.siteURL}/${getPostSlug(post)}/</link>
|
||||
<dc:creator>${post.postingProject.displayName}</dc:creator>
|
||||
<pubDate>${getPostPublishDate(post)?.toUTCString()}</pubDate>
|
||||
${getPostImages(post).length > 0 ?
|
||||
`<content url="${getPostImages(post)[0].fileURL}" medium="image" />`
|
||||
: ''}
|
||||
<content:encoded><![CDATA[
|
||||
${renderPostMarkdown(post.plainTextBody, true)}
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
`)}
|
||||
</channel>
|
||||
</rss>
|
||||
`);
|
|
@ -0,0 +1,44 @@
|
|||
import { getPostImages, getPostPublishDate, getPostSlug, getPosts } from '$lib/cohost';
|
||||
import { minify as minifyXML } from 'minify-xml';
|
||||
import config from '$lib/config';
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET() {
|
||||
const posts = await getPosts();
|
||||
let response = new Response(xml(posts));
|
||||
response.headers.set('Cache-Control', 'max-age=0, s-maxage=3600');
|
||||
response.headers.set('Content-Type', 'application/xml');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const xml = (/** @type {Post[]} */ posts) => minifyXML(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
|
||||
xmlns:xhtml="https://www.w3.org/1999/xhtml"
|
||||
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
|
||||
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
|
||||
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
|
||||
>
|
||||
<url>
|
||||
<loc>${config.siteURL}</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
${posts
|
||||
.map(post => `
|
||||
<url>
|
||||
<loc>${config.siteURL}/${getPostSlug(post)}</loc>
|
||||
<lastmod>${getPostPublishDate(post)?.toISOString()}</lastmod>
|
||||
${getPostImages(post).length > 0 ? `
|
||||
<image:image>
|
||||
<image:loc>${getPostImages(post)[0].fileURL}</image:loc>
|
||||
<image:caption>${post.headline}</image:caption>
|
||||
</image:image>
|
||||
` : ''}
|
||||
</url>
|
||||
`)}
|
||||
</urlset>
|
||||
`);
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 933 KiB |
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M414 158C414 70.797 343.203 0 256 0S98 70.797 98 158v196c0 87.203 70.797 158 158 158s158-70.797 158-158V158Z" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 313 B |
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M354 98c0-54.088-43.912-98-98-98s-98 43.912-98 98v316c0 54.088 43.912 98 98 98s98-43.912 98-98V98Z" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 303 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="500" height="366" viewBox="0 0 500 366" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M299.375 347.12C254.156 360.638 211.346 366.764 170.945 365.497C130.544 364.23 95.8817 354.689 66.9566 336.873C38.0313 319.059 18.0946 291.827 7.14576 255.178C-3.86568 218.32 -2.15088 184.558 12.2904 153.893C26.7319 123.227 50.5241 96.3814 83.6671 73.3558C116.81 50.3302 155.992 32.058 201.211 18.5394C246.221 5.08325 288.858 -1.04798 329.123 0.14595C369.387 1.33964 404.019 10.7761 433.017 28.455C462.016 46.1342 481.989 73.2979 492.938 109.947C503.887 146.595 502.104 180.32 487.589 211.122C473.075 241.924 449.329 268.926 416.354 292.13C383.378 315.334 344.385 333.664 299.375 347.12Z" fill="#FFF9F2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 721 B |
|
@ -0,0 +1,15 @@
|
|||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
},
|
||||
preprocess: [vitePreprocess()],
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,6 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Loading…
Reference in New Issue