Compare commits
3 Commits
924706b3b2
...
post-siden
Author | SHA1 | Date | |
---|---|---|---|
b4a1097845 | |||
adc582116b | |||
7d5c696fa7 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,9 +3,3 @@ node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
**/_test.*
|
14
README.md
14
README.md
@ -1,6 +1,6 @@
|
||||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
|
||||
|
||||
## Creating a project
|
||||
|
||||
@ -8,12 +8,14 @@ If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
npm init svelte@next
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
npm init svelte@next my-app
|
||||
```
|
||||
|
||||
> Note: the `@next` is temporary
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
@ -27,12 +29,10 @@ npm run dev -- --open
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
|
||||
|
11
jsconfig.json
Normal file
11
jsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
6564
package-lock.json
generated
6564
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -1,23 +1,18 @@
|
||||
{
|
||||
"name": "blog.jfmonty2.com",
|
||||
"name": "blog",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"preview": "svelte-kit preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"hast-util-to-text": "^4.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdsvex": "^0.11.0",
|
||||
"svelte": "^4.0.5",
|
||||
"unist-util-find": "^3.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.21",
|
||||
"@sveltejs/kit": "next",
|
||||
"mdsvex": "^0.9.8",
|
||||
"node-sass": "^6.0.1",
|
||||
"svelte": "^3.42.6",
|
||||
"svelte-preprocess": "^4.9.8"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
10
src/app.html
10
src/app.html
@ -4,12 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
|
||||
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="alternate" type="application/atom+xml" href="/feed">
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -34,8 +34,9 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<span class="drop-cap">{initial}</span>
|
||||
{#if remainder.length}
|
||||
<p>
|
||||
<span class="drop-cap">{initial}</span>
|
||||
<span class="first-word" style:--shift={shift}>{remainder}</span>
|
||||
{/if}
|
||||
<slot></slot>
|
||||
</p>
|
||||
|
||||
|
@ -1,67 +0,0 @@
|
||||
<script>
|
||||
export let level;
|
||||
export let id = '';
|
||||
|
||||
const tag = `h${level}`;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
a {
|
||||
/* Works better to set the size here for line-height reasons */
|
||||
font-size: 0.9em;
|
||||
/* color: hsl(0, 0%, 25%); */
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-bottom: 0.05em solid currentcolor;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
/* tiny tweak for optical alignment */
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.before {
|
||||
display: none;
|
||||
padding-right: 0.25em;
|
||||
margin-left: -1.25em;
|
||||
}
|
||||
|
||||
@media(min-width: 58rem) {
|
||||
.before {
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.h:hover .before, .before:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:element this={tag} {id} class="h">
|
||||
<span class="before">
|
||||
<a href="#{id}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg></a></span><span> <!-- Looks ugly but necessary to get rid of spurious whitespace -->
|
||||
<slot></slot>
|
||||
</span>
|
||||
<!-- Icon from https://heroicons.com/ -->
|
||||
<a href="#{id}" class="after">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
</a>
|
||||
</svelte:element>
|
@ -18,7 +18,7 @@
|
||||
|
||||
|
||||
{#if href.startsWith('/') || host(href) === $page.host}
|
||||
<a data-sveltekit-preload-data="hover" {href}>
|
||||
<a sveltekit:prefetch {href}>
|
||||
<slot></slot>
|
||||
</a>
|
||||
{:else}
|
||||
|
@ -1,142 +1,33 @@
|
||||
<script context="module">
|
||||
import { onMount } from 'svelte';
|
||||
import { formatDate } from './datefmt.js';
|
||||
import { makeSlug } from '$lib/utils.js';
|
||||
import { makeSlug } from '$lib/slug.js';
|
||||
|
||||
import Toc from './Toc.svelte';
|
||||
import Link from './Link.svelte';
|
||||
export { Link as a };
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let title, date;
|
||||
export let description = '';
|
||||
export const description = '';
|
||||
export const draft = false;
|
||||
export let toc = null;
|
||||
|
||||
export let slug;
|
||||
export let prev = null;
|
||||
export let next = null;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
|
||||
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.left-gutter {
|
||||
grid-column: 1 / 2;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.post {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-column: 2 / 3;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
hr {
|
||||
grid-column: 2 / 3;
|
||||
width: 100%;
|
||||
border-top: 1px solid hsl(0 0% 75%);
|
||||
border-bottom: none;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45em;
|
||||
|
||||
font-size: 1rem;
|
||||
color: var(--content-color-faded);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.25em;
|
||||
text-decoration-color: transparent;
|
||||
|
||||
transition: 150ms;
|
||||
}
|
||||
.footer a:hover {
|
||||
text-decoration-color: currentColor;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer svg {
|
||||
width: 1em;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
a.prev:hover svg {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
a.next:hover svg {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title} | Joe's Blog</title>
|
||||
<meta property="og:title" content="{title} | Joe's Blog">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://blog.jfmonty2.com/{slug}">
|
||||
<meta property="og:description" content={description}>
|
||||
<meta property="og:site_name" content="Joe's Blog">
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/prism-dracula.css" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<div class="title">
|
||||
<div id="post">
|
||||
<h1 id="{makeSlug(title)}">{title}</h1>
|
||||
<p class="subtitle">{formatDate(date)}</p>
|
||||
</div>
|
||||
|
||||
<div class="left-gutter">
|
||||
{#if toc?.length !== 0}
|
||||
<Toc items={toc} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="post">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="footer">
|
||||
{#if prev}
|
||||
<a href="/{prev}" class="prev" data-sveltekit-preload-data="hover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
Previous
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if next}
|
||||
<!-- we use margin-left rather than justify-content so it works regardless of whether the "previous" link exists -->
|
||||
<a href="/{next}" class="next" style="margin-left: auto;" data-sveltekit-preload-data="hover">
|
||||
Next
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<style>
|
||||
<style lang="scss">
|
||||
/* always applicable */
|
||||
:global(body) {
|
||||
counter-reset: sidenote;
|
||||
@ -8,22 +8,21 @@
|
||||
counter-increment: sidenote;
|
||||
color: #444;
|
||||
margin-left: 0.05rem;
|
||||
}
|
||||
|
||||
.counter:after {
|
||||
&:after {
|
||||
font-size: 0.75em;
|
||||
position: relative;
|
||||
bottom: 0.3rem;
|
||||
color: #8c0606;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenote {
|
||||
color: #555;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.sidenote:before {
|
||||
content: var(--sidenote-index, counter(sidenote)) " ";
|
||||
&:before {
|
||||
content: counter(sidenote) " ";
|
||||
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
|
||||
(because the sidenote is floated it counts as a positioned parent, I think) */
|
||||
position: absolute;
|
||||
@ -33,6 +32,7 @@
|
||||
font-size: 0.75rem;
|
||||
color: #8c0606;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenote-toggle {
|
||||
display: none;
|
||||
@ -46,28 +46,16 @@
|
||||
|
||||
.sidenote {
|
||||
--gap: 2rem;
|
||||
--sidenote-width: min(16rem, calc(50vw - var(--gap) - 1rem - var(--content-width) / 2));
|
||||
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
|
||||
width: var(--sidenote-width);
|
||||
hyphens: auto;
|
||||
position: relative;
|
||||
float: right;
|
||||
clear: right;
|
||||
margin-right: calc(0rem - var(--sidenote-width) - var(--gap)); /* gives us 2rem of space between content and sidenote */
|
||||
margin-right: calc(0rem - var(--sidenote-width) - var(--gap)); // gives us 2rem of space between content and sidenote
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
/* fade-in animation */
|
||||
.sidenote {
|
||||
opacity: 0;
|
||||
animation: fade-in 600ms ease-out;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from {opacity: 0;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
|
||||
.nested.sidenote {
|
||||
margin-right: 0;
|
||||
margin-top: 0.7rem;
|
||||
@ -119,13 +107,26 @@
|
||||
font-size: 1.25rem;
|
||||
color: #8c0606;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dismiss:hover {
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// /* slight tweaks for in between state */
|
||||
// @media (min-width: 52.5em) and (max-width: 70em) {
|
||||
// .sidenote {
|
||||
// padding-left: calc(50vw - 19rem);
|
||||
// }
|
||||
// }
|
||||
// @media (max-width: 52.5em) {
|
||||
// .sidenote {
|
||||
// padding-left: 2rem;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
|
||||
<script context="module">
|
||||
|
@ -1,149 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { makeSlug } from '$lib/utils.js';
|
||||
|
||||
export let items;
|
||||
|
||||
items.forEach(i => i.slug = makeSlug(i.text));
|
||||
|
||||
let headings = [];
|
||||
let currentHeadingSlug = null;
|
||||
let currentSubheadingSlug = null;
|
||||
|
||||
function setCurrentHeading() {
|
||||
for (const h of headings) {
|
||||
const yPos = h.getBoundingClientRect().y;
|
||||
if (yPos > (window.innerHeight / 3)) {
|
||||
break;
|
||||
}
|
||||
if (h.tagName === 'H2') {
|
||||
currentHeadingSlug = h.id;
|
||||
currentSubheadingSlug = null;
|
||||
}
|
||||
if (h.tagName === 'H3') {
|
||||
currentSubheadingSlug = h.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount (() => {
|
||||
// These shouldn't change over the life of the page, so we can cache them
|
||||
headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
|
||||
setCurrentHeading();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={setCurrentHeading} />
|
||||
|
||||
<style>
|
||||
#toc {
|
||||
display: none;
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 2rem;
|
||||
|
||||
max-width: 14rem;
|
||||
color: var(--content-color-faded);
|
||||
opacity: 0;
|
||||
animation: fade-in 600ms ease-out;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@media(min-width: 1300px) {
|
||||
#toc { display: block }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {opacity: 0}
|
||||
to {opacity: 1}
|
||||
}
|
||||
|
||||
/* margin-left is to match the padding on the top-level list items,
|
||||
but here it needs to be margin so that the border is also shifted */
|
||||
h5 {
|
||||
font-variant: petite-caps;
|
||||
font-weight: 500;
|
||||
max-width: fit-content;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid currentcolor;
|
||||
/* make the border stretch beyond the text just a bit, because I like the effect */
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
li.depth-2 {
|
||||
align-items: stretch;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
li.depth-3 {
|
||||
align-items: center;
|
||||
margin-bottom: 0.05rem;
|
||||
}
|
||||
|
||||
.marker {
|
||||
position: absolute;
|
||||
left: -0.6rem;
|
||||
}
|
||||
.bar {
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.dot {
|
||||
width: 0.15rem;
|
||||
height: 0.15rem;
|
||||
border-radius: 50%;
|
||||
/* vertically center within its containing block */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
li.current, li:hover {
|
||||
color: var(--content-color);
|
||||
}
|
||||
.current .marker, li:hover .marker {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div id="toc">
|
||||
<h5>
|
||||
<span class="heading">Contents</span>
|
||||
</h5>
|
||||
<ul>
|
||||
{#each items as item}
|
||||
{#if item.depth === 2}
|
||||
<li class="depth-2" class:current={item.slug === currentHeadingSlug} style:align-items="stretch">
|
||||
<span class="marker bar"></span>
|
||||
<a href="#{item.slug}">{item.text}</a>
|
||||
</li>
|
||||
{:else if item.depth === 3}
|
||||
<li class="depth-3" class:current={item.slug === currentSubheadingSlug} style:align-items="center" style:margin-left="0.75em">
|
||||
<span class="marker dot"></span>
|
||||
<a href="#{item.slug}">{item.text}</a>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
20
src/lib/projects/sidenotes/Frame.svelte
Normal file
20
src/lib/projects/sidenotes/Frame.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import Step from './Step.svelte';
|
||||
import {onMount} from 'svelte';
|
||||
|
||||
let frame;
|
||||
onMount(() => {
|
||||
frame.setAttribute('srcdoc', frame.innerHTML);
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<iframe bind:this={frame}>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<Step />
|
||||
<p>Goodbye world!</p>
|
||||
</body>
|
||||
</html>
|
||||
</iframe>
|
7
src/lib/projects/sidenotes/Step.svelte
Normal file
7
src/lib/projects/sidenotes/Step.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let count = 0;
|
||||
</script>
|
||||
|
||||
<p>hello world!</p>
|
||||
<button on:click={() => count++}>Increment</button>
|
||||
<p>The count is: {count}</p>
|
50
src/lib/slug.js
Normal file
50
src/lib/slug.js
Normal file
@ -0,0 +1,50 @@
|
||||
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
|
||||
const space = /\s/g
|
||||
|
||||
export function makeSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(space, '-')
|
||||
.replace(nonAlphaNum, '')
|
||||
}
|
||||
|
||||
function apply(node, types, fn) {
|
||||
if (typeof types === 'string') {
|
||||
types = new Set([types]);
|
||||
}
|
||||
else if (!(types instanceof Set)) {
|
||||
types = new Set(types)
|
||||
console.log(types)
|
||||
}
|
||||
|
||||
if (types.has(node.type)) {
|
||||
fn(node);
|
||||
}
|
||||
if ('children' in node) {
|
||||
for (let child of node.children) {
|
||||
apply(child, types, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTextContent(node) {
|
||||
let segments = [];
|
||||
apply(node, 'text', textNode => {
|
||||
// skip all-whitespace strings
|
||||
if (textNode.value.match(/^\s+$/)) return;
|
||||
segments.push(textNode.value.trim());
|
||||
});
|
||||
|
||||
return segments.join(' ');
|
||||
}
|
||||
|
||||
export default function slug() {
|
||||
return (tree) => {
|
||||
apply(tree, 'element', e => {
|
||||
if (e.tagName.match(/h[1-6]/)) {
|
||||
let text = getTextContent(e);
|
||||
e.properties.id = makeSlug(text);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
|
||||
const space = /\s+/g;
|
||||
export function makeSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(space, '-')
|
||||
.replace(nonAlphaNum, '');
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
// const Node = {
|
||||
// addChild(child) {
|
||||
// this.children.push(child);
|
||||
// return child;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
export function tag(name, attrs, children) {
|
||||
return {
|
||||
type: 'tag',
|
||||
tag: name,
|
||||
attrs: attrs || {},
|
||||
children: children || [],
|
||||
|
||||
addTag(name, attrs, children) {
|
||||
const child = tag(name, attrs, children);
|
||||
this.children.push(child);
|
||||
return child;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function text(content) {
|
||||
return {
|
||||
type: 'text',
|
||||
text: content,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function serialize(node, depth) {
|
||||
if (!depth) {
|
||||
depth = 0;
|
||||
}
|
||||
|
||||
const indent = ' '.repeat(depth * 4);
|
||||
let fragments = [];
|
||||
|
||||
// version tag, if this is the top level
|
||||
if (depth === 0) {
|
||||
fragments.push('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
}
|
||||
|
||||
fragments.push(`${indent}<${node.tag}`);
|
||||
|
||||
// this happens if there are multiple text nodes within the same parent
|
||||
if (node.type === 'text') {
|
||||
return `${indent}${escape(node.text)}`;
|
||||
}
|
||||
|
||||
if (node.children === undefined) {
|
||||
console.log(node);
|
||||
}
|
||||
|
||||
// opening tag <element attr="value">
|
||||
for (const attr in node.attrs) {
|
||||
fragments.push(` ${attr}="${node.attrs[attr]}"`);
|
||||
}
|
||||
if (node.children.length === 0) {
|
||||
fragments.push(' />');
|
||||
return fragments.join('');
|
||||
}
|
||||
fragments.push('>');
|
||||
|
||||
// if the only child is a single text node, skip recursion and just dump contents directly
|
||||
if (node.children.length === 1 && node.children[0].type === 'text') {
|
||||
const text = escape(node.children[0].text);
|
||||
fragments.push(text);
|
||||
}
|
||||
// otherwise, start a new line for each child node, then recurse
|
||||
else {
|
||||
for (const child of node.children) {
|
||||
fragments.push('\n');
|
||||
fragments.push(serialize(child, depth + 1));
|
||||
}
|
||||
// no need to verify that there were children, we already did that
|
||||
fragments.push(`\n${indent}`);
|
||||
}
|
||||
|
||||
fragments.push(`</${node.tag}>`);
|
||||
|
||||
return fragments.join('');
|
||||
}
|
||||
|
||||
|
||||
function escape(text) {
|
||||
// we aren't going to bother with escaping attributes, so we won't worry about quotes
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
|
||||
import { find } from 'unist-util-find';
|
||||
import { toText } from 'hast-util-to-text';
|
||||
import { makeSlug } from '../lib/utils.js';
|
||||
|
||||
|
||||
export function localRehype() {
|
||||
let printed = false;
|
||||
|
||||
return (tree, vfile) => {
|
||||
const needsDropcap = vfile.data.fm.dropcap !== false
|
||||
let dropcapAdded = false;
|
||||
|
||||
let moduleScript;
|
||||
let imports = new Set();
|
||||
if (needsDropcap) {
|
||||
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
|
||||
}
|
||||
|
||||
visit(tree, node => {
|
||||
// add slugs to headings
|
||||
if (isHeading(node)) {
|
||||
processHeading(node);
|
||||
imports.add("import Heading from '$lib/Heading.svelte';");
|
||||
return SKIP;
|
||||
}
|
||||
|
||||
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
|
||||
if (isModuleScript(node)) {
|
||||
moduleScript = node;
|
||||
}
|
||||
|
||||
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
|
||||
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
|
||||
addDropcap(node);
|
||||
dropcapAdded = true;
|
||||
return SKIP;
|
||||
}
|
||||
});
|
||||
|
||||
// insert our imports at the top of the `<script context="module">` tag
|
||||
if (imports.size > 0) {
|
||||
const script = moduleScript.value;
|
||||
// split the script where the opening tag ends
|
||||
const i = script.indexOf('>');
|
||||
const openingTag = script.slice(0, i + 1);
|
||||
const remainder = script.slice(i + 1);
|
||||
|
||||
// mdvsex uses tabs so we will as well
|
||||
const importScript = Array.from(imports).join('\n\t');
|
||||
|
||||
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function processHeading(node) {
|
||||
const level = node.tagName.slice(1);
|
||||
node.tagName = 'Heading';
|
||||
node.properties.level = level;
|
||||
node.properties.id = makeSlug(toText(node));
|
||||
}
|
||||
|
||||
|
||||
function addDropcap(par) {
|
||||
let txtNode = find(par, {type: 'text'});
|
||||
const i = txtNode.value.search(/\s/);
|
||||
const firstWord = txtNode.value.slice(0, i);
|
||||
const remainder = txtNode.value.slice(i);
|
||||
|
||||
par.children.unshift({
|
||||
type: 'raw',
|
||||
value: `<Dropcap word="${firstWord}" />`,
|
||||
});
|
||||
txtNode.value = remainder;
|
||||
}
|
||||
|
||||
|
||||
function isHeading(node) {
|
||||
return node.type === 'element' && node.tagName.match(/h[1-6]/);
|
||||
}
|
||||
|
||||
function isModuleScript(node) {
|
||||
return node.type === 'raw' && node.value.match(/^<script context="module">/);
|
||||
}
|
||||
|
||||
function isParagraph(node) {
|
||||
return node.type === 'element' && node.tagName === 'p';
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
// build table of contents and inject into frontmatter
|
||||
export function localRemark() {
|
||||
return (tree, vfile) => {
|
||||
let toc = [];
|
||||
let description = null;
|
||||
|
||||
visit(tree, ['heading', 'paragraph'], node => {
|
||||
// build table of contents and inject into frontmatter
|
||||
if (node.type === 'heading') {
|
||||
toc.push({
|
||||
text: toString(node),
|
||||
depth: node.depth,
|
||||
});
|
||||
}
|
||||
|
||||
// inject description (first 25 words of the first paragraph)
|
||||
if (node.type === 'paragraph' && description === null) {
|
||||
description = summarize(node);
|
||||
}
|
||||
});
|
||||
|
||||
vfile.data.fm.toc = toc;
|
||||
vfile.data.fm.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// convert paragraph to single string after stripping everything between html tags
|
||||
function summarize(par) {
|
||||
let newChildren = [];
|
||||
let push = true;
|
||||
for (const child of par.children) {
|
||||
if (child.type === 'html') {
|
||||
push = !push;
|
||||
continue;
|
||||
}
|
||||
if (push) {
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return toString({type: 'paragraph', children: newChildren});
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,37 +0,0 @@
|
||||
<style>
|
||||
.header {
|
||||
background-color: #4f5f68;
|
||||
}
|
||||
|
||||
nav {
|
||||
max-width: 30rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: white;
|
||||
width: 8rem;
|
||||
min-width: 6rem;
|
||||
font-size: 1.5rem;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
nav a:hover {
|
||||
background-color: #00000025;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="header">
|
||||
<nav>
|
||||
<a data-sveltekit-preload-data="hover" href="/">Home</a>
|
||||
<a data-sveltekit-preload-data="hover" href="/posts">Posts</a>
|
||||
<a data-sveltekit-preload-data="hover" href="/about">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
@ -1,8 +0,0 @@
|
||||
export async function load({ data }) {
|
||||
let post = await import(`./_posts/${data.slug}.svx`);
|
||||
post.metadata.slug = data.slug;
|
||||
post.metadata.next = data.next;
|
||||
return {
|
||||
post: post.default,
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { postData, siblingPosts } from './_posts/all.js';
|
||||
|
||||
|
||||
// this is in a "servserside" loader so that we don't end up embedding the metadata
|
||||
// for every post into the final page
|
||||
export function load() {
|
||||
return {
|
||||
slug: postData[0].slug,
|
||||
next: postData[1].slug,
|
||||
};
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<script>
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:component this={data.post} />
|
@ -1,14 +0,0 @@
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
h1, p {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<h1>404</h1>
|
||||
|
||||
<p>That page doesn't exist. Sorry!</p>
|
24
src/routes/[slug].svelte
Normal file
24
src/routes/[slug].svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script context="module">
|
||||
export async function load({ url, params }) {
|
||||
try {
|
||||
let post = await import(`./_posts/${params.slug}.svx`);
|
||||
return {
|
||||
props: {
|
||||
post: post.default
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
error: `Not found: ${url.pathname}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<svelte:component this={post} />
|
@ -1,24 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
|
||||
export async function load({ url, params, data }) {
|
||||
let post;
|
||||
try {
|
||||
post = await import(`../_posts/${params.slug}.svx`);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.message.match(/Unknown variable dynamic import/)) {
|
||||
throw error(404, `Not found: ${url.pathname}`);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
post.metadata.slug = params.slug;
|
||||
post.metadata.prev = data.prev;
|
||||
post.metadata.next = data.next;
|
||||
return {
|
||||
post: post.default,
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { postData } from '../_posts/all.js';
|
||||
|
||||
|
||||
export function load({ params }) {
|
||||
const i = postData.findIndex(p => p.slug === params.slug);
|
||||
return {
|
||||
prev: i > 0 ? postData[i - 1].slug : null,
|
||||
next: i < postData.length - 1 ? postData[i + 1].slug : null,
|
||||
};
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<script>
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:component this={data.post} />
|
45
src/routes/__layout.svelte
Normal file
45
src/routes/__layout.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<style>
|
||||
:global(main) {
|
||||
--content-width: 42rem;
|
||||
box-sizing: border-box;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: #4f5f68;
|
||||
}
|
||||
|
||||
#nav-main {
|
||||
max-width: 30rem;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(6rem, 8rem));
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#nav-main a {
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
#nav-main a:hover {
|
||||
background-color: #00000025;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="header">
|
||||
<nav id="nav-main">
|
||||
<a sveltekit:prefetch href="/">Home</a>
|
||||
<a sveltekit:prefetch href="/posts">Posts</a>
|
||||
<a sveltekit:prefetch href="/">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
@ -31,7 +31,7 @@ docker network create \\
|
||||
-o parent=eth0 \\
|
||||
lan
|
||||
|
||||
docker run --network lan --ip 192.168.50.24 some/image:tag
|
||||
docker run --network lan --ip 192.168.50.24 some/image:version
|
||||
```
|
||||
|
||||
That's it! You're done, congratulations. (Obviously `--subnet`, `--gateway`, and `--parent` should be fed values appropriate to your network.)
|
||||
|
@ -3,7 +3,6 @@ title: Imagining A Passwordless Future
|
||||
description: Can we replace passwords with something more user-friendly?
|
||||
date: 2021-04-30
|
||||
draft: true
|
||||
dropcap: false
|
||||
---
|
||||
<script>
|
||||
import Sidenote from '$lib/Sidenote.svelte';
|
||||
|
@ -1,47 +0,0 @@
|
||||
---
|
||||
title: The Kubernetes Alternative I Wish Existed
|
||||
date: 2023-10-01
|
||||
draft: true
|
||||
---
|
||||
|
||||
<script>
|
||||
import Sidenote from '$lib/Sidenote.svelte';
|
||||
</script>
|
||||
|
||||
I use Kubernetes on my personal server, largely because I wanted to get some experience working with it. It's certainly been helpful in that regard, but after a year and a half or so I think I can pretty confidently say that it's not the ideal tool for my use-case. Duh, I guess? But I think it's worth talking about _why_ that's the case, and what exactly _would_ be the ieal tool.
|
||||
|
||||
## The Kubernetes Way™
|
||||
|
||||
Kubernetes is a very intrusive orchestration system. It would very much like the apps you're running to be doing things _its_ way, and although that's not a _hard_ requirement it tends to make everything subtly more difficult when that isn't the case. In particular, Kubernetes is targeting the situation where you:
|
||||
|
||||
* Have a broad variety of applications that you want to support,
|
||||
* Have written all or most of those applications yourself,<Sidenote>The "you" here is organizational, not personal.</Sidenote>
|
||||
* Need those applications to operate at massive scale, e.g. concurrent users in the millions.
|
||||
|
||||
That's great if you're Google, and surprise! Kubernetes is a largely Google-originated project,<Sidenote>I'm told that it's a derivative of Borg, Google's in-house orchestration platform.</Sidenote> but it's absolute _garbage_ if you (like me) are just self-hosting apps for your own personal use and enjoyment. It's garbage because, while you still want to support a broad variety of applications, you typically _didn't_ write them yourself and you _most definitely don't_ need to scale to millions of concurrent users. More particularly, this means that the Kubernetes approach of expecting everything to be aware that it's running in Kubernetes and make use of the platform (via cluster roles, CRD's etc) is very much _not_ going to fly. Instead, you want your orchestration platform to be as absolutely transparent as possible: ideally, a running application should need to behave no differently in this hypothetical self-hosting-focused orchestration system than it would if it were running by itself on a Raspberry Pi in your garage. _Most especially_, all the distributed-systems crap that Kubernetes forces on you is pretty much unnecessary, because you don't need to support millions<Sidenote>In fact, typically your number of concurrent users is going to be either 1 or 0.</Sidenote> of concurrent users, and you don't care if you incur a little downtime when the application needs to be upgraded or whatever.
|
||||
|
||||
## But Wait
|
||||
|
||||
So then why do you need an orchestration platform at all? Why not just use something like [Harbormaster](https://gitlab.com/stavros/harbormaster) and call it a day? That's a valid question, and maybe you don't! In fact, it's quite likely that you don't - orchestration platforms really only make sense when you want to distribute your workload across multiple physical servers, so if you only have the one then why bother? However, I can still think of a couple of reasons why you'd want a cluster even for your personal stuff:
|
||||
|
||||
* You don't want everything you host to become completely unavailable if you bork up your server somehow. Yes, I did say above that you can tolerate some downtime, and that's still true - but especially if you like tinkering around with low-level stuff like filesystems and networking, it's quite possible that you'll break things badly enough<Sidenote>And be sufficiently busy with other things, given that we're assuming this is just a hobby for you.</Sidenote> that it will be days or weeks before you can find the time to fix them. If you have multiple servers to which the workloads can migrate while one is down, that problem goes away.
|
||||
|
||||
* You don't want to shell out up front for something hefty enough to run All Your Apps, especially as you add more down the road. Maybe you're starting out with a Raspberry pi, and when that becomes insufficient you'd like to just add more Pis rather than putting together a beefy machine with enough RAM to feed your [Paperless](https://github.com/paperless-ngx/paperless-ngx) installation, your [UniFi controller](https://help.ui.com/hc/en-us/articles/360012282453-Self-Hosting-a-UniFi-Network-Server), your Minecraft server(s), and your [Matrix](https://matrix.org) server.
|
||||
|
||||
Okay, sure, maybe this is still a bit niche. But you know what? This is my blog, so I get to be unrealistic if I want to.
|
||||
|
||||
## The Goods
|
||||
|
||||
### Firecracker
|
||||
|
||||
You didn't write all these apps yourself, and you don't trust them any further than you can throw them. Containers are great and all, but you'd like a little more organization. Enter Firecracker. This does add some complexity where resource management is concerned, especially memory, since by default Firecracker wants you to allocate everything up front. But maybe that's ok, or maybe we can build in some [ballooning](https://github.com/firecracker-microvm/firecracker/blob/main/docs/ballooning.md) to keep things under control.
|
||||
|
||||
### Storage
|
||||
|
||||
Kubernetes tends to work best with stateless applications. It's not entirely devoid of [tools](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) for dealing with state, but state requires persistent storage and persistent storage is hard in clusters. I get the sense that for a long time you were almost completely on your own here, although recent options (Longhorn) are improving the situation.
|
||||
|
||||
Regardless, we're selfhosting here, which means virtually _everything_ has state. But fear not! Distributed state is hard, yes, but most of our apps aren't going to be truly distributed. That is, typically there's only going to be one instance running at a time, and it's acceptable to shut down the existing instance before spinning up a new one. So this problem becomes a lot more tractable.
|
||||
|
||||
* Asynchronous replication
|
||||
* Single-writer, multi-reader
|
||||
* Does this exist?
|
@ -2,11 +2,12 @@
|
||||
title: Sidenotes
|
||||
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
|
||||
date: 2023-08-14
|
||||
draft: true
|
||||
---
|
||||
<script>
|
||||
import Dropcap from '$lib/Dropcap.svelte';
|
||||
import Sidenote from '$lib/Sidenote.svelte';
|
||||
import UnstyledSidenote from '$lib/UnstyledSidenote.svelte';
|
||||
import Frame from '$lib/projects/sidenotes/Frame.svelte';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -63,7 +64,7 @@ draft: true
|
||||
}
|
||||
</style>
|
||||
|
||||
One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote>
|
||||
<Dropcap word="One">of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote></Dropcap>
|
||||
|
||||
Footnotes don't really work on the web the way they do on paper, since the web doesn't have page breaks. You _can_ stick your footnotes in a floating box at the bottom of the page, so they're visible at the bottom of the text just like they would be on a printed page, but this sacrifices precious vertical space.<Sidenote>On mobile, it's _horizontal_ space that's at a premium, so I do use this approach there. Although I'm a pretty heavy user of sidenotes, so I have to make them toggleable as well or they'd fill up the entire screen.</Sidenote> Plus, you usually end up with the notes further away from the point of divergence than they would be as sidenotes anyway.
|
||||
|
||||
|
@ -4,21 +4,22 @@ description: They're more similar than they are different, but they say the most
|
||||
date: 2023-06-29
|
||||
---
|
||||
<script>
|
||||
import Dropcap from '$lib/Dropcap.svelte';
|
||||
import Sidenote from '$lib/Sidenote.svelte';
|
||||
</script>
|
||||
|
||||
|
||||
Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.
|
||||
<Dropcap word="Recently">I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.</Dropcap>
|
||||
|
||||
This is necessarily going to focus on a lot of small differences, because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
|
||||
Of course, this is only possible because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
|
||||
* Single-file components with separate sections for markup, style, and logic
|
||||
* Automatically reactive data bindings
|
||||
* Two-way data binding (a point of almost religious contention in certain circles)
|
||||
* An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template wraps the logic, whereas in React, the logic wraps the template.
|
||||
* An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template<Sidenote>Or single-file component, anyway.</Sidenote> embeds the logic, whereas in React, the logic embeds the template.
|
||||
|
||||
I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that's all I've used. I've only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I've never used it myself.
|
||||
|
||||
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>Completely arbitrary, of course, so feel free to disagree!</Sidenote> Starting with:
|
||||
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>I should also clarify that practically everything in this post is just that: a preference. While I obviously plan to explain my preferences and think it would be reasonable for other people to do the same, it's undeniably true that preferences can vary, and in a lot of cases are basically arbitrary. So if you find yourself disagreeing with all or most of what I say, consider it an opportunity to peer into the mindset of The Other Side.</Sidenote> Starting with:
|
||||
|
||||
## Template Syntax
|
||||
|
||||
@ -52,11 +53,11 @@ While Svelte takes the more common approach of wrapping bits of markup in its ow
|
||||
</div>
|
||||
```
|
||||
|
||||
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying about something so trivial, but hey, this whole post is one big bikeshed anyway.</Sidenote>
|
||||
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying<Sidenote>Nested<Sidenote>Doubly-nested sidenote!</Sidenote> sidenote!</Sidenote> about something so trivial,<Sidenote>Second nested sidenote.</Sidenote> but hey, this whole post is one big bikeshed anyway.</Sidenote>
|
||||
|
||||
Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue's custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.<Sidenote>AlpineJS also works this way, but this is the *only* way that it works - it doesn't have an equivalent for Vue's full-fat "app mode" as it were.</Sidenote> This strikes me as a fantastic way to ease the transition between "oh I just need a tiny bit of interactivity on this page, so I'll just sprinkle in some inline components" and "whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now."
|
||||
|
||||
Detractors of this approach might point out that it's harder to spot things like `v-if` and `v-for` when they're hanging out inside of existing HTML tags, but that seems like a problem that's easily solved with a bit of syntax highlighting. However I do have to admit that it's a reversal of the typical order in which you read code: normally you see the control-flow constructs _first_, and only _after_ you've processed those do you start to worry about whatever they're controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) problem where you have to mentally double back and re-read things in a different light. I still don't think it's a huge issue, though, because in every case I'm come across the control flow bits (so `v-if`, `v-for`, and `v-show`) are specified _immediately_ after the opening tag. So you don't really have to double back by an appreciable amount, and it doesn't take too long to get used to it.
|
||||
Detractors of this approach might point out that it's harder to spot things like `v-if` and `v-for` when they're hanging out inside of existing HTML tags, but that seems like a problem that's easily solved with a bit of syntax highlighting.<Sidenote>I'm being unfair here. It's more than just a lack of syntax highlighting, it's a reversal of the typical order in which people are used to reading code, where the control flow is indicated before whatever it's controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) problem where you have to mentally double back and re-read things in a different light. I still don't think it's a huge issue, though, because in every case I'm come across the control flow bits (so `v-if`, `v-for`, and `v-show`) are specified _immediately_ after the opening tag. So you don't really have to double back by an appreciable amount, and it doesn't take too long to get used to it.</Sidenote>
|
||||
|
||||
Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including `:` for `v-bind` and `@` for `v-on`. Svelte doesn't really have an equivalent for this, although it does allow you to shorten `attr={attr}` to `{attr}`, which can be convenient. Which might as well bring us to:
|
||||
|
||||
@ -70,7 +71,7 @@ The reason this threw me is that Svelte makes the very intuitive decision that s
|
||||
|
||||
Two-way bindings in Svelte are similarly straightforward, for example: `<input type="checkbox" bind:checked={isChecked}>` In Vue this would be `<input type="checkbox" v-model="isChecked">`, which when you first see it doesn't exactly scream that the value of `isChecked` is going to apply to the `checked` property of the checkbox. On the other hand, this does give Vue the flexibility of doing special things for e.g. the values of `<select>` elements: `<select v-model="selectedOption">` is doing quite a bit of work, since it has to interact with not only the `<select>` but the child `<option>`s as well. Svelte just throws in the towel here and tells you to do `<select bind:value={selectedOption}>`, which looks great until you realize that `value` isn't technically a valid attribute for a `<select>`. So Svelte's vaunted principle of "using the platform" does get a _little_ bent out of shape here.
|
||||
|
||||
Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component whose attribute you want to bind, rather than a built-in form input. Vue enforces that props be immutable from the inside, i.e. a component isn't supposed to mutate its own props. So from the parent component it doesn't look too bad:
|
||||
Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component whose attribute you want to bind, rather than a builtin form input. Vue enforces that props be immutable from the inside, i.e. a component isn't supposed to mutate its own props. So from the parent component it doesn't look too bad:
|
||||
```markup
|
||||
<ChildComponent v-model="childValue" />`
|
||||
```
|
||||
@ -87,9 +88,9 @@ export default {
|
||||
}
|
||||
```
|
||||
|
||||
In Svelte, you just `bind:` on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don't think there's any denying that's a lot simpler.<Sidenote>I think this is where the "two-way data binding" holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are _implicitly_ two-way, i.e. anyone with a reference tosome stat can mutate it in ways the original owner didn't expect or intend it to. (KnockoutJS observables work this way, I think?) In Svelte's case, though, this is only possible if you explicitly pass the state with `bind:`, which signifies that you _do_ want this state to be mutated by the child and that you have made provisions therefor. My understanding is that in React you'd just be emitting an event from the child component and handling that event up the tree somewhere, so in practice it's basically identical. That said, I haven't used React so perhaps I'm not giving the React Way™ a fair shake here.</Sidenote>
|
||||
In Svelte, you just `bind:` on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don't think there's any denying that's a lot simpler.<Sidenote>I think this is where the "two-way data binding" holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are _implicitly_ two-way, i.e. the child can mutate state that the parent didn't expect or intend it to. In Svelte's case, though, this is only possible if you explicitly pass the state with `bind:`, which signifies that you _do_ want this state to be mutated by the child and that you have made provisions therefor. </Sidenote>
|
||||
|
||||
Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the `class` of an HTML element, for example: `<button :class="{btn: true, primary: false}">` Which doesn't look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is `<button class:btn={isBtn} class:primary={isPrimary}>`, which is a lot more verbose. Vue also lets you bind an array to `class` and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you're toggling them all as a set. <Sidenote>Since I'm a fan of TailwindCSS, this tends to come up for me with some regularity.</Sidenote>
|
||||
Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the `class` of an HTML element, for example: `<button :class="{btn: true, primary: false}">` Which doesn't look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is `<button class:btn={isBtn} class:primary={isPrimary}>`, which is a lot more verbose. Vue also lets you bind an array to `class` and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you're toggling them all as a set.
|
||||
|
||||
The other area where I vastly prefer Vue's approach over Svelte's is in event handlers. Svelte requires that every event handler be a function, either named or inline, so with simple handlers you end up with a lot of `<button on:click={() => counter += 1}` situations. Vue takes the much more reasonable approach of letting you specify a plain statement as your event handler, e.g. `<button @click="counter += 1">`. For whatever reason this has always particularly annoyed me about Svelte, so Vue's take is very refreshing.
|
||||
|
||||
@ -106,7 +107,7 @@ You really only need to access the event when you're doing something more exotic
|
||||
|
||||
In Vue, reactive values (by which I mean "values that can automatically trigger a DOM update when they change") are either passed in as `props`, or declared in `data`. Or derived from either of those sources in `computed`. Then you reference them, either directly in your template or as properties of `this` in your logic. Which works fine, more or less, although you can run into problems if you're doing something fancy with nested objects or functions that get their own `this` scope.<Sidenote>It's worth noting that the Composition API avoids this, at the cost of having to call `ref()` on everything and reference `reactiveVar.value` rather than `reactiveVar` by itself.</Sidenote> The split between how you access something from the template and how you access it from logic was a touch surprising to me at first, though.
|
||||
|
||||
In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically.<Sidenote>And of course, after I first wrote this but just before I was finally ready to publish, Svelte went ahead and [changed this on me](https://svelte.dev/blog/runes). I'll leave my comments here as I originally wrote them, just keep in mind that if these changes stick then Svelte becomes even _more_ similar to Vue's composition API.</Sidenote> Svelte has a lot more freedom here because it's a compiler, rather than a library, so it can easily insert calls to its special `$$invalidate()` function after any update to a value that needs to be reactive.
|
||||
In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically. Svelte has a lot more freedom here because it's a compiler, rather than a library, so it can easily insert calls to its special `$$invalidate()` function after any update to a value that needs to be reactive.
|
||||
|
||||
Both frameworks allow you to either derive reactive values from other values, or just execute arbitrary code in response to data updates. In Vue these are two different concepts - derived reactive values are declared in `computed`, and reactive statements via the `watch` option. In Svelte they're just the same thing: Prefix any statement with `$:` (which is actually valid JS, as it turns out) and it will automatically be re-run any time one of the reactive values that it references gets updated. So both of the following:
|
||||
```js
|
||||
@ -123,7 +124,7 @@ $: console.log(firstname, lastname);
|
||||
|
||||
I go back and forth on this one, but I _think_ I have a slight preference for Svelte (at least, at the moment.) The major difference is that Vue<Sidenote>If you're using the Options API, at least.</Sidenote> enforces a lot more structure than Svelte: Data is in `props`/`data`/`computed`, logic is in `methods`, reactive stuff is in `watch`, etc. Svelte, by contrast, just lets you do basically whatever you want. It does require that you have only one `<script>` tag, so all your logic ends up being co-located, but that's pretty much it. Everything else is just a convention, like declaring props at the top of your script.
|
||||
|
||||
The advantage of Vue's approach is that it can make it easier to find things when you're jumping from template to logic: you see `someFunction(whatever)`, you know it's going to be under `methods`. With Svelte, `someFunction` could be defined anywhere in the script section.<Sidenote>Code structure is actually one area that I think might be improved by the recently-announced Svelte 5 changes: Because you can now declare reactive state anywhere, rather than just at the top level of your script, you can take all the discrete bits of functionality within a single component and bundle each one up in its own function, or even factor them out into different files entirely. I can imagine this being helpful, but I haven't played with it yet so I don't know for sure how it will shake out.</Sidenote>
|
||||
The advantage of Vue's approach is that it can make it easier to find things when you're jumping from template to logic: you see `someFunction(whatever)`, you know it's going to be under `methods`. With Svelte, `someFunction` could be defined anywhere in the script section.
|
||||
|
||||
On the other hand, this actually becomes a downside once your component gets a little bit complex. Separation of concerns is nice and all, but sometimes it just doesn't work very well to split a given component, and it ends up doing several unrelated or at least clearly distinct things. In Vue-land, the relevant bits of state, logic, etc. are all going to be scattered across `data`/`methods`/etc, meaning you can't really see "all the stuff that pertains to this one bit of functionality" in one place. It's also very clunky to split the logic for a single component across multiple JS files, which you might want to do as another way of managing the complexity of a large component. If you were to try, you'd end up with a big "skeleton" in your main component file, e.g.
|
||||
|
||||
@ -145,11 +146,11 @@ export default {
|
||||
|
||||
which doesn't seem very pleasant.
|
||||
|
||||
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization)<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> for the introduction of the Composition API in the first place. Unfortunately it also includes the downside that you have to call `ref()` on all your reactive values, and reference them by their `.value` property rather than just using the main variable. It's funny that this bothers me as much as it does, given that `this.someData` is hardly any more concise than `someData.value`, but there's no accounting for taste, I guess. Using `this` just feels more natural to me, although what feels most natural is Svelte's approach where you don't have to adjust how you reference reactive values at all.
|
||||
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization) for the introduction of the Composition API in the first place.<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> Unfortunately it also includes the downside that you have to call `ref()` on all your reactive values, and reference them by their `.value` property rather than just using the main variable. It's funny that this bothers me as much as it does, given that `this.someData` is hardly any more concise than `someData.value`, but there's no accounting for taste, I guess. Using `this` just feels more natural to me, although what feels most natural is Svelte's approach where you don't have to adjust how you reference reactive values at all.
|
||||
|
||||
Also, as long as we're harping on minor annoyances: For some reason I cannot for the life of me remember to put commas after all my function definitions in `computed`, `methods` etc. in my Vue components. It's such a tiny thing, but it's repeatedly bitten me because my workflow involves Vue automatically rebuilding my app every time I save the file, and I'm not always watching the console output because my screen real estate is in use elsewhere.<Sidenote>E.g. text editor on one screen with two columns of text, web page on one half of the other screen and dev tools on the other half. Maybe I need a third monitor?</Sidenote> So I end up forgetting a comma, the rebuild fails but I don't notice, and then I spend five minutes trying to figure out why my change isn't taking effect before I think to check for syntax errors.
|
||||
|
||||
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is more or less impossible<Sidenote>I mean, you could do it, but you'd have to ship the entire Svelte compiler with your page.</Sidenote> with Svelte is at-runtime or "inline" components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can't do this because it's a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don't want to or even _can't_ add a build step, and in those cases Vue can really shine.
|
||||
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is completely impossible with Svelte is at-runtime or "inline" components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can't do this because it's a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don't want to or even _can't_ add a build step, and in those cases Vue can really shine.
|
||||
|
||||
## Miscellany
|
||||
|
||||
@ -157,20 +158,8 @@ It would be remiss of me, however, not to point out that one thing the Vue Optio
|
||||
|
||||
Performance isn't really a major concern for me when it comes to JS frameworks, since I don't tend to build the kind of extremely-complex apps where the overhead of the framework starts to make a difference. For what it's worth, though, the [Big Benchmark List](https://krausest.github.io/js-framework-benchmark/current.html) has Vue slightly ahead of Svelte when it comes to speed.<Sidenote>Although [recent rumors](https://twitter.com/Rich_Harris/status/1688581184018583558) put the next major version of Svelte _very_ close to that of un-framework'd vanilla JS, so this might change in the future.</Sidenote> I don't know how representative this benchmark is of a real-world workload.
|
||||
|
||||
As far as bundle size goes, it's highly dependent on how many components you're shipping - since Svelte compiles everything down to standalone JS and there's no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there's no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped. The Svelte people say that this problem doesn't tend to crop up a lot in practice, but I have yet to see real-world examples for the bundle size of a non-trivial<Sidenote>Probably because no one wants to bother implementing the exact same app in two different frameworks just to test a theory.</Sidenote> app implemented in Vue vs. Svelte.
|
||||
As far as bundle size goes, it's highly dependent on how many components you're shipping - since Svelte compiles everything down to standalone JS and there's no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there's no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped.
|
||||
|
||||
### Ecosystem
|
||||
|
||||
Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses [SvelteKit](https://kit.svelte.dev) and [mdsvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it's fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte's immaturity issues plus more of its own.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). All in all I'd say if a robust ecosystem is important to you then Vue is probably the better choice at this point.
|
||||
|
||||
### Stability
|
||||
|
||||
Not in terms of "will it crash while you're using it," but in terms of "will code that you write today still be usable in five years." This is always a bit of a big ask in the JS world, because everyone is always pivoting to chase the new shiny. As I write this now (and as I referenced above), Svelte has just announced a [change](https://svelte.dev/blog/runes) to how reactivity is done. The new style is opt-in for the moment, but that's never completely reassuring--there are plenty of examples of opt-in features that became required eventually. Vue had a similar moment with their 2-to-3 switch,<Sidenote>Just like Python, hmm. What is it about the 2-to-3 transition? Maybe we should call it Third System Effect?</Sidenote> but to be fair they have so far stuck to their promise to keep the Options API a first-class citizen.
|
||||
|
||||
I think that means I have to give Vue the edge on this one, because while both frameworks now have an "old style" vs. a "new style" Vue at least has proven their willingness to continue supporting the old style over the last few years.
|
||||
|
||||
## What's Next
|
||||
|
||||
I don't think we've reached the "end-game" when it comes to UI paradigms, either on the web or more generally. I _do_ think that eventually, _probably_ within my lifetime, we will see a stable and long-lasting consensus emerge, and the frenetic pace of "framework churn" in the frontend world will slow down somewhat. What exact form this will take is very much up in the air, of course, but I have a sneaking suspicion that WebAssembly will play a key part, if it can ever get support for directly communicating with the DOM (i.e. without needing to pass through the JS layer). _If_ and when that happens, it will unlock a huge new wave of frontend frameworks that don't have to involve on Javascript at all, and won't that be interesting?
|
||||
|
||||
But for now I'll stick with Svelte, although I think Vue is pretty good too. Just don't make me use React, please.
|
||||
|
@ -1,23 +0,0 @@
|
||||
<style>
|
||||
.content {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>About Me | Joe's Blog</title>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
<div class="content">
|
||||
<h1>About Me</h1>
|
||||
|
||||
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
|
||||
|
||||
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
|
||||
|
||||
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
|
||||
|
||||
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
|
||||
</div>
|
@ -1,53 +0,0 @@
|
||||
import { tag, text, serialize } from '$lib/xml.js';
|
||||
import { postData } from '../_posts/all.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
|
||||
export function GET() {
|
||||
return new Response(renderFeed(), {
|
||||
headers: {'Content-Type': 'application/atom+xml'}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderFeed() {
|
||||
const feed = tag('feed', {xmlns: 'http://www.w3.org/2005/Atom'});
|
||||
|
||||
feed.addTag('id', {}, [text('https://blog.jfmonty2.com/')])
|
||||
feed.addTag('title', {}, [text("Joe's Blog")]);
|
||||
feed.addTag('link', {rel: 'alternate', href: 'https://blog.jfmonty2.com/'});
|
||||
feed.addTag('link', {rel: 'self', href: 'https://blog.jfmonty2.com/feed/'});
|
||||
|
||||
const lastUpdate = iso(postData[0].updated || postData[0].date);
|
||||
feed.addTag('updated', {}, [text(lastUpdate)]);
|
||||
|
||||
const author = feed.addTag('author');
|
||||
author.addTag('name', {}, [text('Joseph Montanaro')]);
|
||||
|
||||
for (const post of postData) {
|
||||
const url = `https://blog.jfmonty2.com/${post.slug}`
|
||||
const entry = feed.addTag('entry');
|
||||
entry.addTag('title', {}, [text(post.title)]);
|
||||
entry.addTag('link', {rel: 'alternate', href: url});
|
||||
entry.addTag('id', {}, [text(url)]);
|
||||
|
||||
const publishedDate = iso(post.date);
|
||||
entry.addTag('published', {}, [text(publishedDate)])
|
||||
const updatedDate = iso(post.updated || post.date);
|
||||
entry.addTag('updated', {}, [text(updatedDate)]);
|
||||
|
||||
entry.addTag('content', {type: 'html'}, [text(renderDescription(post))]);
|
||||
}
|
||||
return serialize(feed);
|
||||
}
|
||||
|
||||
|
||||
function renderDescription(post) {
|
||||
return `<p>${post.description} <a href="https://blog.jfmonty2.com/${post.slug}">Read more</a></p>`;
|
||||
}
|
||||
|
||||
|
||||
function iso(datetimeStr) {
|
||||
return new Date(datetimeStr).toISOString();
|
||||
}
|
18
src/routes/index.svelte
Normal file
18
src/routes/index.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script context="module">
|
||||
export async function load({ fetch }) {
|
||||
const resp = await fetch('/latest.json');
|
||||
const postMeta = await resp.json();
|
||||
const post = await import(`./_posts/${postMeta.slug}.svx`);
|
||||
return {
|
||||
props: {
|
||||
post: post.default,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<svelte:component this={post} />
|
5
src/routes/latest.json.js
Normal file
5
src/routes/latest.json.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { postData } from './posts.js';
|
||||
|
||||
export async function get() {
|
||||
return {body: postData[0]};
|
||||
}
|
@ -1,26 +1,28 @@
|
||||
import { dev } from '$app/environment';
|
||||
const posts = import.meta.globEager('./*.svx');
|
||||
import { dev } from '$app/env';
|
||||
const posts = import.meta.globEager('./_posts/*.svx');
|
||||
|
||||
export let postData = [];
|
||||
|
||||
let postData = [];
|
||||
for (const path in posts) {
|
||||
// skip draft posts in production mode
|
||||
if (!dev && posts[path].metadata.draft) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// slice off the ./ and the .svx
|
||||
const slug = path.slice(2, -4);
|
||||
const slug = path.slice(9, -4)
|
||||
posts[path].metadata.slug = slug;
|
||||
postData.push(posts[path].metadata);
|
||||
}
|
||||
|
||||
|
||||
postData.sort((a, b) => {
|
||||
// sorting in reverse, so we flip the intuitive order
|
||||
if (a.date > b.date) return -1;
|
||||
if (a.date < b.date) return 1;
|
||||
return 0;
|
||||
});
|
||||
})
|
||||
|
||||
export async function get() {
|
||||
return {
|
||||
body: {postData}
|
||||
};
|
||||
}
|
||||
|
||||
export { postData };
|
@ -1,20 +1,27 @@
|
||||
<script>
|
||||
import { formatDate } from '$lib/datefmt.js';
|
||||
import { postData } from '../_posts/all.js';
|
||||
export let postData;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
#posts {
|
||||
/*text-align: center;*/
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
max-width: 24rem;
|
||||
// margin-top: 1.25rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 2rem 0;
|
||||
border-color: #eee;
|
||||
.post {
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* .post-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}*/
|
||||
|
||||
.post-date {
|
||||
color: #808080;
|
||||
}
|
||||
@ -37,14 +44,9 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
color: currentcolor;
|
||||
h3 {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -54,22 +56,18 @@
|
||||
|
||||
<div id="posts">
|
||||
<h1 style:text-align="center">All Posts</h1>
|
||||
{#each postData as post, idx}
|
||||
{#each postData as post}
|
||||
<div class="post">
|
||||
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
|
||||
<h2>
|
||||
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
|
||||
{post.title}
|
||||
<div>
|
||||
<a sveltekit:prefetch class="post-link" href="/{post.slug}">
|
||||
<h3>{post.title}<h3>
|
||||
</a>
|
||||
{#if post.draft}
|
||||
<span class="draft-notice">Draft</span>
|
||||
{/if}
|
||||
</h2>
|
||||
</div>
|
||||
<p>{post.description}</p>
|
||||
</div>
|
||||
|
||||
{#if idx < postData.length - 1}
|
||||
<hr>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -29,12 +29,10 @@ html {
|
||||
line-height: var(--content-line-height);
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--content-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
--content-width: 42rem;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@ -74,6 +72,10 @@ p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/*ul, ol {
|
||||
margin: 0.5rem 0;
|
||||
}*/
|
||||
|
||||
code {
|
||||
background: #eee;
|
||||
border-radius: 0.2rem;
|
||||
@ -85,3 +87,5 @@ code {
|
||||
pre > code {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* TESTING */
|
||||
|
@ -1,24 +1,24 @@
|
||||
import staticAdapter from '@sveltejs/adapter-static';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import staticAdapter from '@sveltejs/adapter-static';
|
||||
import svp from 'svelte-preprocess';
|
||||
import slug from './src/lib/slug.js';
|
||||
|
||||
import { localRemark } from './src/plugins/remark.js';
|
||||
import { localRehype } from './src/plugins/rehype.js';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx'],
|
||||
preprocess: [
|
||||
mdsvex({
|
||||
layout: './src/lib/Post.svelte',
|
||||
remarkPlugins: [localRemark],
|
||||
rehypePlugins: [localRehype],
|
||||
rehypePlugins: [slug],
|
||||
}),
|
||||
svp.scss(),
|
||||
],
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
// hydrate the <div id="svelte"> element in src/app.html
|
||||
adapter: staticAdapter(),
|
||||
prerender: {
|
||||
default: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
281
tmp/crane.svg
Normal file
281
tmp/crane.svg
Normal file
@ -0,0 +1,281 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<!-- Crane image created by macrovector on Freepik: https://www.freepik.com/free-vector/construction-icons-set_1537228.htm#query=crane&position=3&from_view=keyword -->
|
||||
|
||||
<svg
|
||||
width="28.305676mm"
|
||||
height="28.174238mm"
|
||||
viewBox="0 0 28.305676 28.174238"
|
||||
version="1.1"
|
||||
id="svg1392"
|
||||
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||
sodipodi:docname="crane.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1394"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.1089995"
|
||||
inkscape:cx="-5.6899017"
|
||||
inkscape:cy="78.710306"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1389" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-139.84716,-103.71933)">
|
||||
<path
|
||||
d="m 166.79605,124.82179 h 0.18627 v -20.83188 h -0.18627 v 20.83188"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path324" />
|
||||
<path
|
||||
d="m 166.25101,125.97184 h 1.27635 v -0.51893 c 0,-0.1323 -0.10724,-0.23919 -0.23918,-0.23919 h -0.79763 c -0.13229,0 -0.23954,0.10689 -0.23954,0.23919 v 0.51893"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path326" />
|
||||
<path
|
||||
d="m 166.6934,125.21372 h 0.39193 v -0.64981 h -0.39193 v 0.64981"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path328" />
|
||||
<path
|
||||
d="m 165.62554,126.85626 c 0,0.69814 0.56585,1.26365 1.26365,1.26365 0.69779,0 1.26365,-0.56551 1.26365,-1.26365 0,-0.6978 -0.56586,-1.26365 -1.26365,-1.26365 -0.6978,0 -1.26365,0.56585 -1.26365,1.26365"
|
||||
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path330" />
|
||||
<path
|
||||
d="m 143.76637,124.8673 19.84057,-20.49675 -0.0935,-0.0903 -19.84093,20.49639 0.0938,0.0907"
|
||||
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path332" />
|
||||
<path
|
||||
d="m 149.82885,128.1506 2.90478,-6.56802 10.82675,-17.25718 1.80869,1.13488 -10.81899,17.2466 -4.61751,5.50863 z m 13.67261,-24.08167 -10.93188,17.42652 -2.97638,6.72711 0.37782,0.23707 4.72899,-5.64338 10.92553,-17.41488 -2.12408,-1.33244"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path334" />
|
||||
<path
|
||||
d="m 154.67073,122.68783 -0.0988,0.1577 -1.96638,-1.23367 0.0988,-0.15769 1.96638,1.23366"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path336" />
|
||||
<path
|
||||
d="m 154.712,122.74604 -0.18132,0.0413 -0.71791,-3.13443 0.18168,-0.0416 0.71755,3.13478"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path338" />
|
||||
<path
|
||||
d="m 155.85042,120.93135 -3.13443,0.71755 -0.0416,-0.18132 3.13478,-0.71791 0.0413,0.18168"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path340" />
|
||||
<path
|
||||
d="m 155.87935,120.76167 -0.0991,0.15769 -1.96639,-1.23366 0.0991,-0.1577 1.96639,1.23367"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path342" />
|
||||
<path
|
||||
d="m 155.92062,120.81952 -0.18168,0.0416 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13443"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path344" />
|
||||
<path
|
||||
d="m 157.05903,119.00483 -3.13478,0.71791 -0.0413,-0.18133 3.13443,-0.7179 0.0416,0.18132"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path346" />
|
||||
<path
|
||||
d="m 157.08761,118.8355 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.15769 1.96638,1.23366"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path348" />
|
||||
<path
|
||||
d="m 157.12888,118.89336 -0.18132,0.0416 -0.71791,-3.13478 0.18168,-0.0413 0.71755,3.13443"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path350" />
|
||||
<path
|
||||
d="m 158.2673,117.07867 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18133"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path352" />
|
||||
<path
|
||||
d="m 158.29623,116.90898 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15804 1.96639,1.23366"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path354" />
|
||||
<path
|
||||
d="m 158.3375,116.96719 -0.18168,0.0416 -0.71755,-3.13479 0.18133,-0.0413 0.7179,3.13443"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path356" />
|
||||
<path
|
||||
d="m 159.47592,115.1525 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path358" />
|
||||
<path
|
||||
d="m 159.50449,114.98282 -0.0988,0.15804 -1.96638,-1.23366 0.0988,-0.15805 1.96638,1.23367"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path360" />
|
||||
<path
|
||||
d="m 159.54577,115.04102 -0.18133,0.0416 -0.71791,-3.13478 0.18169,-0.0413 0.71755,3.13443"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path362" />
|
||||
<path
|
||||
d="m 160.68418,113.22633 -3.13443,0.71791 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18132"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path364" />
|
||||
<path
|
||||
d="m 160.71311,113.05665 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15805 1.96639,1.23367"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path366" />
|
||||
<path
|
||||
d="m 160.75438,113.11486 -0.18168,0.0416 -0.7179,-3.13478 0.18168,-0.0413 0.7179,3.13443"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path368" />
|
||||
<path
|
||||
d="m 161.8928,111.30017 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path370" />
|
||||
<path
|
||||
d="m 161.92137,111.13048 -0.0991,0.15805 -1.96603,-1.23367 0.0988,-0.15804 1.96638,1.23366"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path372" />
|
||||
<path
|
||||
d="m 161.96265,111.18869 -0.18133,0.0416 -0.71791,-3.13479 0.18169,-0.0416 0.71755,3.13478"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path374" />
|
||||
<path
|
||||
d="m 163.10106,109.374 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.7179 0.0413,0.18168"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path376" />
|
||||
<path
|
||||
d="m 163.12999,109.20432 -0.0991,0.15769 -1.96639,-1.23367 0.0991,-0.15769 1.96639,1.23367"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path378" />
|
||||
<path
|
||||
d="m 163.17126,109.26252 -0.18168,0.0413 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13478"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path380" />
|
||||
<path
|
||||
d="m 164.30968,107.44783 -3.13479,0.71755 -0.0413,-0.18132 3.13443,-0.71791 0.0416,0.18168"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path382" />
|
||||
<path
|
||||
d="m 164.33825,107.27815 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.1577 1.96638,1.23367"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path384" />
|
||||
<path
|
||||
d="m 164.37953,107.33636 -0.18133,0.0413 -0.7179,-3.13443 0.18168,-0.0416 0.71755,3.13479"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path386" />
|
||||
<path
|
||||
d="m 165.51794,105.52167 -3.13443,0.71755 -0.0416,-0.18133 3.13443,-0.7179 0.0416,0.18168"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path388" />
|
||||
<path
|
||||
d="m 153.38944,124.32366 -0.18133,0.0416 -0.64382,-2.81128 0.18133,-0.0416 0.64382,2.81128"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path390" />
|
||||
<path
|
||||
d="m 153.34817,124.26581 -0.0988,0.15769 -1.48908,-0.93415 0.0991,-0.1577 1.48873,0.93416"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path392" />
|
||||
<path
|
||||
d="m 154.64215,122.85752 -2.81163,0.64382 -0.0413,-0.18168 2.81129,-0.64382 0.0416,0.18168"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path394" />
|
||||
<path
|
||||
d="m 152.06476,125.91399 -0.18556,0.012 -0.16228,-2.50931 0.18591,-0.0123 0.16193,2.50966"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path396" />
|
||||
<path
|
||||
d="m 152.02137,125.84096 -0.0988,0.15769 -0.98954,-0.62053 0.0991,-0.15769 0.98919,0.62053"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path398" />
|
||||
<path
|
||||
d="m 153.33405,124.43056 -2.31598,0.95461 -0.0709,-0.17215 2.31599,-0.95462 0.0709,0.17216"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path400" />
|
||||
<path
|
||||
d="m 167.06769,103.88196 c -0.20602,-0.12912 -0.44944,-0.1838 -0.69074,-0.15523 l -2.87549,0.3422 -0.0935,0.16122 2.09691,1.31551 1.58573,-1.32151 c 0.0522,-0.0434 0.0801,-0.10936 0.0759,-0.17709 -0.005,-0.0677 -0.0416,-0.12912 -0.0988,-0.1651"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path402" />
|
||||
<path
|
||||
d="m 163.77733,104.24179 2.71922,-0.33902 c 0.13687,-0.0169 0.27551,0.0138 0.39264,0.0871 l 0.0395,0.0247 c 0.0162,0.0102 0.0265,0.0275 0.0279,0.0466 0.001,0.019 -0.007,0.0378 -0.0215,0.0501 l -1.44533,1.20474 -1.71239,-1.07421"
|
||||
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path404" />
|
||||
<path
|
||||
d="m 145.63997,128.61168 h 3.81459 v 0.62547 h -3.81459 v -0.62547"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path406" />
|
||||
<path
|
||||
d="m 140.21425,126.26888 c 0.0783,-0.35489 0.34149,-0.63994 0.68897,-0.74683 l 6.68832,-2.05246 -0.3549,5.14209 h -6.77756 c -0.18556,0 -0.3609,-0.084 -0.47696,-0.2286 -0.11606,-0.14429 -0.16051,-0.33373 -0.12065,-0.5147 l 0.35278,-1.5995"
|
||||
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path408" />
|
||||
<path
|
||||
d="m 151.90037,131.89357 c 0.75917,0 1.37689,-0.61771 1.37689,-1.37724 0,-0.75918 -0.61772,-1.3769 -1.37689,-1.3769 -0.0243,-0.002 -3.12667,-0.22013 -4.15961,-0.22013 -1.03293,0 -4.13526,0.21802 -4.1663,0.22049 h -7.1e-4 c -0.75212,0 -1.36948,0.61736 -1.36948,1.37654 0,0.75953 0.61771,1.37724 1.37689,1.37724 h 8.31921"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path410" />
|
||||
<path
|
||||
d="m 143.58116,131.69778 c -0.65158,0 -1.18145,-0.52987 -1.18145,-1.18145 0,-0.65123 0.52669,-1.1811 1.17475,-1.1811 l 0.0141,-7.1e-4 c 0.03,-0.002 3.12667,-0.21943 4.15219,-0.21943 1.03082,0 4.11551,0.21696 4.14338,0.21908 l 0.008,7e-4 h 0.008 c 0.65158,0 1.18145,0.53023 1.18145,1.18146 0,0.65158 -0.52987,1.18145 -1.18145,1.18145 h -8.31921"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path412" />
|
||||
<path
|
||||
d="m 150.71891,130.51633 c 0,-0.65229 0.52882,-1.18146 1.18146,-1.18146 0.65263,0 1.18145,0.52917 1.18145,1.18146 0,0.65263 -0.52882,1.18145 -1.18145,1.18145 -0.65264,0 -1.18146,-0.52882 -1.18146,-1.18145"
|
||||
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path414" />
|
||||
<path
|
||||
d="m 151.90037,131.19507 c 0.37429,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30445,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67875,0.30445 -0.67875,0.67875 0,0.37429 0.30445,0.67874 0.67875,0.67874"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path416" />
|
||||
<path
|
||||
d="m 145.53555,129.58146 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15416,0.34432 -0.34431,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path418" />
|
||||
<path
|
||||
d="m 145.54331,131.35311 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34396,0.15417 0.34396,0.34431 0,0.19015 -0.15381,0.34432 -0.34396,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path420" />
|
||||
<path
|
||||
d="m 149.25735,131.35311 c 0,-0.19014 0.15417,-0.34431 0.34432,-0.34431 0.19014,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15417,0.34432 -0.34431,0.34432 -0.19015,0 -0.34432,-0.15417 -0.34432,-0.34432"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path422" />
|
||||
<path
|
||||
d="m 149.94598,129.58146 c 0,-0.19014 -0.15417,-0.34431 -0.34431,-0.34431 -0.19015,0 -0.34432,0.15417 -0.34432,0.34431 0,0.19015 0.15417,0.34432 0.34432,0.34432 0.19014,0 0.34431,-0.15417 0.34431,-0.34432"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path424" />
|
||||
<path
|
||||
d="m 144.76261,130.51633 c 0,-0.65229 -0.52881,-1.18146 -1.18145,-1.18146 -0.65264,0 -1.18145,0.52917 -1.18145,1.18146 0,0.65263 0.52881,1.18145 1.18145,1.18145 0.65264,0 1.18145,-0.52882 1.18145,-1.18145"
|
||||
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path426" />
|
||||
<path
|
||||
d="m 143.58116,131.19507 c 0.3743,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30444,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67874,0.30445 -0.67874,0.67875 0,0.37429 0.30444,0.67874 0.67874,0.67874"
|
||||
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path428" />
|
||||
<path
|
||||
d="m 143.43652,131.0088 h 8.60848 v -0.98495 h -8.60848 v 0.98495"
|
||||
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path430" />
|
||||
<path
|
||||
d="m 147.23664,128.61168 h 3.55812 c 0.33761,0 0.61172,-0.27376 0.61172,-0.61172 v -0.73236 c 0,-0.72073 0.11465,-2.63737 -0.76377,-3.69429 -0.0744,-0.0896 -0.18485,-0.14147 -0.30092,-0.14147 h -2.60173 c -0.16158,0 -0.29704,0.12277 -0.31256,0.28364 l -0.19086,1.97943 v 2.91677"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path432" />
|
||||
<path
|
||||
d="m 150.76301,128.2649 c 0.16969,0 0.30797,-0.13794 0.30797,-0.30762 v -0.7165 c 0,-0.066 7.1e-4,-0.14217 0.002,-0.22648 0.01,-0.76165 0.03,-2.34562 -0.68192,-3.20216 -0.0173,-0.0212 -0.0437,-0.0335 -0.0709,-0.0335 h -2.54529 c -0.008,0 -0.0155,0.006 -0.0162,0.0148 l -0.16404,1.69827 c -0.0141,0.14464 0.025,0.28928 0.10971,0.40711 l 1.52153,2.11702 c 0.11219,0.15628 0.29316,0.24906 0.48578,0.24906 h 1.05163"
|
||||
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path434" />
|
||||
<path
|
||||
d="m 141.47331,125.91928 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path436" />
|
||||
<path
|
||||
d="m 141.47331,126.34649 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20673 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.1143 0.0582,-0.20673 0.13053,-0.20673"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path438" />
|
||||
<path
|
||||
d="m 141.47331,126.77406 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
|
||||
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||
id="path440" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
99
tmp/index.html
Normal file
99
tmp/index.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Under Construction</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: #f2f2f2;
|
||||
padding: 1rem;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#hero {
|
||||
padding: 4rem;
|
||||
background-color: white;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
#hero img {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.0.4/luxon.min.js" integrity="sha512-XdACFfCJeqqfVU8mvvXReyFR130qjFvfv/PZOFGwVyBz0HC+57fNkSacMPF2Dyek5jqi4D7ykFrx/T7N6F2hwQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p style="font-size:2.5rem;color:#505050">Coming Soon™</p>
|
||||
<div id="hero">
|
||||
<img src="/crane.svg">
|
||||
</div>
|
||||
<p>
|
||||
Under Construction for <br />
|
||||
<span id="counter" style="margin-top:0.5rem"></span>
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function u(v, unit) {
|
||||
if (v === 1) {
|
||||
return `${v} ${unit}`;
|
||||
}
|
||||
else {
|
||||
return `${v} ${unit}s`;
|
||||
}
|
||||
}
|
||||
|
||||
function f(n) {
|
||||
let s = n.toString();
|
||||
if (s.length == 1) {
|
||||
return '0' + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const start = luxon.DateTime.fromSeconds(1634529923);
|
||||
function setDuration() {
|
||||
var diff = luxon.DateTime.now().diff(start);
|
||||
const years = Math.floor(diff.as('years'));
|
||||
diff = diff.minus(luxon.Duration.fromObject({years}));
|
||||
const months = Math.floor(diff.as('months'));
|
||||
diff = diff.minus(luxon.Duration.fromObject({months}));
|
||||
const days = Math.floor(diff.as('days'));
|
||||
diff = diff.minus(luxon.Duration.fromObject({days}));
|
||||
const hours = Math.floor(diff.as('hours'))
|
||||
diff = diff.minus(luxon.Duration.fromObject({hours}));
|
||||
const minutes = Math.floor(diff.as('minutes'));
|
||||
diff = diff.minus(luxon.Duration.fromObject({minutes}));
|
||||
const seconds = Math.floor(diff.as('seconds'));
|
||||
diff = diff.minus(luxon.Duration.fromObject({seconds}));
|
||||
const millis = diff.as('milliseconds');
|
||||
|
||||
const timeString = `${u(years, "year")}, ${u(months, "month")}, ${u(days, "day")}, ${f(hours)}:${f(minutes)}:${f(seconds)}.${Math.floor(millis / 100)}`;
|
||||
document.getElementById('counter').innerHTML = timeString;
|
||||
|
||||
window.setTimeout(setDuration, 10);
|
||||
}
|
||||
setDuration();
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
@ -1,6 +0,0 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Reference in New Issue
Block a user