mirror of
https://github.com/LukeHagar/parke.dev.git
synced 2025-12-06 04:20:34 +00:00
Saving state of blog
This commit is contained in:
95
.gitignore
vendored
Normal file
95
.gitignore
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
.output/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
*~
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.turbo/
|
||||||
|
.next/
|
||||||
|
.nuxt/
|
||||||
|
.vite/
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# Bun
|
||||||
|
.bun/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.tmp/
|
||||||
|
.temp/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Lock files (uncomment if you want to ignore them)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
# bun.lockb
|
||||||
|
|
||||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Blog
|
||||||
|
|
||||||
|
This templates used [Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), and [Skeleton](https://www.skeletonlabs.co/) along with a curated template created and implemented by the the creators of Skeleton.
|
||||||
|
|
||||||
|
## How To Install
|
||||||
|
|
||||||
|
Find installation instructions within the FAQ here:
|
||||||
|
|
||||||
|
https://github.com/skeletonlabs/skeleton-templates
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
This template comes pre-configured with the "Cerberus" theme. See customizations in `/src/styles/global.css`.
|
||||||
|
|
||||||
|
[Learn more about customizing themes here](https://www.skeleton.dev/docs/design/themes).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The following is located in `/src`.
|
||||||
|
|
||||||
|
- `/components` - the local components such as header, footer, etc.
|
||||||
|
- `/content` - The MDX content or blog post and authors.
|
||||||
|
- `/layouts` - The page and blog post layouts.
|
||||||
|
- `/pages` - The individual page templates.
|
||||||
|
- `/styles` - Contains the global stylesheet `globals.css`.
|
||||||
|
- `constants.ts` - Static data shared throughout the application.
|
||||||
|
- `content.conig.ts` - Uses Zod to enable type safety for [Astro Content Collections](https://docs.astro.build/en/reference/modules/astro-content/).
|
||||||
|
|
||||||
|
## Media
|
||||||
|
|
||||||
|
Media such as blog images can be located in the `/public` directory.
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
All rountes can be found in the `/src/routes` directory. This template includes:
|
||||||
|
|
||||||
|
- `/` - the homepage of the website.
|
||||||
|
- `/blog` - a grid list of the blogroll.
|
||||||
|
- `/blog/{postFileName}` - each individual blog post.
|
||||||
|
- `/about` - learn more about the project.
|
||||||
|
- `/sandbox` - a hidden sandbox page for testing theme styles.
|
||||||
|
|
||||||
|
## Testing Sandbox
|
||||||
|
|
||||||
|
A hidden sandbox page has been provided at `/sandbox`. This allows you to quickly preview and test various Skeleton elements and components, including: typography, buttons, the color palette, and more. This route can be deleted at your own discretion.
|
||||||
|
|
||||||
|
## Additional Assets
|
||||||
|
|
||||||
|
- Icons from https://lucide.dev/
|
||||||
|
- Images from Unsplash https://unsplash.com/
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This template is served under the terms of the [Personal License](https://v2.skeleton.dev/docs/sponsorship/licensing). Contact [Skeleton Labs](mailto:admin@skeletonlabs.dev) if you are interested in obtaining either a Commercial or Enterprise license.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `pnpm install` | Installs dependencies |
|
||||||
|
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `pnpm build` | Build your production site to `./dist/` |
|
||||||
|
| `pnpm preview` | Preview your build locally, before deploying |
|
||||||
|
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `pnpm astro -- --help` | Get help using the Astro CLI |
|
||||||
45
astro.config.mjs
Normal file
45
astro.config.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
|
// Integrations
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
import svelte from "@astrojs/svelte";
|
||||||
|
import AutoImport from "astro-auto-import";
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
import expressiveCode from "astro-expressive-code";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://example.com",
|
||||||
|
integrations: [
|
||||||
|
// https://docs.astro.build/en/guides/integrations-guide/sitemap/
|
||||||
|
sitemap(),
|
||||||
|
// https://docs.astro.build/en/guides/integrations-guide/svelte/
|
||||||
|
svelte(),
|
||||||
|
// https://expressive-code.com/
|
||||||
|
expressiveCode({
|
||||||
|
defaultProps: { wrap: true },
|
||||||
|
themes: ["dark-plus", "github-dark"],
|
||||||
|
}),
|
||||||
|
// https://github.com/delucis/astro-auto-import/tree/main/packages/astro-auto-import
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
{
|
||||||
|
// The following translates to:
|
||||||
|
// import componentSet from "@components/mdx/index";
|
||||||
|
"@components/mdx/index": [["default", "componentSet"]],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// IMPORTANT: MUST BE LAST INTEGRATION
|
||||||
|
// https://docs.astro.build/en/guides/integrations-guide/mdx/
|
||||||
|
mdx(),
|
||||||
|
],
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
});
|
||||||
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "parke.dev",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.2.0",
|
||||||
|
"@astrojs/rss": "^4.0.11",
|
||||||
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
|
"@astrojs/svelte": "^7.0.6",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"astro": "^5.5.2",
|
||||||
|
"astro-auto-import": "^0.4.4",
|
||||||
|
"astro-expressive-code": "^0.41.0",
|
||||||
|
"simple-icons": "^15.21.0",
|
||||||
|
"svelte": "^5.39.10",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@lucide/svelte": "^0.552.0",
|
||||||
|
"@skeletonlabs/skeleton": "next",
|
||||||
|
"@skeletonlabs/skeleton-svelte": "next",
|
||||||
|
"@tailwindcss/forms": "^0.5.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/IMG_9580.jpg
Normal file
BIN
public/IMG_9580.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/IMG_9586.jpg
Normal file
BIN
public/IMG_9586.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
BIN
public/fonts/atkinson-bold.woff
Normal file
BIN
public/fonts/atkinson-bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/atkinson-regular.woff
Normal file
BIN
public/fonts/atkinson-regular.woff
Normal file
Binary file not shown.
20
src/components/Avatar.svelte
Normal file
20
src/components/Avatar.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar } from "@skeletonlabs/skeleton-svelte";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
image: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { image, name }: Props = $props();
|
||||||
|
|
||||||
|
const initials = name
|
||||||
|
.split(" ")
|
||||||
|
.map((name) => name[0])
|
||||||
|
.join("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Avatar class="size-16">
|
||||||
|
<Avatar.Image src={image} alt={name} />
|
||||||
|
<Avatar.Fallback>{initials}</Avatar.Fallback>
|
||||||
|
</Avatar>
|
||||||
73
src/components/BaseHead.astro
Normal file
73
src/components/BaseHead.astro
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
// Import the global.css file here so that it is included on
|
||||||
|
// all pages through the use of the <BaseHead /> component.
|
||||||
|
import "@styles/global.css";
|
||||||
|
import { SITE_TITLE } from "@src/consts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
author: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
author,
|
||||||
|
image = "/blog-placeholder-1.jpg",
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Global Metadata -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title={SITE_TITLE}
|
||||||
|
href={new URL("rss.xml", Astro.site)}
|
||||||
|
/>
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<!-- Font preloads -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/atkinson-regular.woff"
|
||||||
|
as="font"
|
||||||
|
type="font/woff"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/atkinson-bold.woff"
|
||||||
|
as="font"
|
||||||
|
type="font/woff"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={Astro.url} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content={Astro.url} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||||
28
src/components/Footer.astro
Normal file
28
src/components/Footer.astro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { socialLinks } from "@src/data/social-links";
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="section-padding border-surface-500/10 border-t py-5">
|
||||||
|
<div class="container mx-auto flex flex-col items-center gap-5">
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
{
|
||||||
|
socialLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
class="btn btn-sm hover:preset-tonal-tertiary"
|
||||||
|
href={link.url}
|
||||||
|
target={link.target}
|
||||||
|
rel="noreferrer"
|
||||||
|
title={link.title}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs opacity-50">
|
||||||
|
© Copyright {currentYear}. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
17
src/components/FormattedDate.astro
Normal file
17
src/components/FormattedDate.astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { date } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<time datetime={date.toISOString()}>
|
||||||
|
{
|
||||||
|
date.toLocaleDateString('en-us', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</time>
|
||||||
41
src/components/Header.svelte
Normal file
41
src/components/Header.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// Skeleton
|
||||||
|
import { AppBar } from "@skeletonlabs/skeleton-svelte";
|
||||||
|
// Icons
|
||||||
|
import IconLogo from "@lucide/svelte/icons/rocket";
|
||||||
|
// Utils
|
||||||
|
import { SITE_TITLE } from "@src/consts";
|
||||||
|
|
||||||
|
// For best results limit to 3-5 options max
|
||||||
|
const navigation = [
|
||||||
|
{ label: "Blog", href: "/" },
|
||||||
|
{ label: "Posts", href: "/posts" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 border-b border-surface-100-900 bg-surface-50-950/80 backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<AppBar class="bg-transparent container mx-auto">
|
||||||
|
<AppBar.Toolbar class="grid-cols-[auto_1fr_auto] gap-10">
|
||||||
|
<AppBar.Lead>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="btn-icon md:btn-icon-lg from-secondary-500 to-tertiary-500 rounded-full bg-linear-to-tr text-white"
|
||||||
|
title={SITE_TITLE}
|
||||||
|
>
|
||||||
|
<IconLogo />
|
||||||
|
</a>
|
||||||
|
</AppBar.Lead>
|
||||||
|
<AppBar.Headline>
|
||||||
|
<nav class="flex gap-2">
|
||||||
|
{#each navigation as route}
|
||||||
|
<a class="btn hover:preset-tonal-tertiary" href={route.href}>
|
||||||
|
{route.label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
</AppBar.Headline>
|
||||||
|
</AppBar.Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
</header>
|
||||||
12
src/components/Icon.astro
Normal file
12
src/components/Icon.astro
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
icon: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icon, ...attributes } = Astro.props as Props;
|
||||||
|
/* @vite-ignore */
|
||||||
|
const { default: innerHTML } = await import(`/src/svg/${icon}.svg?raw`);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Fragment {...attributes} set:html={innerHTML} />
|
||||||
153
src/components/Scroller.svelte
Normal file
153
src/components/Scroller.svelte
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
const texts = ["OpenAPI", "Food", "AI", "Cats", "Developer Tools"];
|
||||||
|
|
||||||
|
type Slot = {
|
||||||
|
id: string;
|
||||||
|
content: string; // "old\n\nnew" or just "char"
|
||||||
|
index: number; // 0 → show old row, 1 → show new row
|
||||||
|
changed: boolean;
|
||||||
|
position: number; // slot position (0..longestLength-1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentIndex = $state(0);
|
||||||
|
let currentText = $state(texts[0] ?? "");
|
||||||
|
let previousText = $state("");
|
||||||
|
let slots = $state<Slot[]>([]);
|
||||||
|
let animationKey = $state(0);
|
||||||
|
|
||||||
|
// longest string length across all texts
|
||||||
|
const longestLength = Math.max(...texts.map((t) => t.length));
|
||||||
|
|
||||||
|
// timing & easing (very similar to your example)
|
||||||
|
const charDuration = 450;
|
||||||
|
const intervalInMs = `${charDuration}ms`;
|
||||||
|
const ease = "cubic-bezier(1, 0, 0, 1)";
|
||||||
|
const baseDelay = 40; // global offset
|
||||||
|
const perLetterDelay = 25; // stagger between letters
|
||||||
|
|
||||||
|
function updateSlots(newText: string, oldText: string) {
|
||||||
|
const next: Slot[] = [];
|
||||||
|
|
||||||
|
const newLen = newText.length;
|
||||||
|
const oldLen = oldText.length;
|
||||||
|
|
||||||
|
// center each word inside the fixed board
|
||||||
|
const newOffset = Math.floor((longestLength - newLen) / 2);
|
||||||
|
const oldOffset = Math.floor((longestLength - oldLen) / 2);
|
||||||
|
|
||||||
|
for (let pos = 0; pos < longestLength; pos++) {
|
||||||
|
const newIdx = pos - newOffset;
|
||||||
|
const oldIdx = pos - oldOffset;
|
||||||
|
|
||||||
|
const newChar = newIdx >= 0 && newIdx < newLen ? newText[newIdx] : " ";
|
||||||
|
const oldChar =
|
||||||
|
oldText && oldIdx >= 0 && oldIdx < oldLen ? oldText[oldIdx] : " ";
|
||||||
|
|
||||||
|
// only animate if we have an old string and the char actually changed
|
||||||
|
const changed = !!oldText && newChar !== oldChar;
|
||||||
|
|
||||||
|
const content = changed
|
||||||
|
? `${oldChar || " "}\n\n${newChar || " "}`
|
||||||
|
: newChar || " ";
|
||||||
|
|
||||||
|
next.push({
|
||||||
|
id: `${pos}-${newChar}-${animationKey}`,
|
||||||
|
content,
|
||||||
|
index: 0, // always start at row 0 (old char)
|
||||||
|
changed,
|
||||||
|
position: pos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
slots = next;
|
||||||
|
|
||||||
|
// Don’t animate the very first render
|
||||||
|
if (!oldText) return;
|
||||||
|
|
||||||
|
// Kick the index to 1 on the next frame so CSS can animate top → smooth scroll
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
slots = slots.map((slot) =>
|
||||||
|
slot.changed && slot.content.includes("\n\n")
|
||||||
|
? { ...slot, index: 1 }
|
||||||
|
: slot
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild slots whenever the text changes
|
||||||
|
$effect(() => {
|
||||||
|
updateSlots(currentText, previousText);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (texts.length <= 1) return;
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
previousText = currentText;
|
||||||
|
currentIndex = (currentIndex + 1) % texts.length;
|
||||||
|
currentText = texts[currentIndex];
|
||||||
|
animationKey++;
|
||||||
|
tick();
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="ticker" aria-live="polite">
|
||||||
|
{#each slots as slot (slot.id)}
|
||||||
|
<span class="slot">
|
||||||
|
<span
|
||||||
|
class="slot-inner"
|
||||||
|
style="
|
||||||
|
--index: {slot.index};
|
||||||
|
--interval: {intervalInMs};
|
||||||
|
--ease: {ease};
|
||||||
|
--delay: {baseDelay + slot.position * perLetterDelay}ms;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>{slot.content}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ticker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: monospace;
|
||||||
|
/* font-size: clamp(2rem, 6vw, 4rem); */
|
||||||
|
gap: 0.08em; /* spacing between letters */
|
||||||
|
/* optional: make it obvious this is fixed width */
|
||||||
|
/* border: 1px dashed red; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-inner {
|
||||||
|
height: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sliding-text trick, per letter */
|
||||||
|
.slot-inner > span {
|
||||||
|
white-space: pre;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
top: calc(var(--index) * -2em); /* 0 → old row, 1 → new row */
|
||||||
|
transition: top var(--interval) var(--ease);
|
||||||
|
transition-delay: var(--delay);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
src/components/mdx/Anchor.astro
Normal file
5
src/components/mdx/Anchor.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a class="anchor" {...props}><slot /></a>
|
||||||
5
src/components/mdx/Blockquote.astro
Normal file
5
src/components/mdx/Blockquote.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<blockquote class="blockquote" {...props}><slot /></blockquote>
|
||||||
5
src/components/mdx/Code.astro
Normal file
5
src/components/mdx/Code.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<code class="code" {...props}><slot /></code>
|
||||||
5
src/components/mdx/Heading1.astro
Normal file
5
src/components/mdx/Heading1.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 class="h1" {...props}><slot /></h1>
|
||||||
5
src/components/mdx/Heading2.astro
Normal file
5
src/components/mdx/Heading2.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h2 class="h2 scroll-header" {...props}><slot /></h2>
|
||||||
5
src/components/mdx/Heading3.astro
Normal file
5
src/components/mdx/Heading3.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h3 class="h3 scroll-header" {...props}><slot /></h3>
|
||||||
5
src/components/mdx/Heading4.astro
Normal file
5
src/components/mdx/Heading4.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h3 class="h4 scroll-header" {...props}><slot /></h3>
|
||||||
5
src/components/mdx/Heading5.astro
Normal file
5
src/components/mdx/Heading5.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h5 class="h5 scroll-header" {...props}><slot /></h5>
|
||||||
5
src/components/mdx/Heading6.astro
Normal file
5
src/components/mdx/Heading6.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h6 class="h6 scroll-header" {...props}><slot /></h6>
|
||||||
5
src/components/mdx/HorzRule.astro
Normal file
5
src/components/mdx/HorzRule.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<hr class="hr" {...props} />
|
||||||
7
src/components/mdx/ListOrdered.astro
Normal file
7
src/components/mdx/ListOrdered.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ol class="translate-x-5 list-outside list-decimal space-y-2" {...props}>
|
||||||
|
<slot />
|
||||||
|
</ol>
|
||||||
7
src/components/mdx/ListUnordered.astro
Normal file
7
src/components/mdx/ListUnordered.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul class="translate-x-5 list-outside list-disc space-y-2" {...props}>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
9
src/components/mdx/Table.astro
Normal file
9
src/components/mdx/Table.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
const props = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="table" {...props}>
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
35
src/components/mdx/index.ts
Normal file
35
src/components/mdx/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// MDX Replacement Components
|
||||||
|
// https://docs.astro.build/en/guides/markdown-content/#assigning-custom-components-to-html-elements
|
||||||
|
// https://mdxjs.com/table-of-components/
|
||||||
|
|
||||||
|
import Heading1 from '@components/mdx/Heading1.astro';
|
||||||
|
import Heading2 from '@components/mdx/Heading2.astro';
|
||||||
|
import Heading3 from '@components/mdx/Heading3.astro';
|
||||||
|
import Heading4 from '@components/mdx/Heading4.astro';
|
||||||
|
import Heading5 from '@components/mdx/Heading5.astro';
|
||||||
|
import Heading6 from '@components/mdx/Heading6.astro';
|
||||||
|
import Anchor from '@components/mdx/Anchor.astro';
|
||||||
|
import Blockquote from '@components/mdx/Blockquote.astro';
|
||||||
|
import ListUnordered from '@components/mdx/ListUnordered.astro';
|
||||||
|
import ListOrdered from '@components/mdx/ListOrdered.astro';
|
||||||
|
import Code from '@components/mdx/Code.astro';
|
||||||
|
import HorzRule from '@components/mdx/HorzRule.astro';
|
||||||
|
import Table from '@components/mdx/Table.astro';
|
||||||
|
|
||||||
|
const componentSet = {
|
||||||
|
h1: Heading1,
|
||||||
|
h2: Heading2,
|
||||||
|
h3: Heading3,
|
||||||
|
h4: Heading4,
|
||||||
|
h5: Heading5,
|
||||||
|
h6: Heading6,
|
||||||
|
a: Anchor,
|
||||||
|
blockquote: Blockquote,
|
||||||
|
ul: ListUnordered,
|
||||||
|
ol: ListOrdered,
|
||||||
|
code: Code,
|
||||||
|
hr: HorzRule,
|
||||||
|
table: Table
|
||||||
|
};
|
||||||
|
|
||||||
|
export default componentSet;
|
||||||
6
src/consts.ts
Normal file
6
src/consts.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Place any global data in this file.
|
||||||
|
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||||
|
|
||||||
|
export const SITE_TITLE = "Blog";
|
||||||
|
export const SITE_DESCRIPTION = "Welcome to my blog!";
|
||||||
|
export const SITE_AUTHOR = "Luke Parke";
|
||||||
38
src/content.config.ts
Normal file
38
src/content.config.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { glob } from "astro/loaders";
|
||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||||
|
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
|
||||||
|
// Type-check frontmatter using a schema
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string().optional().default("luke-parke"),
|
||||||
|
topic: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
// Transform string to Date object
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const authors = defineCollection({
|
||||||
|
// Load Markdown and MDX files in the `src/content/authors/` directory.
|
||||||
|
loader: glob({ base: "./src/content/authors", pattern: "**/*.{md,mdx}" }),
|
||||||
|
// Type-check frontmatter using a schema
|
||||||
|
schema: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
portrait: z.string(),
|
||||||
|
// bio: z.string().optional(),
|
||||||
|
location: z.string(),
|
||||||
|
website: z.string(),
|
||||||
|
blusky: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, authors };
|
||||||
10
src/content/authors/luke-parke.md
Normal file
10
src/content/authors/luke-parke.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
id: "luke-parke"
|
||||||
|
name: Luke Parke
|
||||||
|
title: Software Engineer
|
||||||
|
portrait: "IMG_9586.jpg"
|
||||||
|
location: "Austin, Texas"
|
||||||
|
website: "https://parke.dev"
|
||||||
|
blusky: "@lukehagar"
|
||||||
|
email: luke@parke.dev
|
||||||
|
---
|
||||||
46
src/content/blog/Coolify-homelab.mdx
Normal file
46
src/content/blog/Coolify-homelab.mdx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "Coolify is the best thing for Homelabs since Fiber"
|
||||||
|
description: "Deploying self hosted services with Coolify"
|
||||||
|
topic: "Development"
|
||||||
|
tags: ["Coolify", "Homelab", "SelfHosting", "WebDev"]
|
||||||
|
author: "luke-parke"
|
||||||
|
pubDate: "Nov 14 2025"
|
||||||
|
---
|
||||||
|
|
||||||
|
I've been using [Coolify](https://coolify.io) in my Homelab for a couple years now, and I have never enjoyed self hosting this much before.
|
||||||
|
|
||||||
|
Gone are the days of manually managing all of your subdomains and certificates in [Cloudflare](https://thehomelab.wiki/books/dns-reverse-proxy/page/setup-your-domain-with-cloudflare) and [Nginx Proxy Manager](https://nginxproxymanager.com), just setup a wildcard, and Coolify will handle the rest.
|
||||||
|
|
||||||
|
But it doesn't just handle SSL and domain management — it's a full open-source PaaS (Platform as a Service) that makes deploying apps, databases, and services effortless.
|
||||||
|
|
||||||
|
## Why Coolify?
|
||||||
|
|
||||||
|
With Coolify you can:
|
||||||
|
|
||||||
|
- **Deploy any app or service** — from static sites and APIs to full-stack apps built with Next.js, Remix, Nuxt, or SvelteKit. You can even spin up databases, WordPress, Ghost, Plausible Analytics, and more in just a couple clicks.
|
||||||
|
- **Run it anywhere** — your own VPS, Raspberry Pi, Hetzner, AWS, or even a laptop. If it supports Docker, Coolify can manage it.
|
||||||
|
- **Push to deploy** — connect a GitHub, GitLab, or Bitbucket repo and every push can auto-deploy. Pair it with pull request previews and you've got a smooth workflow for testing before merging.
|
||||||
|
- **Stay secure automatically** — free Let's Encrypt SSL for all your custom domains, with automatic renewals.
|
||||||
|
- **Keep your data safe** — one-click database backups to any S3-compatible storage.
|
||||||
|
- **Collaborate with ease** — invite teammates, assign roles, and share access safely.
|
||||||
|
|
||||||
|
And Coolify is fully self-hosted. That means **no vendor lock-in**, full control of your data, and the ability to move servers or providers whenever you like.
|
||||||
|
|
||||||
|
## Performance and Experience
|
||||||
|
|
||||||
|
Performance has been excellent in my Homelab — deployments finish fast, the real-time terminal is responsive, and monitoring tools keep me aware of server health and disk usage at a glance.
|
||||||
|
|
||||||
|
I have deployed a number of personal sites, projects, and other services through it for domains I own.
|
||||||
|
|
||||||
|
Some examples:
|
||||||
|
- [LukeHagar.com](https://lukehagar.com)
|
||||||
|
- [OpenAPI Definition Generator](https://oas-def-gen.lukehagar.com)
|
||||||
|
- [OpenAPI.gg](https://openapi.gg)
|
||||||
|
- [pypistats.dev](https://pypistats.dev)
|
||||||
|
|
||||||
|
|
||||||
|
If you're looking for a way to simplify your self-hosting setup without giving up control, I can't recommend Coolify enough.
|
||||||
|
|
||||||
|
It's been rock solid for me, and I'm genuinely excited to see where the project goes as it continues to evolve (Direct Multi-Server support is confirmed on the roadmap 👀).
|
||||||
|
|
||||||
|
👉 Check it out here: [https://coolify.io](https://coolify.io)
|
||||||
48
src/content/blog/SvelteKit-node-servers.mdx
Normal file
48
src/content/blog/SvelteKit-node-servers.mdx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "Easier deployment of SvelteKit node servers with docker"
|
||||||
|
description: "How to deploy SvelteKit node servers with docker"
|
||||||
|
topic: "Development"
|
||||||
|
tags: ["SvelteKit", "Svelte", "Docker", "Node.js"]
|
||||||
|
author: "luke-parke"
|
||||||
|
pubDate: "Nov 14 2025"
|
||||||
|
---
|
||||||
|
|
||||||
|
export const components = componentSet;
|
||||||
|
|
||||||
|
When you are NOT deploying to a nice and simple platform like Vercel, you may be doing things a little more manually.
|
||||||
|
|
||||||
|
For SvelteKit that is often really easy, tooling like nixpacks allows for a pretty much seamless deployment of most any application anywhere.
|
||||||
|
|
||||||
|
I was working on a simple little test app to explore the functionality of how Coolify and SvelteKit both handles subdomains and I was getting a node version mismatch error due to a dep requiring one of a few specific node versions.
|
||||||
|
|
||||||
|
Due to this I had to go the route of deploying with Docker instead, so here is a bare minimum Dockerfile for SvelteKit should anyone need one in the future.
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# ---- Build stage ----
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies (cache-friendly)
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source and build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Run stage ----
|
||||||
|
FROM node:24-alpine AS run
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the built app
|
||||||
|
COPY --from=build /app/package.json ./
|
||||||
|
COPY --from=build /app/build ./build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "build"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Cheers
|
||||||
23
src/data/social-links.ts
Normal file
23
src/data/social-links.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type SocialLink = {
|
||||||
|
title: "Bluesky" | "GitHub" | "LinkedIn";
|
||||||
|
url: string;
|
||||||
|
target?: "_blank";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const socialLinks: SocialLink[] = [
|
||||||
|
{
|
||||||
|
title: "Bluesky",
|
||||||
|
url: "https://bsky.app/profile/lukehagar.com",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "GitHub",
|
||||||
|
url: "https://github.com/lukehagar",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LinkedIn",
|
||||||
|
url: "https://linkedin.com/in/lukehagar",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
];
|
||||||
126
src/layouts/BlogPost.astro
Normal file
126
src/layouts/BlogPost.astro
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
import LayoutRoot from "@layouts/LayoutRoot.astro";
|
||||||
|
import FormattedDate from "@components/FormattedDate.astro";
|
||||||
|
import Avatar from "@components/Avatar.svelte";
|
||||||
|
import { getEntry } from "astro:content";
|
||||||
|
|
||||||
|
type BlogMetaData = CollectionEntry<"blog">["data"];
|
||||||
|
|
||||||
|
interface Props extends BlogMetaData {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, pubDate, updatedDate, heroImage, content, author } =
|
||||||
|
Astro.props;
|
||||||
|
|
||||||
|
// https://dev.to/michaelburrows/calculate-the-estimated-reading-time-of-an-article-using-javascript-2k9l
|
||||||
|
function readingTime() {
|
||||||
|
const text = content;
|
||||||
|
const wpm = 150;
|
||||||
|
const words = text.trim().split(/\s+/).length;
|
||||||
|
const time = Math.ceil(words / wpm);
|
||||||
|
const minutePlural = time > 1 ? "s" : "";
|
||||||
|
return `${time} min${minutePlural}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorEntry = (await getEntry("authors", author || "luke-parke"))!.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<LayoutRoot
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
author={author}
|
||||||
|
pubDate={new Date()}
|
||||||
|
>
|
||||||
|
<article>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="p-5 md:p-10">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<h1 class="h1">{title}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<hr class="hr container mx-auto" />
|
||||||
|
|
||||||
|
<!-- Metadata -->
|
||||||
|
<section class="p-4 md:p-10">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 md:gap-20"
|
||||||
|
>
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
<!-- Author -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs">Authors</p>
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<p class="text-2xl font-bold">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
client:load
|
||||||
|
image={authorEntry?.portrait}
|
||||||
|
name={authorEntry?.name}
|
||||||
|
/>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-lg font-bold">{authorEntry?.name}</p>
|
||||||
|
<p class="text-xs opacity-75">
|
||||||
|
{authorEntry?.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Published -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs">Published</p>
|
||||||
|
<time class="text-2xl font-bold">
|
||||||
|
<FormattedDate date={pubDate} />
|
||||||
|
{
|
||||||
|
updatedDate && (
|
||||||
|
<div class="text-xs opacity-75">
|
||||||
|
Last updated on <FormattedDate date={updatedDate} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<!-- Reading Time -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs">Reading Time</p>
|
||||||
|
<p class="text-2xl font-bold">
|
||||||
|
{readingTime()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Hero Banner -->
|
||||||
|
{
|
||||||
|
heroImage && (
|
||||||
|
<section class="preset-filled-surface-950-50 p-5 md:p-10">
|
||||||
|
<div class="container mx-auto flex justify-center items-center">
|
||||||
|
{/* Replace `w-ull` with `max-w-full` if you prefer smaller images */}
|
||||||
|
<img
|
||||||
|
width={960}
|
||||||
|
height={480}
|
||||||
|
src={heroImage}
|
||||||
|
alt={title}
|
||||||
|
class="bg-surface-500 aspect-video w-full rounded-container overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Slot -->
|
||||||
|
<div class="container mx-auto space-y-8 px-4 py-10">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</LayoutRoot>
|
||||||
33
src/layouts/LayoutRoot.astro
Normal file
33
src/layouts/LayoutRoot.astro
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
import { SITE_TITLE, SITE_DESCRIPTION, SITE_AUTHOR } from "@src/consts";
|
||||||
|
|
||||||
|
import BaseHead from "@components/BaseHead.astro";
|
||||||
|
import Header from "@components/Header.svelte";
|
||||||
|
import Footer from "@components/Footer.astro";
|
||||||
|
|
||||||
|
type Props = CollectionEntry<"blog">["data"];
|
||||||
|
|
||||||
|
const { title, description, author } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="cerberus" class="dark">
|
||||||
|
<head>
|
||||||
|
<BaseHead
|
||||||
|
title={title || SITE_TITLE}
|
||||||
|
description={description || SITE_DESCRIPTION}
|
||||||
|
author={author || SITE_AUTHOR}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grid min-h-screen grid-rows-[auto_1fr_auto]">
|
||||||
|
<Header client:load />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
212
src/pages/index.astro
Normal file
212
src/pages/index.astro
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
import LayoutRoot from "@layouts/LayoutRoot.astro";
|
||||||
|
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import FormattedDate from "@components/FormattedDate.astro";
|
||||||
|
import Scroller from "@components/Scroller.svelte";
|
||||||
|
import LinkedIn from "@svg/linkedin.svg";
|
||||||
|
import Bluesky from "@svg/bluesky.svg";
|
||||||
|
import GitHub from "@svg/github.svg";
|
||||||
|
import { socialLinks } from "@src/data/social-links";
|
||||||
|
|
||||||
|
const posts = (await getCollection("blog")).sort(
|
||||||
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||||
|
);
|
||||||
|
const authors = (await getCollection("authors")).sort(
|
||||||
|
// Alphabetical by id
|
||||||
|
(a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
Bluesky,
|
||||||
|
GitHub,
|
||||||
|
LinkedIn,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const socialLinksWithIcons = socialLinks.map((link) => ({
|
||||||
|
...link,
|
||||||
|
Icon: iconMap[link.title],
|
||||||
|
}));
|
||||||
|
---
|
||||||
|
|
||||||
|
<LayoutRoot title="" description="" author="" pubDate={new Date()}>
|
||||||
|
<div>
|
||||||
|
<!-- DEBUG -->
|
||||||
|
<!-- <pre class="pre">{JSON.stringify(posts, null, 2)}</pre> -->
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="flex justify-center items-center px-4 py-5 md:py-10">
|
||||||
|
<!-- Responsive Title -->
|
||||||
|
<!-- NOTE: you may need to adjust the font-size based on the number of characters -->
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-6 text-4xl sm:text-6xl xl:text-8xl 2xl:text-9xl justify-center uppercase font-mono font-bold"
|
||||||
|
>
|
||||||
|
<p class="text-center">A Blog about</p>
|
||||||
|
<Scroller client:load />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<hr class="hr container mx-auto" />
|
||||||
|
|
||||||
|
<!-- Featured -->
|
||||||
|
<section class="p-4 md:py-10">
|
||||||
|
<div class="container mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<!-- The Top Post -->
|
||||||
|
<a
|
||||||
|
class="card border border-surface-100-900 hover:preset-tonal overflow-hidden"
|
||||||
|
href={`/blog/${posts[0].id}/`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
posts[0].data.heroImage && (
|
||||||
|
<img
|
||||||
|
class="w-full aspect-video rounded-container"
|
||||||
|
src={posts[0].data.heroImage}
|
||||||
|
alt="Portfolio"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<article class="p-4 space-y-4">
|
||||||
|
<time class="text-xs opacity-75">
|
||||||
|
<FormattedDate date={posts[0].data.pubDate} />
|
||||||
|
</time>
|
||||||
|
<h2 class="h2">{posts[0].data.title}</h2>
|
||||||
|
<p class="opacity-75">{posts[0].data.description}</p>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
<!-- Additional Featured Posts -->
|
||||||
|
<div class="grid grid-cols-1 gap-4 place-content-start">
|
||||||
|
<!-- Loop Posts 2-4 -->
|
||||||
|
{
|
||||||
|
posts.slice(1, 4).map((post) => (
|
||||||
|
<a
|
||||||
|
class="card border border-surface-100-900 hover:preset-tonal overflow-hidden"
|
||||||
|
href={`/blog/${post.id}/`}
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
class={`grid grid-cols-1 ${post.data.heroImage ? "sm:grid-cols-[280px_1fr]" : undefined}`}
|
||||||
|
>
|
||||||
|
{post.data.heroImage && (
|
||||||
|
<img
|
||||||
|
class="w-full aspect-video"
|
||||||
|
src={post.data.heroImage}
|
||||||
|
alt="Portfolio"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div class="space-y-2 p-4">
|
||||||
|
<time class="text-xs opacity-75">
|
||||||
|
<FormattedDate date={post.data.pubDate} />
|
||||||
|
</time>
|
||||||
|
<h2 class="h2">{post.data.title}</h2>
|
||||||
|
<p class="opacity-75">{posts[0].data.description}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Latest -->
|
||||||
|
<section class="preset-filled-surface-950-50 p-5 md:p-10">
|
||||||
|
<div class="container mx-auto space-y-4 md:space-y-10">
|
||||||
|
<h2 class="h3">Latest Posts</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{
|
||||||
|
posts.slice(1, 5).map((post) => (
|
||||||
|
<a
|
||||||
|
class="card border border-surface-900-100 hover:bg-surface-900-100 overflow-hidden"
|
||||||
|
href={`/blog/${post.id}/`}
|
||||||
|
>
|
||||||
|
{post.data.heroImage && (
|
||||||
|
<img
|
||||||
|
class="w-full aspect-video"
|
||||||
|
src={post.data.heroImage}
|
||||||
|
alt="Portfolio"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<article class="p-4 space-y-2">
|
||||||
|
<time class="block text-base">
|
||||||
|
<FormattedDate date={post.data.pubDate} />
|
||||||
|
</time>
|
||||||
|
<h3 class="h3">{post.data.title}</h3>
|
||||||
|
<p class="opacity-75">{posts[0].data.description}</p>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Authors / Categories -->
|
||||||
|
<section class="preset-filled-primary-500 p-5 md:p-10">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 lg:grid-cols-[1fr_2fr] items-center gap-10 lg:gap-20"
|
||||||
|
>
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h2 class="h2">Who am I?</h2>
|
||||||
|
<p class="">
|
||||||
|
Hi, I'm Luke Parke
|
||||||
|
<br />
|
||||||
|
I am a husband and father, a native Austinite, and a software engineer.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
And a few other things too:
|
||||||
|
<ul class="list-inside list-disc space-y-2">
|
||||||
|
<li>A lover of Svelte and SvelteKit</li>
|
||||||
|
<li>A fan of OpenAPI</li>
|
||||||
|
<li>A user of Dokploy</li>
|
||||||
|
<li>And a massive nerd</li>
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<p>Feel free to follow me on your platform of choice:</p>
|
||||||
|
<div class="flex items-center gap-2 divide-x divide-black">
|
||||||
|
{
|
||||||
|
socialLinksWithIcons.map(({ Icon, ...link }) => (
|
||||||
|
<a
|
||||||
|
class="flex items-center gap-2 px-2"
|
||||||
|
href={link.url}
|
||||||
|
target={link.target}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{Icon && <Icon class="size-6 fill-white" />}
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Authors -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-10">
|
||||||
|
{
|
||||||
|
authors.map((author) => (
|
||||||
|
<a
|
||||||
|
class="text-center space-y-2"
|
||||||
|
href={author.data.website}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="w-full rounded-full mx-auto overflow-hidden"
|
||||||
|
src={author.data.portrait}
|
||||||
|
alt={author.data.id}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 class="h2 md:h3">{author.data.name}</h4>
|
||||||
|
<p class="opacity-75">{author.data.title}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</LayoutRoot>
|
||||||
21
src/pages/posts/[...slug].astro
Normal file
21
src/pages/posts/[...slug].astro
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import BlogPost from "@layouts/BlogPost.astro";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("blog");
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: { slug: post.id },
|
||||||
|
props: post,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
type Props = CollectionEntry<"blog">;
|
||||||
|
|
||||||
|
const post = Astro.props;
|
||||||
|
const { Content } = await render(post);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BlogPost {...post.data} content={post.body ?? ""}>
|
||||||
|
<Content />
|
||||||
|
</BlogPost>
|
||||||
70
src/pages/posts/index.astro
Normal file
70
src/pages/posts/index.astro
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import LayoutRoot from "@layouts/LayoutRoot.astro";
|
||||||
|
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import FormattedDate from "@components/FormattedDate.astro";
|
||||||
|
|
||||||
|
const posts = (await getCollection("blog")).sort(
|
||||||
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<LayoutRoot title="" description="" author="luke-parke" pubDate={new Date()}>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="flex justify-center items-center px-4 py-5 md:py-10">
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text -->
|
||||||
|
<svg
|
||||||
|
class="container mx-auto w-full"
|
||||||
|
viewBox="0 0 1750 280"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
font-size={300}
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
dominant-baseline="central"
|
||||||
|
text-anchor="middle"
|
||||||
|
class="fill-surface-950-50 uppercase font-bold"
|
||||||
|
>
|
||||||
|
ALL POSTS
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<hr class="hr container mx-auto" />
|
||||||
|
|
||||||
|
<!-- Articles -->
|
||||||
|
<nav
|
||||||
|
class="container mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4 px-4 md:px-0 py-4 md:py-10"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
posts.map((post) => (
|
||||||
|
<a
|
||||||
|
href={`/blog/${post.id}/`}
|
||||||
|
class="card border border-surface-100-900 hover:preset-tonal overflow-hidden"
|
||||||
|
>
|
||||||
|
{post.data.heroImage && (
|
||||||
|
<img
|
||||||
|
width={720}
|
||||||
|
height={360}
|
||||||
|
src={post.data.heroImage}
|
||||||
|
alt={post.data.title}
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<article class="grid grid-cols-1 gap-4 p-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-xs opacity-75">
|
||||||
|
<FormattedDate date={post.data.pubDate} />
|
||||||
|
</p>
|
||||||
|
<h2 class="h2">{post.data.title}</h2>
|
||||||
|
<p class="opacity-75">{post.data.description}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</LayoutRoot>
|
||||||
16
src/pages/rss.xml.js
Normal file
16
src/pages/rss.xml.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import rss from "@astrojs/rss";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import { SITE_TITLE, SITE_DESCRIPTION } from "@src/consts";
|
||||||
|
|
||||||
|
export async function GET(context) {
|
||||||
|
const posts = await getCollection("blog");
|
||||||
|
return rss({
|
||||||
|
title: SITE_TITLE,
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
site: context.site,
|
||||||
|
items: posts.map((post) => ({
|
||||||
|
...post.data,
|
||||||
|
link: `/blog/${post.id}/`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
16
src/styles/global.css
Normal file
16
src/styles/global.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin '@tailwindcss/forms';
|
||||||
|
|
||||||
|
@import "@skeletonlabs/skeleton";
|
||||||
|
@import "@skeletonlabs/skeleton/themes/cerberus";
|
||||||
|
|
||||||
|
@import '@skeletonlabs/skeleton-svelte';
|
||||||
|
|
||||||
|
/* Theme Overrides --- */
|
||||||
|
|
||||||
|
[data-theme="cerberus"] {
|
||||||
|
--text-scaling: 1;
|
||||||
|
--radius-base: 0.25rem;
|
||||||
|
--radius-container: 0.25rem;
|
||||||
|
--color-surface-950: oklch(0% 0 none);
|
||||||
|
}
|
||||||
3
src/svg/bluesky.svg
Normal file
3
src/svg/bluesky.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="9.9966 9.6552 580.0034 510.6799" xml:space="preserve">
|
||||||
|
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 774 B |
3
src/svg/github.svg
Normal file
3
src/svg/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg height="800px" width="800px" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1002 B |
12
src/svg/linkedin.svg
Normal file
12
src/svg/linkedin.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 382 382" xml:space="preserve">
|
||||||
|
<path d="M347.445,0H34.555C15.471,0,0,15.471,0,34.555v312.889C0,366.529,15.471,382,34.555,382h312.889
|
||||||
|
C366.529,382,382,366.529,382,347.444V34.555C382,15.471,366.529,0,347.445,0z M118.207,329.844c0,5.554-4.502,10.056-10.056,10.056
|
||||||
|
H65.345c-5.554,0-10.056-4.502-10.056-10.056V150.403c0-5.554,4.502-10.056,10.056-10.056h42.806
|
||||||
|
c5.554,0,10.056,4.502,10.056,10.056V329.844z M86.748,123.432c-22.459,0-40.666-18.207-40.666-40.666S64.289,42.1,86.748,42.1
|
||||||
|
s40.666,18.207,40.666,40.666S109.208,123.432,86.748,123.432z M341.91,330.654c0,5.106-4.14,9.246-9.246,9.246H286.73
|
||||||
|
c-5.106,0-9.246-4.14-9.246-9.246v-84.168c0-12.556,3.683-55.021-32.813-55.021c-28.309,0-34.051,29.066-35.204,42.11v97.079
|
||||||
|
c0,5.106-4.139,9.246-9.246,9.246h-44.426c-5.106,0-9.246-4.14-9.246-9.246V149.593c0-5.106,4.14-9.246,9.246-9.246h44.426
|
||||||
|
c5.106,0,9.246,4.14,9.246,9.246v15.655c10.497-15.753,26.097-27.912,59.312-27.912c73.552,0,73.131,68.716,73.131,106.472
|
||||||
|
L341.91,330.654L341.91,330.654z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
5
svelte.config.js
Normal file
5
svelte.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { vitePreprocess } from '@astrojs/svelte';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@src/*": ["src/*"],
|
||||||
|
"@layouts/*": ["src/layouts/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@content/*": ["src/content/*"],
|
||||||
|
"@styles/*": ["src/styles/*"],
|
||||||
|
"@svg/*": ["src/svg/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user