Personal portfolio site for Justin Frank — designer/developer from the Texas Panhandle, based in the Dallas/Fort Worth area.
Built with SvelteKit 2 + Svelte 5, fully static output via @sveltejs/adapter-static. Styled with plain SCSS (no frameworks). Writing/case studies authored in Markdown via mdsvex.
| Layer | Tool |
|---|---|
| Framework | SvelteKit 2 / Svelte 5 |
| Build | Vite 6 |
| Styling | Dart Sass (sass) via vitePreprocess |
| Content | mdsvex (.md files → Svelte routes) |
| Output | Fully static (build/) |
| Fonts | Butler (self-hosted woff/woff2) + Google Fonts |
Replaces: Gulp 3, Bootstrap 3, jQuery 1.x, FitText plugin, Universal Analytics.
npm run dev # Start dev server at localhost:5173
npm run build # Build static site to build/
npm run preview # Preview the production build locallypersonal-site/
├── src/
│ ├── app.html # HTML shell — favicons, Google Fonts, %sveltekit.head/body%
│ ├── app.scss # Global styles: @font-face, @keyframes, body, a, .container
│ ├── lib/
│ │ └── styles/
│ │ ├── _variables.scss # Colors, fonts, breakpoints ($screen-* aliases included)
│ │ └── _mixins.scss # Shared mixins (extend as needed)
│ ├── components/
│ │ ├── SiteHeader.svelte # Header, nav, "howdy" headline
│ │ ├── AboutSection.svelte # Animated gradient section, avatar, callout, bio
│ │ ├── SkillsSection.svelte # Cultural + technical skills lists
│ │ ├── PortfolioSection.svelte # Data-driven project cards
│ │ └── SiteFooter.svelte # Footer with border-top
│ ├── routes/
│ │ ├── +layout.js # export const prerender = true
│ │ ├── +layout.svelte # Imports app.scss, GA4 placeholder
│ │ ├── +page.svelte # Composes all section components
│ │ └── writing/
│ │ └── [slug]/
│ │ ├── +page.js # Auto-discovers .md files via import.meta.glob
│ │ └── +page.svelte # Renders mdsvex content
│ └── writing/ # Drop .md posts here — auto-prerendered
├── static/
│ ├── img/ # Site images (plains, avatar, portfolio shots)
│ ├── fonts/
│ │ ├── butler/ # Butler woff/woff2 files
│ │ └── butler-stencil/ # Butler Stencil woff/woff2 files
│ ├── favicon*.png, apple-touch-icon.png, safari-pinned-tab.svg
│ └── site.webmanifest
├── svelte.config.js
├── vite.config.js
└── package.json
Header with background image, logo, nav links, and the "howdy" headline. The headline uses font-size: clamp(20px, 20vw, 250px) — replaces the old jQuery FitText plugin with zero JavaScript.
The animated gradient (purple-haze) section. CSS Grid replaces Bootstrap's offset column layout. The gradient animation is driven by a @keyframes Gradient defined globally in app.scss — keyframe names are always global in CSS, so the scoped animation: reference in the component works correctly.
Cultural and technical skills rendered as two flex-wrapped ul lists.
Data-driven — projects are defined as a plain JS array at the top of the <script> block. Adding a project means appending one object to the array:
const projects = [
{
title: 'Project Title',
description: 'What you did.',
attribution: 'All work completed for Client Name.',
image1x: '/img/project1x.png',
image2x: '/img/project2x.png',
colorClass: 'ruby', // or 'electricBlue'
},
];Minimal footer with a ::before border-top rule and copyright text.
Variables and mixins are injected globally into every <style lang="scss"> block via Vite's additionalData. This means you never need to @import or @use them manually in a component.
/* Available everywhere — no import needed */
$ruby, $electric-blue, $deep-cove, $white, $alto
$open-sans, $source-code, $butler-medium-stencil, $butler-medium
$mobile, $tablet, $large
$screen-xs, $screen-sm, $screen-sm-min, $screen-md, $screen-lgRule: _variables.scss and _mixins.scss must never contain output-generating CSS (no selectors, no @font-face, no @keyframes). Only variables, functions, and mixins. Global output-generating rules live in app.scss.
- Create a Markdown file in
src/writing/:
---
title: "Case Study: Project Name"
date: "2026-03-01"
---
# Heading
Write in plain Markdown. You can also embed Svelte components.- That's it. The
import.meta.globin+page.jsauto-discovers all.mdfiles andentries()derives the slugs — no config needed. The post prerendered at/writing/case-study-project-nameon the next build.
The GA4 snippet is in src/routes/+layout.svelte, commented out. Replace G-XXXXXXXXXX with your measurement ID and uncomment:
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>This project uses Svelte 5 syntax throughout.
Props — use $props() instead of export let:
<script>
let { data } = $props();
</script>Derived state — use $derived() for values computed from props:
<script>
let { data } = $props();
const title = $derived(data.meta.title);
</script>Rendering children — layouts use {@render children()} instead of <slot>:
<script>
let { children } = $props();
</script>
{@render children()}Reactive state — use $state() for mutable local state (not needed in this project's static components, but the pattern for future use):
<script>
let count = $state(0);
</script>The build/ directory is a fully static site. Point any static host at it:
- Netlify: build command
npm run build, publish directorybuild - Vercel: framework preset
SvelteKit, or build commandnpm run build, outputbuild - GitHub Pages: build and commit
build/or use a CI action
No server-side runtime required.