feat: Add project
This commit is contained in:
commit
002f364996
13
.eslintignore
Normal file
13
.eslintignore
Normal file
@ -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
|
31
.eslintrc.cjs
Normal file
31
.eslintrc.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:svelte/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
ignorePatterns: ['*.cjs'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vercel
|
14
.prettierignore
Normal file
14
.prettierignore
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
31
README.md
Normal file
31
README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SvelteKit Markdown Blog
|
||||||
|
|
||||||
|
Learn how to build and extend a blazingly fast SvelteKit Markdown blog for poets.
|
||||||
|
|
||||||
|
## Post
|
||||||
|
|
||||||
|
✍️ https://joyofcode.xyz/sveltekit-markdown-blog
|
||||||
|
|
||||||
|
## Remote Development
|
||||||
|
|
||||||
|
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/joysofcode/sveltekit-markdown-blog)
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
### 🧑🤝🧑 Clone the project
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://github.com/joysofcode/sveltekit-markdown-blog.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦️ Install dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💿️ Run the development server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run dev
|
||||||
|
```
|
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"name": "sveltekit-markdown-blog",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-vercel": "^2.2.0",
|
||||||
|
"@sveltejs/kit": "^1.5.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"mdsvex": "^0.10.6",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
|
"svelte": "^3.54.0",
|
||||||
|
"svelte-check": "^3.0.1",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/jetbrains-mono": "^4.5.12",
|
||||||
|
"@fontsource/manrope": "^4.5.13",
|
||||||
|
"lucide-svelte": "^0.162.0",
|
||||||
|
"open-props": "^1.5.7",
|
||||||
|
"rehype-slug": "^5.1.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
|
"remark-unwrap-images": "^3.0.1",
|
||||||
|
"shiki": "^0.14.1"
|
||||||
|
}
|
||||||
|
}
|
2586
pnpm-lock.yaml
generated
Normal file
2586
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
160
src/app.css
Normal file
160
src/app.css
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
@import '@fontsource/manrope';
|
||||||
|
@import '@fontsource/jetbrains-mono';
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* font */
|
||||||
|
--font-sans: 'Manrope', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
|
||||||
|
/* dark */
|
||||||
|
--brand-dark: var(--orange-3);
|
||||||
|
--text-1-dark: var(--gray-3);
|
||||||
|
--text-2-dark: var(--gray-5);
|
||||||
|
--surface-1-dark: var(--gray-12);
|
||||||
|
--surface-2-dark: var(--gray-11);
|
||||||
|
--surface-3-dark: var(--gray-10);
|
||||||
|
--surface-4-dark: var(--gray-9);
|
||||||
|
--background-dark: var(--gradient-8);
|
||||||
|
--border-dark: var(--gray-9);
|
||||||
|
|
||||||
|
/* light */
|
||||||
|
--brand-light: var(--orange-10);
|
||||||
|
--text-1-light: var(--gray-8);
|
||||||
|
--text-2-light: var(--gray-7);
|
||||||
|
--surface-1-light: var(--gray-0);
|
||||||
|
--surface-2-light: var(--gray-1);
|
||||||
|
--surface-3-light: var(--gray-2);
|
||||||
|
--surface-4-light: var(--gray-3);
|
||||||
|
--background-light: none;
|
||||||
|
--border-light: var(--gray-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--brand: var(--brand-dark);
|
||||||
|
--text-1: var(--text-1-dark);
|
||||||
|
--text-2: var(--text-2-dark);
|
||||||
|
--surface-1: var(--surface-1-dark);
|
||||||
|
--surface-2: var(--surface-2-dark);
|
||||||
|
--surface-3: var(--surface-3-dark);
|
||||||
|
--surface-4: var(--surface-4-dark);
|
||||||
|
--background: var(--background-dark);
|
||||||
|
--border: var(--border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--brand: var(--brand-light);
|
||||||
|
--text-1: var(--text-1-light);
|
||||||
|
--text-2: var(--text-2-light);
|
||||||
|
--surface-1: var(--surface-1-light);
|
||||||
|
--surface-2: var(--surface-2-light);
|
||||||
|
--surface-3: var(--surface-3-light);
|
||||||
|
--surface-4: var(--surface-4-light);
|
||||||
|
--background: var(--background-light);
|
||||||
|
--border: var(--border-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[color-scheme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--brand: var(--brand-dark);
|
||||||
|
--text-1: var(--text-1-dark);
|
||||||
|
--text-2: var(--text-2-dark);
|
||||||
|
--surface-1: var(--surface-1-dark);
|
||||||
|
--surface-2: var(--surface-2-dark);
|
||||||
|
--surface-3: var(--surface-3-dark);
|
||||||
|
--surface-4: var(--surface-4-dark);
|
||||||
|
--background: var(--background-dark);
|
||||||
|
--border: var(--border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
[color-scheme='light'] {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--brand: var(--brand-light);
|
||||||
|
--text-1: var(--text-1-light);
|
||||||
|
--text-2: var(--text-2-light);
|
||||||
|
--surface-1: var(--surface-1-light);
|
||||||
|
--surface-2: var(--surface-2-light);
|
||||||
|
--surface-3: var(--surface-3-light);
|
||||||
|
--surface-4: var(--surface-4-light);
|
||||||
|
--background: var(--background-light);
|
||||||
|
--border: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color: var(--text-1);
|
||||||
|
accent-color: var(--link);
|
||||||
|
background-image: var(--background);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: var(--radius-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-1 {
|
||||||
|
background-color: var(--surface-1);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-2 {
|
||||||
|
background-color: var(--surface-2);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-3 {
|
||||||
|
background-color: var(--surface-3);
|
||||||
|
color: var(--text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-4 {
|
||||||
|
background-color: var(--surface-4);
|
||||||
|
color: var(--text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :is(h2, h3, h4, h5, h6) {
|
||||||
|
margin-top: var(--size-8);
|
||||||
|
margin-bottom: var(--size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p:not(:is(h2, h3, h4, h5, h6) + p) {
|
||||||
|
margin-top: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :is(ul, ol) {
|
||||||
|
list-style-type: '🔥';
|
||||||
|
padding-left: var(--size-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :is(ul, ol) li {
|
||||||
|
margin-block: var(--size-2);
|
||||||
|
padding-inline-start: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
max-inline-size: 100%;
|
||||||
|
padding: var(--size-3);
|
||||||
|
border-radius: 8px;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
21
src/app.html
Normal file
21
src/app.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<link rel="icon" href="https://fav.farm/🔥" />
|
||||||
|
<link rel="alternate" type="application/atom+xml" href="/rss.xml" />
|
||||||
|
<script type="module">
|
||||||
|
const theme = localStorage.getItem('color-scheme')
|
||||||
|
|
||||||
|
theme
|
||||||
|
? document.documentElement.setAttribute('color-scheme', theme)
|
||||||
|
: localStorage.setItem('color-scheme', 'dark')
|
||||||
|
</script>
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
src/lib/components/custom/img.svelte
Normal file
6
src/lib/components/custom/img.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let src: string
|
||||||
|
export let alt: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img {src} {alt} loading="lazy" />
|
3
src/lib/components/custom/index.ts
Normal file
3
src/lib/components/custom/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import img from './img.svelte'
|
||||||
|
|
||||||
|
export { img }
|
5
src/lib/config.ts
Normal file
5
src/lib/config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { dev } from '$app/environment'
|
||||||
|
|
||||||
|
export const title = 'Shakespeare'
|
||||||
|
export const description = 'SvelteKit blog for poets'
|
||||||
|
export const url = dev ? 'http://localhost:5173/' : 'https://shakespeare.pages.dev'
|
23
src/lib/theme.ts
Normal file
23
src/lib/theme.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import { browser } from '$app/environment'
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
const userTheme = browser && localStorage.getItem('color-scheme')
|
||||||
|
|
||||||
|
export const theme = writable(userTheme ?? 'dark')
|
||||||
|
|
||||||
|
export function toggleTheme() {
|
||||||
|
theme.update((currentTheme) => {
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('color-scheme', newTheme)
|
||||||
|
localStorage.setItem('color-scheme', newTheme)
|
||||||
|
|
||||||
|
return newTheme
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(newTheme: Theme) {
|
||||||
|
theme.set(newTheme)
|
||||||
|
}
|
10
src/lib/types.ts
Normal file
10
src/lib/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type Categories = 'sveltekit' | 'svelte'
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
description: string
|
||||||
|
date: string
|
||||||
|
categories: Categories[]
|
||||||
|
published: boolean
|
||||||
|
}
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
type DateStyle = Intl.DateTimeFormatOptions['dateStyle']
|
||||||
|
|
||||||
|
export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') {
|
||||||
|
const formatter = new Intl.DateTimeFormat(locales, { dateStyle })
|
||||||
|
return formatter.format(new Date(date))
|
||||||
|
}
|
6
src/mdsvex.svelte
Normal file
6
src/mdsvex.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
import { img } from '$lib/components/custom'
|
||||||
|
export { img }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
9
src/posts/counter.svelte
Normal file
9
src/posts/counter.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
const increment = () => (count += 1)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={increment}>
|
||||||
|
{count}
|
||||||
|
</button>
|
19
src/posts/first-post.md
Normal file
19
src/posts/first-post.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: First post
|
||||||
|
description: First post.
|
||||||
|
date: '2023-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
Hey friends! 👋
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function greet(name: string) {
|
||||||
|
console.log(`Hey ${name}! 👋`)
|
||||||
|
}
|
||||||
|
```
|
23
src/posts/second-post.md
Normal file
23
src/posts/second-post.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: Second
|
||||||
|
description: Second post.
|
||||||
|
date: '2023-4-16'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Counter from './counter.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## Svelte
|
||||||
|
|
||||||
|
Media inside the **static** folder is served from `/`.
|
||||||
|
|
||||||
|
![Svelte](favicon.png)
|
||||||
|
|
||||||
|
## Counter
|
||||||
|
|
||||||
|
<Counter />
|
15
src/routes/+error.svelte
Normal file
15
src/routes/+error.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="error">
|
||||||
|
<h1>{$page.status}: {$page.error?.message}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
45
src/routes/+layout.svelte
Normal file
45
src/routes/+layout.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Footer from './footer.svelte'
|
||||||
|
import Header from './header.svelte'
|
||||||
|
import PageTransition from './transition.svelte'
|
||||||
|
|
||||||
|
import 'open-props/style'
|
||||||
|
import 'open-props/normalize'
|
||||||
|
import 'open-props/buttons'
|
||||||
|
import '../app.css'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<PageTransition url={data.url}>
|
||||||
|
<slot />
|
||||||
|
</PageTransition>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.layout {
|
||||||
|
height: 100%;
|
||||||
|
max-inline-size: 1440px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-block: var(--size-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
.layout {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
7
src/routes/+layout.ts
Normal file
7
src/routes/+layout.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const prerender = true
|
||||||
|
|
||||||
|
export async function load({ url }) {
|
||||||
|
return {
|
||||||
|
url: url.pathname
|
||||||
|
}
|
||||||
|
}
|
51
src/routes/+page.svelte
Normal file
51
src/routes/+page.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { formatDate } from '$lib/utils'
|
||||||
|
import * as config from '$lib/config'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{config.title}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<ul class="posts">
|
||||||
|
{#each data.posts as post}
|
||||||
|
<li class="post">
|
||||||
|
<a href={post.slug} class="title">{post.title}</a>
|
||||||
|
<p class="date">{formatDate(post.date)}</p>
|
||||||
|
<p class="description">{post.description}</p>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.posts {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
max-inline-size: var(--size-content-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-fluid-3);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: var(--size-3);
|
||||||
|
}
|
||||||
|
</style>
|
7
src/routes/+page.ts
Normal file
7
src/routes/+page.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { Post } from '$lib/types'
|
||||||
|
|
||||||
|
export async function load({ fetch }) {
|
||||||
|
const response = await fetch('api/posts')
|
||||||
|
const posts: Post[] = await response.json()
|
||||||
|
return { posts }
|
||||||
|
}
|
55
src/routes/[slug]/+page.svelte
Normal file
55
src/routes/[slug]/+page.svelte
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { formatDate } from '$lib/utils'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.meta.title}</title>
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:title" content={data.meta.title} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<hgroup>
|
||||||
|
<h1>{data.meta.title}</h1>
|
||||||
|
<p>Published at {formatDate(data.meta.date)}</p>
|
||||||
|
</hgroup>
|
||||||
|
|
||||||
|
<div class="tags">
|
||||||
|
{#each data.meta.categories as category}
|
||||||
|
<span class="surface-4">#{category}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prose">
|
||||||
|
<svelte:component this={data.content} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
max-inline-size: var(--size-content-3);
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 + p {
|
||||||
|
margin-top: var(--size-2);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-3);
|
||||||
|
margin-top: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags > * {
|
||||||
|
padding: var(--size-2) var(--size-3);
|
||||||
|
border-radius: var(--radius-round);
|
||||||
|
}
|
||||||
|
</style>
|
14
src/routes/[slug]/+page.ts
Normal file
14
src/routes/[slug]/+page.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { error } from '@sveltejs/kit'
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
try {
|
||||||
|
const post = await import(`../../posts/${params.slug}.md`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: post.default,
|
||||||
|
meta: post.metadata
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw error(404, `Could not find ${params.slug}`)
|
||||||
|
}
|
||||||
|
}
|
2
src/routes/about/+page.svelte
Normal file
2
src/routes/about/+page.svelte
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<h1>About</h1>
|
||||||
|
<p>I like long walks on the beach.</p>
|
30
src/routes/api/posts/+server.ts
Normal file
30
src/routes/api/posts/+server.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { json } from '@sveltejs/kit'
|
||||||
|
import type { Post } from '$lib/types'
|
||||||
|
|
||||||
|
async function getPosts() {
|
||||||
|
let posts: Post[] = []
|
||||||
|
|
||||||
|
const paths = import.meta.glob('/src/posts/*.md', { eager: true })
|
||||||
|
|
||||||
|
for (const path in paths) {
|
||||||
|
const file = paths[path]
|
||||||
|
const slug = path.split('/').at(-1)?.replace('.md', '')
|
||||||
|
|
||||||
|
if (file && typeof file === 'object' && 'metadata' in file && slug) {
|
||||||
|
const metadata = file.metadata as Omit<Post, 'slug'>
|
||||||
|
const post = { ...metadata, slug } satisfies Post
|
||||||
|
post.published && posts.push(post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts = posts.sort(
|
||||||
|
(first, second) => new Date(second.date).getTime() - new Date(first.date).getTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
return posts
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const posts = await getPosts()
|
||||||
|
return json(posts)
|
||||||
|
}
|
2
src/routes/contact/+page.svelte
Normal file
2
src/routes/contact/+page.svelte
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<h1>Contact</h1>
|
||||||
|
<p>New phone, who dis?</p>
|
18
src/routes/footer.svelte
Normal file
18
src/routes/footer.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as config from '$lib/config'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>{config.title} © {new Date().getFullYear()}</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
padding-block: var(--size-7);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
</style>
|
52
src/routes/header.svelte
Normal file
52
src/routes/header.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Toggle from './toggle.svelte'
|
||||||
|
import * as config from '$lib/config'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="title">
|
||||||
|
<b>{config.title}</b>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="links">
|
||||||
|
<li>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/rss.xml" target="_blank">RSS</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Toggle />
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav {
|
||||||
|
padding-block: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
margin-block: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-7);
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
37
src/routes/rss.xml/+server.ts
Normal file
37
src/routes/rss.xml/+server.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import * as config from '$lib/config'
|
||||||
|
import type { Post } from '$lib/types'
|
||||||
|
|
||||||
|
export const prerender = true
|
||||||
|
|
||||||
|
export async function GET({ fetch }) {
|
||||||
|
const response = await fetch('api/posts')
|
||||||
|
const posts: Post[] = await response.json()
|
||||||
|
|
||||||
|
const headers = { 'Content-Type': 'application/xml' }
|
||||||
|
|
||||||
|
const xml = `
|
||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>${config.title}</title>
|
||||||
|
<description>${config.description}</description>
|
||||||
|
<link>${config.url}</link>
|
||||||
|
<atom:link href="${config.url}/rss.xml" rel="self" type="application/rss+xml"/>
|
||||||
|
${posts
|
||||||
|
.map(
|
||||||
|
(post) => `
|
||||||
|
<item>
|
||||||
|
<title>${post.title}</title>
|
||||||
|
<description>${post.description}</description>
|
||||||
|
<link>${config.url}/${post.slug}</link>
|
||||||
|
<guid isPermaLink="true">${config.url}/${post.slug}</guid>
|
||||||
|
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||||
|
</item>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`.trim()
|
||||||
|
|
||||||
|
return new Response(xml, { headers })
|
||||||
|
}
|
35
src/routes/toggle.svelte
Normal file
35
src/routes/toggle.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { Moon, Sun } from 'lucide-svelte'
|
||||||
|
import { theme, toggleTheme } from '$lib/theme'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={toggleTheme} aria-label="Toggle theme">
|
||||||
|
{#if $theme === 'dark'}
|
||||||
|
<div in:fly={{ y: 10 }}>
|
||||||
|
<Sun />
|
||||||
|
<span>Light</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div in:fly={{ y: -10 }}>
|
||||||
|
<Moon />
|
||||||
|
<span>Dark</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: inherit;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button > * {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
</style>
|
17
src/routes/transition.svelte
Normal file
17
src/routes/transition.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade } from 'svelte/transition'
|
||||||
|
|
||||||
|
export let url: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key url}
|
||||||
|
<div class="transition" in:fade>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.transition {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
36
svelte.config.js
Normal file
36
svelte.config.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-vercel'
|
||||||
|
import { vitePreprocess } from '@sveltejs/kit/vite'
|
||||||
|
|
||||||
|
import { mdsvex, escapeSvelte } from 'mdsvex'
|
||||||
|
import shiki from 'shiki'
|
||||||
|
import remarkUnwrapImages from 'remark-unwrap-images'
|
||||||
|
import remarkToc from 'remark-toc'
|
||||||
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
|
||||||
|
/** @type {import('mdsvex').MdsvexOptions} */
|
||||||
|
const mdsvexOptions = {
|
||||||
|
extensions: ['.md'],
|
||||||
|
layout: {
|
||||||
|
_: './src/mdsvex.svelte'
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
highlighter: async (code, lang = 'text') => {
|
||||||
|
const highlighter = await shiki.getHighlighter({ theme: 'poimandres' })
|
||||||
|
const html = escapeSvelte(highlighter.codeToHtml(code, { lang }))
|
||||||
|
return `{@html \`${html}\` }`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remarkPlugins: [remarkUnwrapImages, [remarkToc, { tight: true }]],
|
||||||
|
rehypePlugins: [rehypeSlug]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
extensions: ['.svelte', '.md'],
|
||||||
|
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
|
||||||
|
kit: {
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -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
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
Loading…
Reference in New Issue
Block a user