Compare commits

...

43 Commits

Author SHA1 Message Date
Leni Aniva cc44b37a35 Merge pull request 'feat: Use new entity-based cover' (#42) from home/cover into main
Reviewed-on: #42
2025-07-06 15:05:36 -07:00
Leni Aniva 1a3b0a2e05
merge: branch 'main' into home/cover 2025-07-05 21:25:27 -07:00
Leni Aniva 795e10938e Merge pull request 'feat(extra): Specialize Research and Archive pages' (#43) from extra/research-archive into main
Reviewed-on: #43
2025-07-05 21:16:56 -07:00
Leni Aniva 50555eb10c
feat: Specialize /research and /archives 2025-07-05 20:45:04 -07:00
Leni Aniva ed2f417999
fix: Graphics of the cover 2025-06-23 08:57:49 -07:00
Leni Aniva afc3e2d2fb
fix: Occlusion and perspective 2025-06-22 22:15:32 -07:00
Leni Aniva 24c7b137df
feat: Add background for mobile mode 2025-06-22 21:29:22 -07:00
Leni Aniva f014ebd1cd
fix: Perspective 2025-06-22 17:16:36 -07:00
Leni Aniva 6024a151b2
fix: Do not nest `p` inside `div` 2025-06-22 17:01:55 -07:00
Leni Aniva 3f71107a0a
fix: All special effects 2025-06-22 16:42:32 -07:00
Leni Aniva f5843184d5
feat: Add robots.txt 2025-06-22 16:27:55 -07:00
Leni Aniva 2d1e5d20fb
feat: Add aria-label 2025-06-22 16:25:32 -07:00
Leni Aniva 8d0fb521d4
feat: Dark mode svg 2025-06-22 16:21:39 -07:00
Leni Aniva 33ebd62026
fix: Style on mobile 2025-06-22 15:55:59 -07:00
Leni Aniva 23f715e6ac
feat: Links on cover image 2025-06-22 14:46:52 -07:00
Leni Aniva 83666fc504
chore: Update svelte, add new cover svg 2025-06-21 23:43:26 -07:00
Leni Aniva 045311168d Merge pull request 'feat: Front cover theme switch' (#40) from home/background into main
Reviewed-on: #40
2025-06-21 19:12:14 -07:00
Leni Aniva 2b11a969fc
feat: Use svh for front cover and theme-color 2025-02-04 20:35:55 -08:00
Leni Aniva 4b9a131d35
feat: Front cover theme switch 2025-02-04 17:15:26 -08:00
Leni Aniva c04e04667c
chore: Update version to 0.0.4 2025-01-25 16:12:48 -08:00
Leni Aniva 7cf3e31b22 Merge pull request 'fix: Background styling on iOS' (#38) from styling/cover into main
Reviewed-on: #38
2025-01-25 16:10:12 -08:00
Leni Aniva c2362e26ef Merge pull request 'feat: Sticky navbar' (#30) from component/navbar into main
Reviewed-on: #30
2025-01-25 16:09:00 -08:00
Leni Aniva 442e589874
fix: Background on iOS 2025-01-25 16:07:28 -08:00
Leni Aniva f8bdcb811a
fix: ARIA warning 2025-01-25 15:44:27 -08:00
Leni Aniva 1d7be8373f
merge: branch 'main' into component/navbar 2025-01-25 15:39:36 -08:00
Leni Aniva 7daa6eb70d Merge pull request 'feat: Series' (#31) from post/series into main
Reviewed-on: #31
2025-01-25 15:38:01 -08:00
Leni Aniva 662a298b28
fix: Posts with empty series 2025-01-25 15:36:12 -08:00
Leni Aniva e7984f9c33
merge: branch 'main' into post/series 2025-01-25 15:23:01 -08:00
Leni Aniva 7d9fe36140
test: Series navigation test 2025-01-25 15:17:34 -08:00
Leni Aniva 56e6efc795
feat: Implement series 2025-01-25 15:06:09 -08:00
Leni Aniva 6f5933d902
feat: Improved navbar on mobile 2025-01-25 14:32:12 -08:00
Leni Aniva 46ca660a13
merge: branch 'main' into component/navbar 2024-12-10 18:26:06 -08:00
Leni Aniva 5dbb8e7a8c
doc: Documentation about metadata.json 2024-11-05 11:55:27 -08:00
Leni Aniva 7b900c2993
feat: Read highlighter languages from metadata 2024-11-05 11:50:00 -08:00
Leni Aniva 0d87cea96f
feat: Add more icons 2024-11-05 11:37:04 -08:00
Leni Aniva d18c3f62c7
refactor: Add series filter on `getPosts` 2024-10-27 20:17:42 -07:00
Leni Aniva e8e52588e4
fix: Make name non-sticky 2024-10-26 18:19:39 -07:00
Leni Aniva 482f7e59a7
feat: Sticky navbar 2024-10-26 18:16:20 -07:00
Leni Aniva 78a414c747 Merge pull request 'fix: Page navigation links' (#25) from post/page into main
Reviewed-on: #25
2024-10-18 14:36:58 -07:00
Leni Aniva 0cea3461dc
fix: Styling current link 2024-10-18 11:42:16 -07:00
Leni Aniva 60fc46af4a
fix: Styling disabled nav-link 2024-10-18 11:34:16 -07:00
Leni Aniva d756598342
fix: Next page link on first page 2024-10-18 11:31:34 -07:00
Leni Aniva 6e7ccd73fc Merge pull request 'fix: Stacking series problem and undefined descriptions problem' (#24) from post/header into main
Reviewed-on: #24
2024-10-18 11:27:46 -07:00
32 changed files with 10076 additions and 6084 deletions

View File

@ -11,7 +11,7 @@ repos:
hooks: hooks:
- id: conventional-pre-commit - id: conventional-pre-commit
stages: [commit-msg] stages: [commit-msg]
args: [feat, fix, chore, test, build, merge, refactor] args: [feat, fix, chore, test, build, merge, refactor, doc]
- repo: local - repo: local
hooks: hooks:
- id: eslint - id: eslint

View File

@ -7,10 +7,10 @@ A blog framework
To create the production version, To create the production version,
```bash ```bash
npm run build pnpm run build
``` ```
You can preview the production build with `npm run preview`. You can preview the production build with `pnpm run dev --open`.
### Content ### Content
@ -19,28 +19,39 @@ To insert custom content, create a directory with a similar structure to
directory. The website will be built statically in `build/`. Note that changing directory. The website will be built statically in `build/`. Note that changing
`favicon.svg` will require modifying the `static/` folder. `favicon.svg` will require modifying the `static/` folder.
Set `metadata.json` based on the example to have a custom avatar and background
image.
- `name`: Displayed name on the home page
- `blogName`: Displayed name of the blog
- `url`: The site url
- `frontDescription`: The text above the main navbar
- `frontCover`: Path to the cover image
- `picture`: Avatar
- `links`: A map of brands to links
- `extra`: Extra pages showing on the navbar
- `markdown`: `{ "highlighterLangs": [ ... ], "katex": [ ... ] }`
## Developing ## Developing
Install [pre-commit](https://pre-commit.com/). Install [pre-commit](https://pre-commit.com/).
Install dependencies with `npm install` (or `pnpm install` or `yarn`). Then Install dependencies with `pnpm install`. Then start the dev server with
start the dev server with
```bash ```bash
npm run dev [-- --open] pnpm run dev [-- --open]
``` ```
### Formatting ### Formatting
``` bash ``` bash
npx eslint pnpx eslint
``` ```
### Testing ### Testing
To run tests To run tests
``` bash ``` bash
npx playwright test pnpx playwright test
``` ```
All tests must have the suffix `.spec.[jt]s`. Be cautious while updating All tests must have the suffix `.spec.[jt]s`. Be cautious while updating

5927
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "chrysoblog", "name": "chrysoblog",
"version": "0.0.2", "version": "0.0.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -16,47 +16,47 @@
"prepare": "svelte-kit sync" "prepare": "svelte-kit sync"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.29.0",
"@playwright/test": "^1.40.0", "@playwright/test": "^1.53.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^5.1.0",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.21",
"eslint": "^9.11.1", "eslint": "^9.29.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.9.0", "globals": "^15.15.0",
"mdsvex": "^0.12.3", "mdsvex": "^0.12.6",
"mdsvex-relative-images": "^1.0.3", "mdsvex-relative-images": "^1.0.3",
"prettier": "^3.1.1", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.13",
"rehype-katex-svelte": "^1.2.0", "rehype-katex-svelte": "^1.2.0",
"remark-math": "^3.0.0", "remark-math": "^3.0.1",
"svelte": "^4.2.7", "svelte": "^5.34.7",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.17",
"typescript": "5.5", "typescript": "~5.5.4",
"typescript-eslint": "^8.7.0", "typescript-eslint": "^8.34.1",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vite": "^5.0.3", "vite": "^6.0.0",
"vitest": "^2.0.0" "vitest": "^2.1.9"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.7.2",
"bits-ui": "^0.21.13", "bits-ui": "^2.8.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.451.0", "lucide-svelte": "^0.451.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-github-blockquote-alert": "^1.2.1", "remark-github-blockquote-alert": "^1.3.1",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"shiki": "^1.22.0", "shiki": "^1.29.2",
"svelte-radix": "^1.1.1", "svelte-radix": "^1.1.1",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.2.1" "tailwind-variants": "^0.2.1"
} }
} }

4258
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -115,16 +115,18 @@ a, button {
text-shadow: 0 0 5px; text-shadow: 0 0 5px;
} }
.nav-link, .nav-link * { .nav-link, .nav-link * {
color: var(--navlink);
@apply text-lg; @apply text-lg;
font-weight: 500; font-weight: 500;
font-family: serif; font-family: serif;
} }
.nav-link:not(.disabled-link):not(.current-link) {
color: var(--navlink);
}
a.disabled-link { a.disabled-link {
color: rgb(128,128,128); color: rgb(128,128,128);
pointer-events: none; pointer-events: none;
} }
a:not(.icon) { main a:not(.icon) {
position: relative; position: relative;
text-decoration: inherit; text-decoration: inherit;

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<meta name="theme-color" content="#028760" />
</head> </head>
<body data-sveltekit-preload-data="hover" class="font-serif"> <body data-sveltekit-preload-data="hover" class="font-serif">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>

View File

@ -0,0 +1,6 @@
---
title: Research
---
# Research
I teach Lean!

View File

@ -12,5 +12,12 @@
}, },
"extra": [ "extra": [
{ "route": "/archives", "name": "Archives" } { "route": "/archives", "name": "Archives" }
],
"markdown": {
"highlighterLangs": [
"python",
"lean",
"tex"
] ]
}
} }

View File

@ -26,3 +26,7 @@ $$
$$ $$
\vec z^{(l+1)} = \mat M\vec z^{(l)} + \vec b \vec z^{(l+1)} = \mat M\vec z^{(l)} + \vec b
$$ $$
```tex
\\newcommand{\\cirno}{9}
```

8
src/lib/Cover.svelte Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -37,9 +37,7 @@
<p class="text-gray-200 flex justify-center"> <p class="text-gray-200 flex justify-center">
{#each (post?.tags ?? []) as tag, i} {#each (post?.tags ?? []) as tag, i}
{#if i > 0} {#if i > 0}
<div class="separator text-sm">
<DividerVertical /> <DividerVertical />
</div>
{/if} {/if}
<a href="/tag/{tag}">{tag}</a> <a href="/tag/{tag}">{tag}</a>
{/each} {/each}

View File

@ -20,7 +20,10 @@
} }
</script> </script>
<button class="nav-link" on:click={toggleTheme} aria-label="Toggle Theme"> <button
id="theme-switch"
class="nav-link"
on:click={toggleTheme} aria-label="Toggle Theme">
{#if $theme === "tokiwa"} {#if $theme === "tokiwa"}
<Sun /> <Sun />
{:else} {:else}
@ -30,6 +33,7 @@
<style> <style>
button { button {
flex-shrink: 0;
padding: 0; padding: 0;
font-weight: inherit; font-weight: inherit;
background: none; background: none;

5266
src/lib/cover-work.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 340 KiB

View File

@ -15,7 +15,8 @@ export function transformPostMeta(metadata: PostMetadata, slug: string) : Post {
}; };
} }
export async function getPosts(tag: string | null = null) : Promise<Post[]> { // Generates a list of all posts filtering by tags and series, reverse chronologically ordered.
export async function getPosts(filter: { tag?: string, series?: string }) : Promise<Post[]> {
const allPostFiles = import.meta.glob('$content/post/*.md'); const allPostFiles = import.meta.glob('$content/post/*.md');
const iterablePostFiles = Object.entries(allPostFiles); const iterablePostFiles = Object.entries(allPostFiles);
@ -27,8 +28,10 @@ export async function getPosts(tag: string | null = null) : Promise<Post[]> {
return transformPostMeta(metadata, slug); return transformPostMeta(metadata, slug);
}) })
); );
if (tag) if (filter?.tag)
posts = posts.filter(post => post.tags.includes(tag)) posts = posts.filter(post => post.tags.includes(filter.tag!))
if (filter?.series)
posts = posts.filter(post => post.series?.includes(filter.series!) ?? false)
posts.sort((post1, post2) => { posts.sort((post1, post2) => {
const date1: Date = post1.date; const date1: Date = post1.date;

View File

@ -1,8 +1,8 @@
import siteMetadata from '$content/metadata.json';
export const routes: { route: string, name: string, disabled?: boolean }[] = [ export const routes: { route: string, name: string, disabled?: boolean }[] = [
{ route: "/", name: "Home" }, { route: "/", name: "Home" },
{ route: "/post", name: "Blog" }, { route: "/post", name: "Blog" },
{ route: "/tag", name: "Tags" }, { route: "/tag", name: "Tags" },
{ route: "/gallery", name: "Gallery", disabled: true }, { route: "/gallery", name: "Gallery", disabled: true },
].concat(siteMetadata?.extra || []); { route: "/research", name: "Research" },
{ route: "/archives", name: "Archives" },
]

View File

@ -1,4 +1,4 @@
import { writable } from 'svelte/store' import { writable, get } from 'svelte/store'
import { browser } from "$app/environment" import { browser } from "$app/environment"
type Theme = 'tokiwa' | 'star' type Theme = 'tokiwa' | 'star'
@ -11,3 +11,11 @@ export const theme = writable(userTheme ?? 'tokiwa')
export function setTheme(newTheme: Theme) { export function setTheme(newTheme: Theme) {
theme.set(newTheme) theme.set(newTheme)
} }
export function getThemeObject(key: string, obj: { [key in Theme]: string } | string) {
if (typeof obj === 'object') {
return obj[key];
} else {
return obj;
}
}

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { name } from '$content/metadata.json';
import Footer from "./Footer.svelte"; import Footer from "./Footer.svelte";
import Navbar from "./Navbar.svelte"; import Navbar from "./Navbar.svelte";
import PageTransition from "./PageTransition.svelte"; import PageTransition from "./PageTransition.svelte";
@ -11,7 +12,8 @@
<!-- Homepage has its own navbar with animations --> <!-- Homepage has its own navbar with animations -->
{#if $page.url.pathname != "/"} {#if $page.url.pathname != "/"}
<div id="layout"> <div id="layout">
<div id="navbar"> <div id="nav">
<p id="bio-name">{name}</p>
<Navbar /> <Navbar />
<slot name="header" /> <slot name="header" />
</div> </div>
@ -31,6 +33,12 @@
{/if} {/if}
<style> <style>
#bio-name {
font-family: serif;
font-size: 3rem;
font-weight: normal;
color: var(--name);
}
#layout { #layout {
max-width: max(80vw, 100vh); max-width: max(80vw, 100vh);
margin-left: auto; margin-left: auto;
@ -58,7 +66,7 @@
width: min(100vw, max(50vw, 100vh)); width: min(100vw, max(50vw, 100vh));
grid-area: content; grid-area: content;
} }
#navbar { #nav {
align-self: right; align-self: right;
padding-left: 10px; padding-left: 10px;
grid-area: nav; grid-area: nav;

View File

@ -1,12 +1,39 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { derived } from 'svelte/store'
import { routes } from "$lib/sitemap.ts" import { routes } from "$lib/sitemap.ts"
import { Separator } from "$lib/components/ui/separator/index.js"; import { Separator } from "$lib/components/ui/separator/index.js";
import { theme, setTheme, getThemeObject } from '$lib/theme'
import { Rss } from "lucide-svelte"; import { Rss } from "lucide-svelte";
import ThemeSwitch from "$lib/components/ThemeSwitch.svelte" import ThemeSwitch from "$lib/components/ThemeSwitch.svelte"
import Cover from "$lib/Cover.svelte"
let scrollPosition: number = .5; let scrollPosition: number = .5;
let scrollHeight: number = 1; let scrollHeight: number = 1;
let progress: number = 0; let progress: number
= 0.0;
let button_blog;
function toggleTheme() {
theme.update((currentTheme) => {
const newTheme = currentTheme === 'tokiwa' ? 'star' : 'tokiwa'
document.documentElement.setAttribute('color-scheme', newTheme)
localStorage.setItem('color-scheme', newTheme)
if (newTheme === "tokiwa") {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
setTheme(newTheme);
return newTheme
})
}
onMount(() => {
let entity_switch = document.getElementById("switch");
entity_switch.addEventListener("click", toggleTheme);
});
function onScroll() { function onScroll() {
if (scrollHeight > 1) { if (scrollHeight > 1) {
progress = Math.max(0, Math.min(1, scrollPosition / scrollHeight)); progress = Math.max(0, Math.min(1, scrollPosition / scrollHeight));
@ -19,15 +46,22 @@
import metadata from '$content/metadata.json'; import metadata from '$content/metadata.json';
const iconMap: Map<string, string> = { const iconMap: Map<string, string> = {
"bitbucket": "fa-brands fa-bitbucket", "orcid": "fa-brands fa-orcid",
"google-scholar": "fa-brands fa-google-scholar",
"discord": "fa-brands fa-discord", "discord": "fa-brands fa-discord",
"email": "fa-solid fa-envelope", "email": "fa-solid fa-envelope",
"bitbucket": "fa-brands fa-bitbucket",
"github": "fa-brands fa-github", "github": "fa-brands fa-github",
"gitlab": "fa-brands fa-gitlab", "gitlab": "fa-brands fa-gitlab",
"google-scholar": "fa-brands fa-google-scholar",
"instagram": "fa-brands fa-instagram",
"open-source": "fa-brands fa-osi", "open-source": "fa-brands fa-osi",
"orcid": "fa-brands fa-orcid",
"bluesky": "fa-brands fa-bluesky",
"youtube": "fa-brands fa-youtube",
"instagram": "fa-brands fa-instagram",
"bandcamp": "fa-brands fa-bandcamp",
"pixiv": "fa-brands fa-pixiv",
}; };
const iconLinks: [string, string][] = Object.entries(metadata.links).map( const iconLinks: [string, string][] = Object.entries(metadata.links).map(
([key, link]) => [iconMap[key], link]); ([key, link]) => [iconMap[key], link]);
@ -38,36 +72,31 @@
</svelte:head> </svelte:head>
<div <div
id="background" id="cover"
style="opacity: {1 - progress}; background-image: url('{metadata.frontCover}')" style="opacity: {1 - progress}; --grid-line: #999; --grid-line-dark: #333; --grid-cell: #707078"
> >
</div> <div id="cover-image">
<Cover />
</div>
<div id="front"> <div id="navbar">
<div id="floater"> <div>
<p class="text-2xl floater-elem" style="color: #888">{metadata.frontDescription}</p> <p class="text-2xl" style="color: #888; z-index: 2">{metadata.frontDescription}</p>
<div </div>
class="floater-elem" <div id="link-container" class="h-5 items-center">
style="padding-bottom: {(1 - progress) * 50}vh"
>
<div id="navbar" class="h-5 items-center space-x-4 text-sm">
<a class="nav-link" href="#bio">Bio</a> <a class="nav-link" href="#bio">Bio</a>
<Separator class="bg-gray-500 dark:bg-gray-100" orientation="vertical" />
{#each routes as item} {#each routes as item}
{#if item.route !== "/"} {#if item.route !== "/"}
<a <a
href={item.route} href={item.route}
class="nav-link" class="nav-link"
class:disabled-link={item.disabled}>{item.name}</a> class:disabled-link={item.disabled}>{item.name}</a>
<Separator class="bg-gray-500 dark:bg-gray-100" orientation="vertical" />
{/if} {/if}
{/each} {/each}
<a class="icon nav-link" href="/rss.xml"><Rss /></a> <a class="icon nav-link" href="/rss.xml"><Rss /></a>
<Separator class="bg-gray-500 dark:bg-gray-100" orientation="vertical" />
<ThemeSwitch /> <ThemeSwitch />
</div> </div>
</div> </div>
</div>
</div> </div>
<main> <main>
@ -78,7 +107,7 @@
<p id="bio-name">{name}</p> <p id="bio-name">{name}</p>
<div class="w-100"> <div class="w-100">
{#each iconLinks as [cl, link]} {#each iconLinks as [cl, link]}
<a class="icon m-1" href={link}><i class="fa-xl {cl}"></i></a> <a class="icon m-1" href={link} aria-label={cl}><i class="fa-xl {cl}"></i></a>
{/each} {/each}
</div> </div>
</div> </div>
@ -100,40 +129,130 @@
/> />
<style> <style>
#background { #cover {
background-attachment: fixed; background:
background-size: cover; repeating-linear-gradient(128.6deg, transparent, var(--grid-line) 1px, var(--grid-line) 2px, transparent 1px, transparent 55px),
background-repeat: no-repeat; repeating-linear-gradient(11deg, transparent, var(--grid-line) 1px, var(--grid-line) 2px, transparent 1px, transparent 40px),
background-position: center center; var(--grid-cell)
z-index: -10; ;
position: fixed; /*
min-width: 100%; position: absolute;
min-height: 100%; background-size: contain;
background-position: center center; */
overscroll-behaviour: none; overscroll-behaviour: none;
}
#front {
height: 100vh;
display: flex; display: flex;
flex-direction: column;
overscroll-behavior: none;
}
#floater {
display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
margin-top: auto; flex-direction: column;
margin-left: auto; visibility: visible;
margin-right: auto; }
:global(.dark #cover) {
background:
repeating-linear-gradient(128.6deg, transparent, var(--grid-line-dark) 1px, var(--grid-line-dark) 2px, transparent 1px, transparent 55px),
repeating-linear-gradient(11deg, transparent, var(--grid-line-dark) 1px, var(--grid-line-dark) 2px, transparent 1px, transparent 40px),
black
;
}
:global(#cover-image > svg) {
max-width: 100svw;
max-height: 100svh;
width: 100%;
height: 100%;
}
:global(svg a) {
pointer-events: all;
:global(path) {
filter: contrast(50%);
}
:global(.effect) {
visibility: hidden;
}
&:hover,
&:focus {
:global(.effect) {
visibility: visible;
}
:global(path) {
filter: contrast(100%);
}
}
}
:global(.dark .shadow) {
visibility: hidden;
}
:global(svg .shadow) {
pointer-events: none;
}
:global(#switch-right) {
visibility: hidden;
}
:global(#switch-left) {
visibility: visible;
}
:global(.dark #switch-right) {
visibility: visible;
}
:global(.dark #switch-left) {
visibility: hidden;
}
:global(.effect > .glow) {
filter: blur(.3rem);
}
.nav-link {
display: inline-block;
} }
#navbar { #navbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; padding-top: 30%;
flex-direction: column;
div {
padding-left: 5%;
} }
.floater-elem {
margin-left: auto;
margin-right: auto;
} }
#link-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* mobile */
@media (max-width: 700px) {
#navbar {
visibility: visible;
height: 100svh;
}
#cover {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
#cover-image {
position: absolute;
max-height: 100svh;
max-width: 100svw;
pointer-events: none;
:global(.effect) {
visibility: visible;
}
}
:global(#cover-image a) {
pointer-events: none;
}
:global(svg .entity-label) {
visibility: hidden;
}
}
/* desktop */
@media (min-width: 701px) {
#navbar {
padding-top: 0;
visibility: hidden;
height: 0;
width: 0;
}
}
/* main */
main { main {
width: 100%; width: 100%;
@apply bg-background; @apply bg-background;

View File

@ -1,43 +1,95 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { routes } from "$lib/sitemap.ts" import { routes } from "$lib/sitemap.ts"
import { name } from '$content/metadata.json';
import ThemeSwitch from "$lib/components/ThemeSwitch.svelte" import ThemeSwitch from "$lib/components/ThemeSwitch.svelte"
import { Rss } from "lucide-svelte"; import { Rss, Menu } from "lucide-svelte";
function isCurrentLink(pathname, route) { function isCurrentLink(pathname, route) {
return route != "/" && pathname.startsWith(route) return route != "/" && pathname.startsWith(route)
|| route == "/post" && pathname.startsWith("/page"); || route == "/post" && pathname.startsWith("/page");
} }
import { onMount } from 'svelte';
let showDropdown = false;
function onDropdownToggle(event) {
showDropdown = !showDropdown;
}
function onKeyDown(event) {
if (event.key == "Escape") {
showDropdown = !showDropdown;
}
}
</script> </script>
<div id="navbar" class="h-5 space-x-4"> <nav
<p id="bio-name">{name}</p> id="navbar"
class="h-5 space-x-4"
class:dropdown-opened={showDropdown}>
<div
class="link-container"
role="button"
tabindex={0}
on:keydown={onKeyDown}
on:click={() => { showDropdown = false; }}
>
{#each routes as item} {#each routes as item}
<a <a
class="nav-link" class="nav-link"
class:current-link={isCurrentLink($page.url.pathname, item.route)}
class:disabled-link={item.disabled} class:disabled-link={item.disabled}
href={item.route}>{item.name}</a> class:current-link={isCurrentLink($page.url.pathname, item.route)}
on:click={() => { showDropdown = false; }}
href={item.route}>
{item.name}
</a>
{/each} {/each}
<a class="nav-link icon" href="/rss.xml"><Rss /></a> <a class="nav-link icon" href="/rss.xml"><Rss /></a>
<div><ThemeSwitch /></div> <div><ThemeSwitch /></div>
</div> </div>
<button
id="dropdown-toggle"
aria-hidden="true"
on:click={onDropdownToggle}>
<Menu />
</button>
</nav>
<svelte:window on:keydown={onKeyDown} />
<style> <style>
#bio-name { .link-container {
font-family: serif;
font-size: 3rem;
font-weight: normal;
color: var(--name);
}
#navbar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
justify-content: between; position: sticky;
top: 0;
}
#dropdown-toggle {
display: none;
}
/* mobile mode */
@media (max-width: 768px) {
.link-container {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 100vh;
justify-content: center;
margin: 0;
padding-left: 5vw;
padding-right: 5vw;
background: hsl(var(--background) / .8);
/* Initial hidden state */
opacity: 0;
transform: translateY(-100%);
transition: transform 0.2s, opacity 0.2s;
} }
@media (max-width: 700px) {
#navbar { #navbar {
display: inline-block; display: inline-block;
white-space: pre-wrap; white-space: pre-wrap;
@ -45,5 +97,14 @@
#navbar .nav-link { #navbar .nav-link {
display: inline-block; display: inline-block;
} }
#dropdown-toggle {
display: initial;
z-index: 5;
}
.dropdown-opened > .link-container {
opacity: 1;
z-index: 1;
transform: translateY(0);
}
} }
</style> </style>

View File

@ -1,12 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const post = await import(`$content/extra/${params.slug}.md`);
const Content = post.default;
return {
slug: params.slug,
metadata: post.metadata,
Content,
};
}

View File

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
const { slug, metadata, Content } = data; const { metadata, Content } = data;
import siteMetadata from '$content/metadata.json'; import siteMetadata from '$content/metadata.json';
const title = metadata?.title || (slug[0].toUpperCase() + slug.slice(1)); const title = metadata?.title
</script> </script>
<svelte:head> <svelte:head>

View File

@ -0,0 +1,14 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async (_) => {
const post = await import(`$content/extra/archives.md`);
if (!post) throw error(404);
const { name } = post.metadata;
const Content = post.default;
return {
name,
Content,
};
}

View File

@ -12,4 +12,4 @@
</svelte:head> </svelte:head>
<hr class="separator" /> <hr class="separator" />
<PostList {posts} pageN=1 {maxPageN}/> <PostList {posts} pageN={1} {maxPageN}/>

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { TrackPrevious, TrackNext } from 'svelte-radix';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
const { post, Content } = data; const { post, Content, seriesNeighbours } = data;
import siteMetadata from '$content/metadata.json'; import siteMetadata from '$content/metadata.json';
import PostHeader from '$lib/components/PostHeader.svelte'; import PostHeader from '$lib/components/PostHeader.svelte';
</script> </script>
@ -17,3 +18,39 @@
<Content /> <Content />
</article> </article>
<hr /> <hr />
<ul id="series">
{#each seriesNeighbours as { name, prevSlug, prevTitle, nextSlug, nextTitle }}
<li class="series-neighbour">
{#if prevTitle}
<a href="/post/{prevSlug}">{prevTitle}</a>
<p class="dummy"><TrackPrevious /></p>
{/if}
<p class="series-tag">{name}</p>
{#if nextTitle}
<p class="dummy"><TrackNext /></p>
<a href="/post/{nextSlug}">{nextTitle}</a>
{/if}
</li>
{/each}
</ul>
<style>
#series {
text-align: center;
}
.series-tag {
color: var(--series);
display: inline-block;
}
.dummy {
display: inline-block;
color: rgba(128,128,128, .6);
vertical-align: middle;
padding-left: 1em;
padding-right: 1em;
}
.series-neighbour {
padding-left: 2em;
padding-right: 2em;
}
</style>

View File

@ -1,16 +1,54 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { transformPostMeta } from '$lib/posts'; import type { Post } from '$lib/types'
import { getPosts, transformPostMeta } from '$lib/posts';
export const load: PageLoad = async ({ params }) => { export const load: PageLoad = async ({ params }) => {
try { try {
const post = await import(`$content/post/${params.slug}.md`); const post = await import(`$content/post/${params.slug}.md`);
if (!post) throw error(404); if (!post) throw error(404);
const metadata: Post = transformPostMeta(post.metadata, params.slug);
const Content = post.default; const Content = post.default;
const seriesNeighbours : {
name: string,
prevSlug?: string,
prevTitle?: string,
nextSlug?: string,
nextTitle?: string,
}[] = await Promise.all((metadata.series || []).map(async (series: string) => {
const postInSeries: Post[] = await getPosts({ series });
let prevSlug = undefined;
let prevTitle = undefined;
let nextSlug = undefined;
let nextTitle = undefined;
for (const p of postInSeries) {
if (p.date > metadata.date) {
nextSlug = p.slug;
nextTitle = p.title;
}
if (p.date < metadata.date) {
prevSlug = p.slug;
prevTitle = p.title;
break;
}
}
return { return {
post: transformPostMeta(post.metadata, params.slug), name: series,
prevSlug,
prevTitle,
nextSlug,
nextTitle,
};
}));
return {
post: metadata,
Content, Content,
seriesNeighbours,
}; };
} }
catch { catch {

View File

@ -0,0 +1,18 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const { metadata, Content } = data;
import siteMetadata from '$content/metadata.json';
const title = metadata?.title
</script>
<svelte:head>
<title>{title} | {siteMetadata.blogName}</title>
</svelte:head>
<hr />
<article class="prose lg:prose-xl dark:prose-dark p-4">
<Content />
</article>
<hr />

View File

@ -0,0 +1,14 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async (_) => {
const post = await import(`$content/extra/research.md`);
if (!post) throw error(404);
const { name } = post.metadata;
const Content = post.default;
return {
name,
Content,
};
}

View File

@ -2,7 +2,7 @@ import type { PageLoad } from './$types';
import { getPosts } from '$lib/posts'; import { getPosts } from '$lib/posts';
export const load: PageLoad = async ({ params }) => { export const load: PageLoad = async ({ params }) => {
const posts = await getPosts(params.slug); const posts = await getPosts({ tag: params.slug });
return { return {
name: params.slug, name: params.slug,
posts, posts,

34
static/robots.txt Normal file
View File

@ -0,0 +1,34 @@
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Amazonbot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-Agent: FacebookBot
Disallow: /
User-Agent: Applebot
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: Diffbot
Disallow: /
User-agent: ImagesiftBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-agent: Omgili
Disallow: /
User-agent: YouBot
Disallow: /

View File

@ -1,5 +1,6 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import adapter from '@sveltejs/adapter-static'; import adapter from '@sveltejs/adapter-static';
import fs from 'fs';
import { mdsvex, escapeSvelte } from 'mdsvex'; import { mdsvex, escapeSvelte } from 'mdsvex';
import remarkAlert from './src/lib/markdown.js' import remarkAlert from './src/lib/markdown.js'
@ -12,7 +13,9 @@ import remarkMath from 'remark-math'
const contentDir = process.env?.CHRYSOBLOG_CONTENT ?? "src/content"; const contentDir = process.env?.CHRYSOBLOG_CONTENT ?? "src/content";
const langs = [ const metadata = await fs.promises.readFile(contentDir + "/metadata.json")
.then((file) => JSON.parse(file.toString()));
const langs = metadata?.markdown.highlighterLangs ?? [
"javascript", "javascript",
"lean", "lean",
"lisp", "lisp",
@ -26,11 +29,7 @@ const langs = [
"c++", "c++",
"sh", "sh",
]; ];
const highlightingThemes = { const katexOptions = metadata?.markdown.katex ?? {
light: 'vitesse-light',
dark: 'vitesse-dark',
};
const katexOptions = {
macros: { macros: {
"\\Nat": "\\mathbb N", "\\Nat": "\\mathbb N",
"\\Complex": "\\mathbb C", "\\Complex": "\\mathbb C",
@ -39,6 +38,10 @@ const katexOptions = {
"\\rv": "\\mathrm", "\\rv": "\\mathrm",
}, },
}; };
const highlightingThemes = {
light: 'vitesse-light',
dark: 'vitesse-dark',
};
const themes = Object.keys(highlightingThemes).map(function(k){ return highlightingThemes[k] }); const themes = Object.keys(highlightingThemes).map(function(k){ return highlightingThemes[k] });
const highlighter = await getHighlighter({ const highlighter = await getHighlighter({

View File

@ -0,0 +1,9 @@
import { expect, test } from '@playwright/test';
test('Navigate in series', async ({ page }) => {
await page.goto('/post');
await page.getByText('Placeholder 2').click();
await expect(page).toHaveURL("/post/placeholder2/");
await page.getByText('Placeholder 1').click();
await expect(page).toHaveURL("/post/placeholder1/");
});