mirror of
https://github.com/tonytins/tonybark.com.git
synced 2025-03-15 07:21:21 +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:
|
||||
```sh
|
||||
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
|
||||
|
|
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": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"preview": "vite preview -v",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint ."
|
||||
|
@ -25,13 +25,11 @@
|
|||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.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": {
|
||||
"@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-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { profile } from "./identity";
|
||||
const { profile } = $props()
|
||||
</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;">
|
||||
|
@ -11,10 +11,6 @@
|
|||
</div>
|
||||
<div class="p-3">
|
||||
<p class="">{profile.description}</p>
|
||||
{#if profile.about}
|
||||
<br/>
|
||||
<p class="">{profile.about}</p>
|
||||
{/if}
|
||||
</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 remarkParse from 'remark-parse'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
||||
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 type { Node } from 'unist'
|
||||
import type { Root, Element } from 'hast'
|
||||
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:
|
||||
// https://github.com/whtwnd/whitewind-blog/blob/7eb8d4623eea617fd562b93d66a0e235323a2f9a/frontend/src/services/DocProvider.tsx#L122
|
||||
const customSchema = {
|
||||
|
@ -63,14 +75,14 @@ const rehypeUpgradeImage: Plugin<any, Root, Node> = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export async function load({ params }) {
|
||||
const rawResponse = await fetch(`${pds}/xrpc/com.atproto.repo.getRecord?repo=${profile.did}&collection=com.whtwnd.blog.entry&rkey=${params.rkey}`)
|
||||
const response = await rawResponse.json()
|
||||
try {
|
||||
|
||||
return {
|
||||
title: response["value"]["title"],
|
||||
mdcontent: String(
|
||||
export async function parse(mdposts: Map<string, MarkdownPost>) {
|
||||
let posts: Map<string, Post> = new Map()
|
||||
for (let [ rkey, post ] of mdposts) {
|
||||
posts.set(rkey, {
|
||||
title: post.title,
|
||||
rkey: post.rkey,
|
||||
createdAt: post.createdAt,
|
||||
content: String(
|
||||
await unified()
|
||||
.use(remarkParse, { fragment: true }) // Parse the MD
|
||||
.use(remarkGfm) // Parse GH specific MD
|
||||
|
@ -79,11 +91,9 @@ export async function load({ params }) {
|
|||
.use(rehypeUpgradeImage)
|
||||
.use(rehypeSanitize, customSchema as Schema) // Sanitize the HTML
|
||||
.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">
|
||||
import Profile from '$lib/Profile.svelte';
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
let { data, children } = $props();
|
||||
</script>
|
||||
|
||||
<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-[large]" href="/about">About</a>
|
||||
</nav>
|
||||
<Profile/>
|
||||
<Profile profile={data.profile}/>
|
||||
{@render children()}
|
||||
</div>
|
||||
<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">
|
||||
const {data} = $props()
|
||||
const posts = data.posts
|
||||
const posts = Array.from(data.posts.values())
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import type { Post } from '$lib/parser.ts';
|
||||
|
||||
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>
|
||||
|
||||
|
||||
<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" />
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-center pt-4 pb-4">{data.title}</h1>
|
||||
<hr class="pb-4">
|
||||
<article class="mx-auto max-w-4xl px-2 sm:px-6 lg:px-8 prose dark:prose-invert">
|
||||
{@html data.mdcontent}
|
||||
</article>
|
||||
{#if post !== undefined}
|
||||
<h1 class="text-center pt-4 pb-4">{post.title}</h1>
|
||||
<hr class="pb-4">
|
||||
<article class="mx-auto max-w-4xl px-2 sm:px-6 lg:px-8 prose dark:prose-invert">
|
||||
{@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