feat: Add project

This commit is contained in:
mattcroat 2023-04-28 15:24:55 +02:00
commit 002f364996
41 changed files with 3489 additions and 0 deletions

13
.eslintignore Normal file
View 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
View 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
View 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

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest

14
.prettierignore Normal file
View 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
View 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
View 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
View 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 Normal file

File diff suppressed because it is too large Load Diff

160
src/app.css Normal file
View 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
View 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
View 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>

View File

@ -0,0 +1,6 @@
<script lang="ts">
export let src: string
export let alt: string
</script>
<img {src} {alt} loading="lazy" />

View File

@ -0,0 +1,3 @@
import img from './img.svelte'
export { img }

5
src/lib/config.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 }
}

View 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">&num;{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>

View 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}`)
}
}

View File

@ -0,0 +1,2 @@
<h1>About</h1>
<p>I like long walks on the beach.</p>

View 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)
}

View File

@ -0,0 +1,2 @@
<h1>Contact</h1>
<p>New phone, who dis?</p>

18
src/routes/footer.svelte Normal file
View File

@ -0,0 +1,18 @@
<script lang="ts">
import * as config from '$lib/config'
</script>
<footer>
<p>{config.title} &copy {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
View 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>

View 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
View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

36
svelte.config.js Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite'
const config = {
plugins: [sveltekit()]
}
export default config