Compare commits
24 Commits
post/heade
...
main
Author | SHA1 | Date |
---|---|---|
|
c04e04667c | |
|
7cf3e31b22 | |
|
c2362e26ef | |
|
442e589874 | |
|
f8bdcb811a | |
|
1d7be8373f | |
|
7daa6eb70d | |
|
662a298b28 | |
|
e7984f9c33 | |
|
7d9fe36140 | |
|
56e6efc795 | |
|
6f5933d902 | |
|
46ca660a13 | |
|
5dbb8e7a8c | |
|
7b900c2993 | |
|
0d87cea96f | |
|
d18c3f62c7 | |
|
e8e52588e4 | |
|
482f7e59a7 | |
|
78a414c747 | |
|
0cea3461dc | |
|
60fc46af4a | |
|
d756598342 | |
|
6e7ccd73fc |
|
@ -11,7 +11,7 @@ repos:
|
|||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: [feat, fix, chore, test, build, merge, refactor]
|
||||
args: [feat, fix, chore, test, build, merge, refactor, doc]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: eslint
|
||||
|
|
12
README.md
12
README.md
|
@ -19,6 +19,18 @@ To insert custom content, create a directory with a similar structure to
|
|||
directory. The website will be built statically in `build/`. Note that changing
|
||||
`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
|
||||
|
||||
Install [pre-commit](https://pre-commit.com/).
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "chrysoblog",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chrysoblog",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"bits-ui": "^0.21.13",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chrysoblog",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
|
|
@ -115,11 +115,13 @@ a, button {
|
|||
text-shadow: 0 0 5px;
|
||||
}
|
||||
.nav-link, .nav-link * {
|
||||
color: var(--navlink);
|
||||
@apply text-lg;
|
||||
font-weight: 500;
|
||||
font-family: serif;
|
||||
}
|
||||
.nav-link:not(.disabled-link):not(.current-link) {
|
||||
color: var(--navlink);
|
||||
}
|
||||
a.disabled-link {
|
||||
color: rgb(128,128,128);
|
||||
pointer-events: none;
|
||||
|
|
|
@ -12,5 +12,12 @@
|
|||
},
|
||||
"extra": [
|
||||
{ "route": "/archives", "name": "Archives" }
|
||||
],
|
||||
"markdown": {
|
||||
"highlighterLangs": [
|
||||
"python",
|
||||
"lean",
|
||||
"tex"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,3 +26,7 @@ $$
|
|||
$$
|
||||
\vec z^{(l+1)} = \mat M\vec z^{(l)} + \vec b
|
||||
$$
|
||||
|
||||
```tex
|
||||
\\newcommand{\\cirno}{9}
|
||||
```
|
||||
|
|
|
@ -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 iterablePostFiles = Object.entries(allPostFiles);
|
||||
|
||||
|
@ -27,8 +28,10 @@ export async function getPosts(tag: string | null = null) : Promise<Post[]> {
|
|||
return transformPostMeta(metadata, slug);
|
||||
})
|
||||
);
|
||||
if (tag)
|
||||
posts = posts.filter(post => post.tags.includes(tag))
|
||||
if (filter?.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) => {
|
||||
const date1: Date = post1.date;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { page } from '$app/stores';
|
||||
import { name } from '$content/metadata.json';
|
||||
import Footer from "./Footer.svelte";
|
||||
import Navbar from "./Navbar.svelte";
|
||||
import PageTransition from "./PageTransition.svelte";
|
||||
|
@ -11,7 +12,8 @@
|
|||
<!-- Homepage has its own navbar with animations -->
|
||||
{#if $page.url.pathname != "/"}
|
||||
<div id="layout">
|
||||
<div id="navbar">
|
||||
<div id="nav">
|
||||
<p id="bio-name">{name}</p>
|
||||
<Navbar />
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
@ -31,6 +33,12 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
#bio-name {
|
||||
font-family: serif;
|
||||
font-size: 3rem;
|
||||
font-weight: normal;
|
||||
color: var(--name);
|
||||
}
|
||||
#layout {
|
||||
max-width: max(80vw, 100vh);
|
||||
margin-left: auto;
|
||||
|
@ -58,7 +66,7 @@
|
|||
width: min(100vw, max(50vw, 100vh));
|
||||
grid-area: content;
|
||||
}
|
||||
#navbar {
|
||||
#nav {
|
||||
align-self: right;
|
||||
padding-left: 10px;
|
||||
grid-area: nav;
|
||||
|
|
|
@ -19,15 +19,22 @@
|
|||
|
||||
import metadata from '$content/metadata.json';
|
||||
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",
|
||||
"email": "fa-solid fa-envelope",
|
||||
|
||||
"bitbucket": "fa-brands fa-bitbucket",
|
||||
"github": "fa-brands fa-github",
|
||||
"gitlab": "fa-brands fa-gitlab",
|
||||
"google-scholar": "fa-brands fa-google-scholar",
|
||||
"instagram": "fa-brands fa-instagram",
|
||||
"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(
|
||||
([key, link]) => [iconMap[key], link]);
|
||||
|
@ -101,18 +108,17 @@
|
|||
|
||||
<style>
|
||||
#background {
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
z-index: -10;
|
||||
position: fixed;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
min-width: 100vw;
|
||||
height: 100vh;
|
||||
overscroll-behaviour: none;
|
||||
}
|
||||
#front {
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overscroll-behavior: none;
|
||||
|
|
|
@ -1,43 +1,95 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { routes } from "$lib/sitemap.ts"
|
||||
import { name } from '$content/metadata.json';
|
||||
import ThemeSwitch from "$lib/components/ThemeSwitch.svelte"
|
||||
import { Rss } from "lucide-svelte";
|
||||
import { Rss, Menu } from "lucide-svelte";
|
||||
|
||||
function isCurrentLink(pathname, route) {
|
||||
return route != "/" && pathname.startsWith(route)
|
||||
|| 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>
|
||||
|
||||
<div id="navbar" class="h-5 space-x-4">
|
||||
<p id="bio-name">{name}</p>
|
||||
<nav
|
||||
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}
|
||||
<a
|
||||
class="nav-link"
|
||||
class:current-link={isCurrentLink($page.url.pathname, item.route)}
|
||||
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}
|
||||
<a class="nav-link icon" href="/rss.xml"><Rss /></a>
|
||||
<div><ThemeSwitch /></div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="dropdown-toggle"
|
||||
aria-hidden="true"
|
||||
on:click={onDropdownToggle}>
|
||||
<Menu />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<style>
|
||||
#bio-name {
|
||||
font-family: serif;
|
||||
font-size: 3rem;
|
||||
font-weight: normal;
|
||||
color: var(--name);
|
||||
}
|
||||
#navbar {
|
||||
.link-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
|
@ -45,5 +97,14 @@
|
|||
#navbar .nav-link {
|
||||
display: inline-block;
|
||||
}
|
||||
#dropdown-toggle {
|
||||
display: initial;
|
||||
z-index: 5;
|
||||
}
|
||||
.dropdown-opened > .link-container {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,4 +12,4 @@
|
|||
</svelte:head>
|
||||
|
||||
<hr class="separator" />
|
||||
<PostList {posts} pageN=1 {maxPageN}/>
|
||||
<PostList {posts} pageN={1} {maxPageN}/>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { TrackPrevious, TrackNext } from 'svelte-radix';
|
||||
import type { PageData } from './$types';
|
||||
export let data: PageData;
|
||||
const { post, Content } = data;
|
||||
const { post, Content, seriesNeighbours } = data;
|
||||
import siteMetadata from '$content/metadata.json';
|
||||
import PostHeader from '$lib/components/PostHeader.svelte';
|
||||
</script>
|
||||
|
@ -17,3 +18,39 @@
|
|||
<Content />
|
||||
</article>
|
||||
<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>
|
||||
|
|
|
@ -1,16 +1,54 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
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 }) => {
|
||||
try {
|
||||
const post = await import(`$content/post/${params.slug}.md`);
|
||||
if (!post) throw error(404);
|
||||
const metadata: Post = transformPostMeta(post.metadata, params.slug);
|
||||
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 {
|
||||
post: transformPostMeta(post.metadata, params.slug),
|
||||
name: series,
|
||||
prevSlug,
|
||||
prevTitle,
|
||||
nextSlug,
|
||||
nextTitle,
|
||||
};
|
||||
}));
|
||||
|
||||
return {
|
||||
post: metadata,
|
||||
Content,
|
||||
seriesNeighbours,
|
||||
};
|
||||
}
|
||||
catch {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { PageLoad } from './$types';
|
|||
import { getPosts } from '$lib/posts';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const posts = await getPosts(params.slug);
|
||||
const posts = await getPosts({ tag: params.slug });
|
||||
return {
|
||||
name: params.slug,
|
||||
posts,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import fs from 'fs';
|
||||
|
||||
import { mdsvex, escapeSvelte } from 'mdsvex';
|
||||
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 langs = [
|
||||
const metadata = await fs.promises.readFile(contentDir + "/metadata.json")
|
||||
.then((file) => JSON.parse(file.toString()));
|
||||
const langs = metadata?.markdown.highlighterLangs ?? [
|
||||
"javascript",
|
||||
"lean",
|
||||
"lisp",
|
||||
|
@ -26,11 +29,7 @@ const langs = [
|
|||
"c++",
|
||||
"sh",
|
||||
];
|
||||
const highlightingThemes = {
|
||||
light: 'vitesse-light',
|
||||
dark: 'vitesse-dark',
|
||||
};
|
||||
const katexOptions = {
|
||||
const katexOptions = metadata?.markdown.katex ?? {
|
||||
macros: {
|
||||
"\\Nat": "\\mathbb N",
|
||||
"\\Complex": "\\mathbb C",
|
||||
|
@ -39,6 +38,10 @@ const katexOptions = {
|
|||
"\\rv": "\\mathrm",
|
||||
},
|
||||
};
|
||||
const highlightingThemes = {
|
||||
light: 'vitesse-light',
|
||||
dark: 'vitesse-dark',
|
||||
};
|
||||
|
||||
const themes = Object.keys(highlightingThemes).map(function(k){ return highlightingThemes[k] });
|
||||
const highlighter = await getHighlighter({
|
||||
|
|
|
@ -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/");
|
||||
});
|
Loading…
Reference in New Issue