mirror of
https://github.com/tonytins/tonybark.com.git
synced 2025-03-22 17:52:19 +00:00
fix: attempt to fix safari
This commit is contained in:
parent
4bc3dd5ca4
commit
995a6a987a
13 changed files with 346 additions and 287 deletions
|
@ -16,8 +16,6 @@ npm run dev
|
||||||
Change environment variables:
|
Change environment variables:
|
||||||
```sh
|
```sh
|
||||||
PUBLIC_HANDLE="myhandle.bsky.social" # Your handle, or DID
|
PUBLIC_HANDLE="myhandle.bsky.social" # Your handle, or DID
|
||||||
PUBLIC_ABOUT="Welcome to my blog!" # Optional description of the kinds of posts you'll be making, or a greeting.
|
|
||||||
# PUBLIC_ABOUT Shows up under your Bluesky profile description in the profile card.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Standalone
|
#### Standalone
|
||||||
|
|
361
package-lock.json
generated
361
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview -v",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
|
@ -25,13 +25,11 @@
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3",
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.9",
|
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
|
||||||
"marked": "^15.0.3",
|
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { profile } from "./identity";
|
const { profile } = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4 bg-local relative border-gray-500 border-[1px] rounded-[1em] m-2" style="background-image: url({profile.banner}); background-size: cover; background-position: center;">
|
<div class="p-4 bg-local relative border-gray-500 border-[1px] rounded-[1em] m-2" style="background-image: url({profile.banner}); background-size: cover; background-position: center;">
|
||||||
|
@ -11,10 +11,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<p class="">{profile.description}</p>
|
<p class="">{profile.description}</p>
|
||||||
{#if profile.about}
|
|
||||||
<br/>
|
|
||||||
<p class="">{profile.about}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,56 +0,0 @@
|
||||||
import {env} from "$env/dynamic/public"
|
|
||||||
|
|
||||||
async function safeFetch(url: string) {
|
|
||||||
const response = await fetch(url)
|
|
||||||
if (!response.ok) throw new Error(response.status + ":" + response.statusText)
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Profile {
|
|
||||||
avatar: string,
|
|
||||||
banner: string,
|
|
||||||
displayName: string,
|
|
||||||
did: string,
|
|
||||||
handle: string,
|
|
||||||
description: string,
|
|
||||||
about: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!env.PUBLIC_HANDLE) {
|
|
||||||
throw new Error("Expected PUBLIC_HANDLE environment variable.")
|
|
||||||
}
|
|
||||||
|
|
||||||
const getProfile = await safeFetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${env.PUBLIC_HANDLE}`)
|
|
||||||
export const profile: Profile = {
|
|
||||||
avatar: getProfile["avatar"],
|
|
||||||
banner: getProfile["banner"],
|
|
||||||
displayName: getProfile["displayName"],
|
|
||||||
did: getProfile["did"],
|
|
||||||
handle: getProfile["handle"],
|
|
||||||
description: getProfile["description"],
|
|
||||||
about: env.PUBLIC_ABOUT,
|
|
||||||
}
|
|
||||||
export const avatar: string = getProfile["avatar"]
|
|
||||||
let split = profile.did.split(":")
|
|
||||||
let diddoc;
|
|
||||||
if (split[0] === "did") {
|
|
||||||
if (split[1] === "plc") {
|
|
||||||
diddoc = await safeFetch(`https://plc.directory/${profile.did}`)
|
|
||||||
} else if (split[1] === "web") {
|
|
||||||
diddoc = await safeFetch("https://" + split[2] + "/.well-known/did.json")
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid DID, Not blessed method")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid DID, malformed")
|
|
||||||
}
|
|
||||||
let pdsurl;
|
|
||||||
for (let service of diddoc["service"]) {
|
|
||||||
if (service["id"] === "#atproto_pds") {
|
|
||||||
pdsurl = service["serviceEndpoint"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!pdsurl) {
|
|
||||||
throw new Error("DID lacks #atproto_pds service")
|
|
||||||
}
|
|
||||||
export const pds: string = pdsurl;
|
|
|
@ -1 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
|
@ -1,17 +1,29 @@
|
||||||
import { pds, profile } from "$lib/identity";
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import rehypeStringify from 'rehype-stringify'
|
import rehypeStringify from 'rehype-stringify'
|
||||||
import remarkParse from 'remark-parse'
|
import remarkParse from 'remark-parse'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkRehype from 'remark-rehype'
|
import remarkRehype from 'remark-rehype'
|
||||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import type { Schema } from '../../../node_modules/rehype-sanitize/lib'
|
import type { Schema } from '../../node_modules/rehype-sanitize/lib'
|
||||||
import { unified } from 'unified'
|
import { unified } from 'unified'
|
||||||
import type { Node } from 'unist'
|
import type { Node } from 'unist'
|
||||||
import type { Root, Element } from 'hast'
|
import type { Root, Element } from 'hast'
|
||||||
import type { Plugin } from "unified";
|
import type { Plugin } from "unified";
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
title: string,
|
||||||
|
rkey: string,
|
||||||
|
createdAt: Date,
|
||||||
|
content: string // content parsed to html
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownPost {
|
||||||
|
title: string,
|
||||||
|
rkey: string,
|
||||||
|
createdAt: Date,
|
||||||
|
mdcontent: string // markdown content
|
||||||
|
}
|
||||||
|
|
||||||
// WhiteWind's own custom schema:
|
// WhiteWind's own custom schema:
|
||||||
// https://github.com/whtwnd/whitewind-blog/blob/7eb8d4623eea617fd562b93d66a0e235323a2f9a/frontend/src/services/DocProvider.tsx#L122
|
// https://github.com/whtwnd/whitewind-blog/blob/7eb8d4623eea617fd562b93d66a0e235323a2f9a/frontend/src/services/DocProvider.tsx#L122
|
||||||
const customSchema = {
|
const customSchema = {
|
||||||
|
@ -63,14 +75,14 @@ const rehypeUpgradeImage: Plugin<any, Root, Node> = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load({ params }) {
|
export async function parse(mdposts: Map<string, MarkdownPost>) {
|
||||||
const rawResponse = await fetch(`${pds}/xrpc/com.atproto.repo.getRecord?repo=${profile.did}&collection=com.whtwnd.blog.entry&rkey=${params.rkey}`)
|
let posts: Map<string, Post> = new Map()
|
||||||
const response = await rawResponse.json()
|
for (let [ rkey, post ] of mdposts) {
|
||||||
try {
|
posts.set(rkey, {
|
||||||
|
title: post.title,
|
||||||
return {
|
rkey: post.rkey,
|
||||||
title: response["value"]["title"],
|
createdAt: post.createdAt,
|
||||||
mdcontent: String(
|
content: String(
|
||||||
await unified()
|
await unified()
|
||||||
.use(remarkParse, { fragment: true }) // Parse the MD
|
.use(remarkParse, { fragment: true }) // Parse the MD
|
||||||
.use(remarkGfm) // Parse GH specific MD
|
.use(remarkGfm) // Parse GH specific MD
|
||||||
|
@ -79,11 +91,9 @@ export async function load({ params }) {
|
||||||
.use(rehypeUpgradeImage)
|
.use(rehypeUpgradeImage)
|
||||||
.use(rehypeSanitize, customSchema as Schema) // Sanitize the HTML
|
.use(rehypeSanitize, customSchema as Schema) // Sanitize the HTML
|
||||||
.use(rehypeStringify) // Stringify
|
.use(rehypeStringify) // Stringify
|
||||||
.process(response["value"]["content"])
|
.process(post.mdcontent)
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
error(404)
|
|
||||||
}
|
}
|
||||||
|
return posts
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Profile from '$lib/Profile.svelte';
|
import Profile from '$lib/Profile.svelte';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
let { children } = $props();
|
let { data, children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="box-border mx-auto px-8 max-w-[1000px] pb-8">
|
<div class="box-border mx-auto px-8 max-w-[1000px] pb-8">
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<a class="font-medium text-[x-large]" href="/">Home</a>
|
<a class="font-medium text-[x-large]" href="/">Home</a>
|
||||||
<a class="font-medium text-[large]" href="/about">About</a>
|
<a class="font-medium text-[large]" href="/about">About</a>
|
||||||
</nav>
|
</nav>
|
||||||
<Profile/>
|
<Profile profile={data.profile}/>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<footer class="text-center"><a class="hover:underline hover:text-blue-500" href="https://github.com/hugeblank/whitebreeze">WhiteBreeze</a></footer>
|
<footer class="text-center"><a class="hover:underline hover:text-blue-500" href="https://github.com/hugeblank/whitebreeze">WhiteBreeze</a></footer>
|
82
src/routes/+layout.ts
Normal file
82
src/routes/+layout.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { PUBLIC_HANDLE } from "$env/static/public"
|
||||||
|
import { parse, type MarkdownPost, type Post } from '$lib/parser';
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
avatar: string,
|
||||||
|
banner: string,
|
||||||
|
displayName: string,
|
||||||
|
did: string,
|
||||||
|
handle: string,
|
||||||
|
description: string,
|
||||||
|
pds: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeFetch(url: string) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) throw new Error(response.status + ":" + response.statusText)
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProfile(): Promise<Profile> {
|
||||||
|
const fetchProfile = await safeFetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${PUBLIC_HANDLE}`)
|
||||||
|
let split = fetchProfile["did"].split(":")
|
||||||
|
let diddoc;
|
||||||
|
if (split[0] === "did") {
|
||||||
|
if (split[1] === "plc") {
|
||||||
|
diddoc = await safeFetch(`https://plc.directory/${fetchProfile["did"]}`)
|
||||||
|
} else if (split[1] === "web") {
|
||||||
|
diddoc = await safeFetch("https://" + split[2] + "/.well-known/did.json")
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid DID, Not blessed method")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid DID, malformed")
|
||||||
|
}
|
||||||
|
let pdsurl;
|
||||||
|
for (let service of diddoc["service"]) {
|
||||||
|
if (service["id"] === "#atproto_pds") {
|
||||||
|
pdsurl = service["serviceEndpoint"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!pdsurl) {
|
||||||
|
throw new Error("DID lacks #atproto_pds service")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
avatar: fetchProfile["avatar"],
|
||||||
|
banner: fetchProfile["banner"],
|
||||||
|
displayName: fetchProfile["displayName"],
|
||||||
|
did: fetchProfile["did"],
|
||||||
|
handle: fetchProfile["handle"],
|
||||||
|
description: fetchProfile["description"],
|
||||||
|
pds: pdsurl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Maybe also add the option to show "url" level records as an env setting.
|
||||||
|
|
||||||
|
let profile: Profile;
|
||||||
|
let posts: Map<string, Post>;
|
||||||
|
export async function load() {
|
||||||
|
if (profile === undefined) {
|
||||||
|
profile = await getProfile();
|
||||||
|
}
|
||||||
|
if (posts === undefined) {
|
||||||
|
const rawResponse = await fetch(`${profile.pds}/xrpc/com.atproto.repo.listRecords?repo=${profile.did}&collection=com.whtwnd.blog.entry`)
|
||||||
|
const response = await rawResponse.json()
|
||||||
|
let mdposts: Map<string, MarkdownPost> = new Map();
|
||||||
|
for (let record of response["records"]) {
|
||||||
|
const matches = record["uri"].split("/")
|
||||||
|
const rkey = matches[matches.length - 1]
|
||||||
|
if (matches && matches.length === 5 && record["value"] && record["value"]["visibility"] === "public") {
|
||||||
|
mdposts.set(rkey, {
|
||||||
|
title: record["value"]["title"],
|
||||||
|
createdAt: new Date(record["value"]["createdAt"]),
|
||||||
|
mdcontent: record["value"]["content"],
|
||||||
|
rkey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
posts = await parse(mdposts)
|
||||||
|
}
|
||||||
|
return { posts, profile };
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
import { pds, profile } from "$lib/identity";
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
interface PostSummary {
|
|
||||||
title: string,
|
|
||||||
rkey: string,
|
|
||||||
createdAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Maybe also add the option to show "url" level records as an env setting.
|
|
||||||
export async function load({ params }) {
|
|
||||||
const rawResponse = await fetch(`${pds}/xrpc/com.atproto.repo.listRecords?repo=${profile.did}&collection=com.whtwnd.blog.entry`)
|
|
||||||
try {
|
|
||||||
const response = await rawResponse.json()
|
|
||||||
const rx = /\/.{13}$/;
|
|
||||||
let posts: PostSummary[] = [];
|
|
||||||
for (let record of response["records"]) {
|
|
||||||
const matches = rx.exec(record["uri"])
|
|
||||||
if (matches && matches[0] && record["value"] && record["value"]["visibility"] === "public") {
|
|
||||||
posts.push({
|
|
||||||
title: record["value"]["title"],
|
|
||||||
rkey: matches[0],
|
|
||||||
createdAt: new Date(record["value"]["createdAt"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { posts };
|
|
||||||
} catch {
|
|
||||||
//ignored
|
|
||||||
}
|
|
||||||
return {posts: []};
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const {data} = $props()
|
const {data} = $props()
|
||||||
const posts = data.posts
|
const posts = Array.from(data.posts.values())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import type { Post } from '$lib/parser.ts';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
// TODO: make it so links scroll to the right spot
|
let post: Post | undefined = $state(undefined)
|
||||||
|
if ( data.posts.has(data.rkey)) {
|
||||||
|
post = data.posts.get(data.rkey) as Post
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.title} - WhiteBreeze</title>
|
{#if post !== undefined}
|
||||||
|
<title>{post.title} - WhiteBreeze</title>
|
||||||
|
{:else}
|
||||||
|
<title>WhiteBreeze</title>
|
||||||
|
{/if}
|
||||||
<meta name="description" content="This is where the description goes for SEO" />
|
<meta name="description" content="This is where the description goes for SEO" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 class="text-center pt-4 pb-4">{data.title}</h1>
|
{#if post !== undefined}
|
||||||
<hr class="pb-4">
|
<h1 class="text-center pt-4 pb-4">{post.title}</h1>
|
||||||
<article class="mx-auto max-w-4xl px-2 sm:px-6 lg:px-8 prose dark:prose-invert">
|
<hr class="pb-4">
|
||||||
{@html data.mdcontent}
|
<article class="mx-auto max-w-4xl px-2 sm:px-6 lg:px-8 prose dark:prose-invert">
|
||||||
</article>
|
{@html post.content}
|
||||||
|
</article>
|
||||||
|
{:else}
|
||||||
|
<h1 class="text-center pt-4 pb-4">No such post</h1>
|
||||||
|
{/if}
|
5
src/routes/[rkey]/+page.ts
Normal file
5
src/routes/[rkey]/+page.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const load = ({ params }) => {
|
||||||
|
return {
|
||||||
|
rkey: params.rkey
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue