Tweaks, fixes, and launch prep

This commit is contained in:
2026-02-06 19:09:48 -03:00
parent 22b21d254c
commit 2959360d65
34 changed files with 496 additions and 81 deletions

View File

@@ -1,4 +1,4 @@
# CI: runs on every push. Lint, check, test, build (dev), e2e.
# CI: runs on every push. Install, lint, check, test, build (dev), e2e.
when:
- event: pull_request
- event: push
@@ -7,15 +7,75 @@ when:
- event: manual
steps:
build:
install:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
lint:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run lint
depends_on:
- install
check:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run check
depends_on:
- install
test:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run test:run
depends_on:
- install
build:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run build
depends_on:
- install
build-full:
image: node:22-bookworm-slim
commands:
- apt-get update
- apt-get install -y --no-install-recommends ca-certificates libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdrm2 libgbm1 libgtk-3-0 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2
- rm -rf /var/lib/apt/lists/*
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run critical-css:install
- pnpm run build:full
depends_on:
- install
e2e:
image: node:22-bookworm-slim
commands:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm install --frozen-lockfile
- pnpm run build
- pnpm exec playwright install chromium --with-deps
- pnpm run test:e2e
depends_on:
- build

View File

@@ -10,7 +10,8 @@ This repo is a **one-page static** Linktree-style site for mifi.dev. It is **not
- **TypeScript** (always)
- **pnpm** (only); do not use npm or yarn
- **PostCSS** for CSS (nesting + preset-env)
- **Critical CSS** via post-build script (`scripts/critical-css.mjs`); full build with critical CSS is `pnpm run build:full` (requires Chromium)
- **Critical CSS** via post-build script (`scripts/critical-css.mjs`); full build with critical CSS is `pnpm run build:full` (run `pnpm run critical-css:install` once to install Chromium)
- **CSP-safe scripts:** Post-build `scripts/externalize-inline-script.mjs` moves SvelteKits inline bootstrap script to `_app/immutable/bootstrap.[hash].js` so CSP can use `script-src 'self'` without `unsafe-inline`
- **Content:** JSON in `src/lib/data/` (e.g. `links.json`), loaded in `+page.server.ts` at build time
- **CSP:** Set by Traefik middleware; do not add CSP in app code
@@ -19,8 +20,8 @@ This repo is a **one-page static** Linktree-style site for mifi.dev. It is **not
- Use **semantic HTML** and **JSON-LD** for SEO; target **WCAG 2.2 AAA** for accessibility.
- **No unsafe-inline** scripts; all JS via `<script src="...">`.
- **Dev container** uses the same Linux + Playwright Chromium as CI so e2e/visual-regression snapshots are comparable.
- **Docker:** Single image with both variants; **nginx** does host-based routing (mifi.dev / www.mifi.dev → `/html/dev`, mifi.bio / www.mifi.bio → `/html/bio`). Traefik labels for both hosts; network `marina-net`. Dockerfile builds both with `CONTENT_VARIANT=dev` and `CONTENT_VARIANT=bio`.
- **CI:** Woodpecker; pipeline runs lint, unit tests, e2e/visual regression, build (pnpm).
- **Docker:** Single image with both variants; **nginx** does host-based routing (mifi.dev / www.mifi.dev → `/html/dev`, mifi.bio / www.mifi.bio → `/html/bio`). Traefik labels for both hosts; network `marina-net`. Dockerfile builds both with `CONTENT_VARIANT=dev` and `CONTENT_VARIANT=bio`, each with critical CSS inlined.
- **CI:** Woodpecker; pipeline runs lint, unit tests, e2e/visual regression, build (pnpm), and build-full (critical CSS; installs Chromium).
## Key paths
@@ -28,6 +29,7 @@ This repo is a **one-page static** Linktree-style site for mifi.dev. It is **not
- `src/lib/data/` JSON content
- `src/app.html` HTML shell
- `scripts/critical-css.mjs` post-build critical CSS
- `scripts/externalize-inline-script.mjs` post-build: extract bootstrap script for CSP
- `.devcontainer/` dev container (Node, pnpm, Playwright Linux)
- `Dockerfile` multi-stage build (both dev + bio variants), then nginx with host-based routing
- `nginx/default.conf` nginx server blocks for mifi.dev and mifi.bio
@@ -37,6 +39,6 @@ This repo is a **one-page static** Linktree-style site for mifi.dev. It is **not
- Install: `pnpm install`
- Dev: `pnpm dev`
- Build: `pnpm build` (or `pnpm build:full` for critical CSS)
- Build: `pnpm build` (static SSR HTML + externalized script; or `pnpm build:full` for critical CSS; run `pnpm run critical-css:install` once first)
- Lint: `pnpm lint`
- Test: `pnpm test:run`, `pnpm test:e2e`

View File

@@ -1,10 +1,24 @@
# Multi-stage: build both variants (dev + bio), then nginx with host-based routing.
# Multi-stage: build both variants (dev + bio) with critical CSS, then nginx with host-based routing.
# No buildx; plain docker build.
FROM node:22-bookworm-slim AS builder
# Chromium deps for critical CSS (Puppeteer headless)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnss3 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbcommon0 \
libxrandr2 \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@latest --activate
@@ -14,13 +28,16 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Install Chromium for critical CSS (used by the "critical" package)
RUN pnpm run critical-css:install
COPY . .
# Build dev variant, move output, then build bio variant (same build/ dir).
# Build dev variant with critical CSS, move output, then build bio variant with critical CSS.
RUN set -e && \
CONTENT_VARIANT=dev pnpm run build && \
CONTENT_VARIANT=dev pnpm run build && pnpm run critical-css && \
cp -r build /out/dev && \
CONTENT_VARIANT=bio pnpm run build && \
CONTENT_VARIANT=bio pnpm run build && pnpm run critical-css && \
cp -r build /out/bio
# Runtime: nginx serves /out/dev and /out/bio by Host header.
@@ -29,5 +46,6 @@ FROM nginx:alpine
COPY --from=builder /out/dev /usr/share/nginx/html/dev
COPY --from=builder /out/bio /usr/share/nginx/html/bio
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY nginx/snippets/ /etc/nginx/snippets/
EXPOSE 80

View File

@@ -34,8 +34,9 @@ pnpm preview # preview build at http://localhost:4173
| `pnpm dev:bio` | Start Vite dev server for mifi.bio |
| `pnpm dev:dev` | Start Vite dev server for mifi.dev |
| `pnpm build` | Build static site to `build/` |
| `pnpm build:full` | Build + inline critical CSS (requires Chromium: `pnpm exec puppeteer browsers install chromium`) |
| `pnpm preview` | Serve `build/` locally |
| `pnpm build:full` | Build + inline critical CSS (run `pnpm run critical-css:install` once to install Chromium) |
| `pnpm critical-css:install` | Install Chromium for critical CSS (required once before first `build:full`) |
| `pnpm preview` | Serve `build/` locally |
| `pnpm check` | Run `svelte-kit sync` and `svelte-check` |
| `pnpm lint` | ESLint + Stylelint |
| `pnpm format` | Prettier (write) |
@@ -63,7 +64,7 @@ pnpm preview # preview build at http://localhost:4173
- **Format:** Prettier
- **Unit tests:** Vitest
- **E2E / visual regression:** Playwright (use same Linux build in dev container and CI)
- **Critical CSS:** Post-build step via `critical` (run `pnpm build:full` with Chromium installed)
- **Critical CSS:** Post-build step via `critical` (run `pnpm run critical-css:install` once, then `pnpm build:full`)
## Build and run with Docker

View File

@@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test';
test('homepage has title and main content', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/mifi\.dev/);
await expect(page.getByRole('heading', { level: 1 })).toContainText('mifi.dev');
await expect(page.getByRole('main')).toBeVisible();
});

24
e2e/homepage.spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';
test('homepage has title and main content', async ({ page }) => {
await page.goto('/');
// Title and hero (variant-agnostic)
await expect(page).toHaveTitle(/mifi\.(dev|bio)/);
await expect(page.getByRole('heading', { level: 1 })).toContainText('mifi');
// Key landmarks: header, main, footer
await expect(page.getByRole('banner')).toBeVisible();
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByRole('contentinfo')).toBeVisible();
// Skip link targets main content (a11y)
const skipLink = page.getByRole('link', { name: /skip to main content/i });
await expect(skipLink).toBeVisible();
await expect(skipLink).toHaveAttribute('href', '#main-content');
await expect(page.locator('main#main-content')).toBeVisible();
// At least one link (both variants have link sections)
const linkCount = await page.getByRole('link').count();
expect(linkCount).toBeGreaterThan(0);
});

View File

@@ -1,12 +1,29 @@
# Host-based routing: mifi.dev / www.mifi.dev → dev root, mifi.bio / www.mifi.bio → bio root
# Security headers are handled upstream by Traefik
server {
listen 80 default_server;
server_name mifi.dev www.mifi.dev;
root /usr/share/nginx/html/dev;
index index.html;
# Map canonical .well-known paths to variant-specific files
location = /.well-known/security.txt {
alias $document_root/.well-known/dev.security.txt;
add_header Cache-Control "public, max-age=86400";
}
location = /.well-known/appspecific/com.chrome.devtools.json {
alias $document_root/.well-known/dev.com.chrome.devtools.json;
add_header Cache-Control "public, max-age=86400";
}
include /etc/nginx/snippets/cache-rules.conf;
location / {
try_files $uri $uri/ /index.html;
}
error_page 404 /index.html;
}
server {
@@ -14,7 +31,22 @@ server {
server_name mifi.bio www.mifi.bio;
root /usr/share/nginx/html/bio;
index index.html;
# Map canonical .well-known paths to variant-specific files
location = /.well-known/security.txt {
alias $document_root/.well-known/bio.security.txt;
add_header Cache-Control "public, max-age=86400";
}
location = /.well-known/appspecific/com.chrome.devtools.json {
alias $document_root/.well-known/bio.com.chrome.devtools.json;
add_header Cache-Control "public, max-age=86400";
}
include /etc/nginx/snippets/cache-rules.conf;
location / {
try_files $uri $uri/ /index.html;
}
error_page 404 /index.html;
}

View File

@@ -0,0 +1,64 @@
# Cache and security rules for static site (included by both dev and bio server blocks)
# Security headers are handled upstream by Traefik
# HTML: no-cache (always revalidate)
location ~ \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
expires 0;
}
# CSS and JavaScript: long cache with immutable (hashed filenames)
location ~* \.(css|js)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# Images: long cache (30 days)
location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
add_header Cache-Control "public, max-age=2592000";
access_log off;
}
# SVG images: long cache (30 days)
location ~* \.svg$ {
add_header Cache-Control "public, max-age=2592000";
add_header Content-Type image/svg+xml;
access_log off;
}
# Fonts: long cache with immutable
location ~* \.(woff|woff2|ttf|otf|eot)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# Documents: medium cache (30 days)
location ~* \.(pdf|doc|docx)$ {
add_header Cache-Control "public, max-age=2592000";
access_log off;
}
# robots.txt: short cache (1 day)
location = /robots.txt {
add_header Cache-Control "public, max-age=86400";
access_log off;
}
# favicon: long cache (30 days)
location = /favicon.svg {
add_header Cache-Control "public, max-age=2592000";
add_header Content-Type image/svg+xml;
access_log off;
}
# .well-known (security.txt, ACME, etc.)
location ^~ /.well-known/ {
add_header Cache-Control "public, max-age=86400";
}
# Deny hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}

View File

@@ -5,8 +5,12 @@
"scripts": {
"dev:bio": "CONTENT_VARIANT=bio vite dev",
"dev:dev": "CONTENT_VARIANT=dev vite dev",
"build": "vite build",
"build:full": "vite build && pnpm run critical-css",
"build": "vite build && node scripts/externalize-inline-script.mjs",
"build:bio": "CONTENT_VARIANT=bio vite build && node scripts/externalize-inline-script.mjs",
"build:dev": "CONTENT_VARIANT=dev vite build && node scripts/externalize-inline-script.mjs",
"build:full": "vite build && pnpm run critical-css && node scripts/externalize-inline-script.mjs",
"build:full:bio": "CONTENT_VARIANT=bio vite build && pnpm run critical-css && node scripts/externalize-inline-script.mjs",
"build:full:dev": "CONTENT_VARIANT=dev vite build && pnpm run critical-css && node scripts/externalize-inline-script.mjs",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -19,7 +23,11 @@
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"critical-css": "node scripts/critical-css.mjs"
"test:e2e:snapshots:dev": "CONTENT_VARIANT=dev pnpm run build && CONTENT_VARIANT=dev pnpm exec playwright test --update-snapshots",
"test:e2e:snapshots:bio": "CONTENT_VARIANT=bio pnpm run build && CONTENT_VARIANT=bio pnpm exec playwright test --update-snapshots",
"critical-css": "node scripts/critical-css.mjs",
"critical-css:install": "pnpm exec puppeteer browsers install chrome",
"externalize-inline-script": "node scripts/externalize-inline-script.mjs"
},
"devDependencies": {
"@playwright/test": "^1.49.0",

View File

@@ -1,35 +1,46 @@
/**
* Post-build step: inline critical CSS in built HTML.
* Reads build/index.html, extracts critical CSS, inlines in <head>,
* Reads <buildDir>/index.html, extracts critical CSS, inlines in <head>,
* and defers non-critical styles (preload + link at end of body).
*
* Usage: node scripts/critical-css.mjs [buildDir]
* buildDir: path to build output (default: "build"). Use from repo root.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { join } from 'node:path';
import { cwd } from 'node:process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const buildDir = join(__dirname, '..', 'build');
const buildDir = join(cwd(), process.argv[2] || 'build');
const htmlPath = join(buildDir, 'index.html');
// critical/penthouse use a nested puppeteer; point it at our installed Chrome
try {
const puppeteer = await import('puppeteer');
const executablePath = puppeteer.default?.executablePath?.();
if (executablePath) {
process.env.PUPPETEER_EXECUTABLE_PATH = executablePath;
}
} catch {
// no top-level puppeteer or no executable; rely on env or default
}
try {
const { generate } = await import('critical');
const html = readFileSync(htmlPath, 'utf-8');
const { html: outHtml } = await generate({
base: buildDir,
html,
inline: true,
inline: { strategy: 'default' }, // preload in head + link at end of body (no inline JS, CSP-safe)
dimensions: [{ width: 1280, height: 720 }],
penthouse: { timeout: 30000 },
});
writeFileSync(htmlPath, outHtml, 'utf-8');
console.log('Critical CSS inlined in build/index.html');
console.log(`Critical CSS inlined in ${htmlPath}`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error('Critical CSS step failed:', msg);
if (msg.includes('Browser is not downloaded')) {
console.error(
'Install Chromium for critical CSS: pnpm exec puppeteer browsers install chromium',
);
console.error('Install Chromium first: pnpm run critical-css:install');
console.error('Or run "pnpm run build" without critical CSS.');
}
process.exit(1);

View File

@@ -0,0 +1,93 @@
/**
* Post-build: extract SvelteKit's inline bootstrap script to an external file
* and replace it with <script src="..."> so CSP can use script-src 'self' without unsafe-inline.
*
* Usage: node scripts/externalize-inline-script.mjs [buildDir]
* buildDir: path to build output (default: "build"). Use from repo root.
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join } from 'node:path';
import { cwd } from 'node:process';
const buildDir = join(cwd(), process.argv[2] || 'build');
const htmlPath = join(buildDir, 'index.html');
function findSvelteKitInlineScript(html) {
// Find <script> without src= whose content contains __sveltekit_ (the bootstrap)
let scriptStart = html.indexOf('<script>');
while (scriptStart !== -1) {
const result = extractScriptContent(html, scriptStart);
if (result && result.content.includes('__sveltekit_')) return result;
scriptStart = html.indexOf('<script>', scriptStart + 1);
}
return null;
}
function extractScriptContent(html, scriptStart) {
if (scriptStart === -1) return null;
const contentStart = scriptStart + '<script>'.length;
let pos = contentStart;
let inString = null;
let escape = false;
const endTag = '</script>';
while (pos < html.length) {
if (escape) {
escape = false;
pos++;
continue;
}
if (inString) {
if (html[pos] === '\\') {
escape = true;
pos++;
continue;
}
if (html[pos] === inString) {
inString = null;
}
pos++;
continue;
}
if (html[pos] === '"' || html[pos] === "'") {
inString = html[pos];
pos++;
continue;
}
if (html.slice(pos, pos + endTag.length) === endTag) {
return {
start: scriptStart,
end: pos + endTag.length,
content: html.slice(contentStart, pos),
};
}
pos++;
}
return null;
}
try {
let html = readFileSync(htmlPath, 'utf-8');
const found = findSvelteKitInlineScript(html);
if (!found) {
console.log('No SvelteKit inline bootstrap script found in', htmlPath);
process.exit(0);
}
const content = found.content;
const hash = createHash('sha256').update(content).digest('hex').slice(0, 8);
const filename = `bootstrap.${hash}.js`;
const immutableDir = join(buildDir, '_app', 'immutable');
mkdirSync(immutableDir, { recursive: true });
const scriptPath = join(immutableDir, filename);
writeFileSync(scriptPath, content.trimStart(), 'utf-8');
const scriptTag = `<script src="./_app/immutable/${filename}"></script>`;
html =
html.slice(0, found.start) +
scriptTag +
html.slice(found.end);
writeFileSync(htmlPath, html, 'utf-8');
console.log('Externalized SvelteKit bootstrap to', scriptPath);
} catch (err) {
console.error('externalize-inline-script failed:', err instanceof Error ? err.message : String(err));
process.exit(1);
}

View File

@@ -3,12 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preload" href="/assets/js/theme-store.js" as="script" />
<title>mifi.dev</title>
<script src="/assets/js/theme-store.js"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<script src="/assets/js/theme-store.js"></script>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Moon, SunMoon, Sun } from '@lucide/svelte';
import { getStoredTheme, setTheme } from '$lib/theme';
import type { ThemeMode } from '$lib/theme';
@@ -18,7 +17,8 @@
const themeOffset = $derived(OFFSETS[current]);
onMount(() => {
$effect(() => {
if (typeof document === 'undefined') return;
current = getStoredTheme();
});

View File

@@ -19,6 +19,12 @@ export const GA_MEASUREMENT_IDS: Record<'dev' | 'bio', string> = {
bio: 'G-885B0KYWZ1',
};
/** theme-color meta values per variant (match tokens-{variant}.css --color-bg) */
export const THEME_COLORS: Record<'dev' | 'bio', { light: string; dark: string }> = {
dev: { light: '#f5f4f8', dark: '#131118' }, // hsl(260 20% 98%) / hsl(260 18% 8%)
bio: { light: '#f4f6f9', dark: '#111318' }, // hsl(220 22% 98%) / hsl(220 18% 8%)
};
export const UTM_MEDIUM = 'link';
export const UTM_CAMPAIGN = 'landing';

View File

@@ -17,14 +17,6 @@
font-display: swap;
}
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('/assets/fonts/plus-jakarta-sans-v12-latin-500.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('/assets/fonts/plus-jakarta-sans-v12-latin-regular.woff2') format('woff2');
@@ -49,19 +41,3 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Inter;
src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Inter;
src: url('/assets/fonts/inter-v20-latin-600.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}

View File

@@ -1,5 +1,5 @@
import contentData from '$lib/data/links.json';
import { VARIANT_HOSTS, GA_MEASUREMENT_IDS } from '$lib/config';
import { VARIANT_HOSTS, GA_MEASUREMENT_IDS, THEME_COLORS } from '$lib/config';
import type { Site, ContentData, ProcessedLink } from '$lib/data/types';
import type { LayoutServerLoad } from './$types';
import { ContentVariant, HeroLayout } from '$lib/data/constants';
@@ -18,6 +18,9 @@ export type LayoutServerDataOut = {
};
variant: string;
gaMeasurementId: string;
/** theme-color meta values for current variant */
themeColorLight: string;
themeColorDark: string;
};
export const load: LayoutServerLoad<LayoutServerDataOut> = (): LayoutServerDataOut => {
@@ -58,11 +61,14 @@ export const load: LayoutServerLoad<LayoutServerDataOut> = (): LayoutServerDataO
contactLinks: siteDef?.contactLinks,
qrCodeImage: siteDef?.qrCodeImage ?? undefined,
};
const themeColors = THEME_COLORS[variant];
return {
site,
contactLinks,
links: { sections },
variant,
gaMeasurementId: GA_MEASUREMENT_IDS[variant],
themeColorLight: themeColors.light,
themeColorDark: themeColors.dark,
};
};

View File

@@ -1,37 +1,82 @@
<script lang="ts">
import '../app.css';
import type { Snippet } from 'svelte';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
import '../app.css';
const jsonLd = {
let { children, data }: { children: Snippet; data: LayoutData } = $props();
const jsonLd = $derived({
'@context': 'https://schema.org',
'@type': 'WebSite' as const,
name: data.site.title,
url: data.site.url,
description: data.site.metaDescription,
};
});
$: personLd = data.site.person
? {
'@context': 'https://schema.org' as const,
'@type': 'Person' as const,
name: data.site.person.name,
url: data.site.url,
sameAs: data.site.person.sameAs,
}
: null;
const personLd = $derived(
data.site.person
? {
'@context': 'https://schema.org' as const,
'@type': 'Person' as const,
name: data.site.person.name,
url: data.site.url,
sameAs: data.site.person.sameAs,
}
: null
);
// Inject as HTML to avoid Prettier parsing ld+json script body as JS (Babel syntax error)
const ldJsonTag = (payload: string) =>
'<' + 'script type="application/ld+json">' + payload + '<' + '/script>';
$: jsonLdHtml = ldJsonTag(JSON.stringify(jsonLd));
$: personLdHtml = personLd != null ? ldJsonTag(JSON.stringify(personLd)) : '';
const jsonLdHtml = $derived(ldJsonTag(JSON.stringify(jsonLd)));
const personLdHtml = $derived(
personLd != null ? ldJsonTag(JSON.stringify(personLd)) : ''
);
</script>
<svelte:head>
<link rel="stylesheet" href="/assets/tokens-{data.variant}.css" />
<link
rel="preload"
href="/assets/fonts/fraunces-variable-opsz-wght.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/plus-jakarta-sans-v12-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/plus-jakarta-sans-v12-latin-regular.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-regular.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/apple-touch-icon.png" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content={data.themeColorLight} media="(prefers-color-scheme: light)" />
<meta name="theme-color" content={data.themeColorDark} media="(prefers-color-scheme: dark)" />
<!-- eslint-disable-next-line svelte/no-at-html-tags -- safe: our own JSON.stringify, no user input -->
{@html jsonLdHtml}
{#if personLdHtml}
@@ -44,4 +89,4 @@
<header class="site-header">
<ThemeToggle />
</header>
<slot />
{@render children()}

View File

@@ -1 +1,2 @@
export const prerender = true;
export const ssr = true;

View File

@@ -22,6 +22,30 @@
<svelte:head>
<title>{data.site.title}</title>
<meta name="description" content={data.site.metaDescription} />
<link rel="canonical" href={data.site.url} />
<meta name="robots" content="index, follow" />
{#if data.site.person?.name}
<meta name="author" content={data.site.person.name} />
{/if}
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={data.site.title} />
<meta property="og:description" content={data.site.metaDescription} />
<meta property="og:url" content={data.site.url} />
<meta property="og:site_name" content={data.site.title} />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="{data.site.url}/assets/images/mifi-{data.variant}-og-image.webp" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={data.site.title} />
<meta name="twitter:description" content={data.site.metaDescription} />
<meta name="twitter:image" content="{data.site.url}/assets/images/mifi-{data.variant}-twitter-image.webp" />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="1200" />
</svelte:head>
<main id="main-content">

View File

@@ -0,0 +1,12 @@
{
"name": "mifi.bio",
"url": "https://mifi.bio",
"description": "mifi.bio",
"icons": [
{
"src": "https://mifi.dev/assets/images/apple-touch-icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -0,0 +1,11 @@
# Canonical URL for this file (recommended for validation)
Canonical: https://mifi.bio/.well-known/security.txt
# Contact for reporting security vulnerabilities (required)
Contact: mailto:security@mifi.holdings
# Optional: link to your vulnerability disclosure policy when you have one
# Policy: https://mifi.bio/security
# Date after which this file is considered stale (required; RFC 3339; max 1 year recommended)
Expires: 2027-02-01T00:00:00.000Z

View File

@@ -0,0 +1,12 @@
{
"name": "mifi.dev",
"url": "https://mifi.dev",
"description": "mifi.dev",
"icons": [
{
"src": "https://mifi.dev/assets/images/apple-touch-icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -0,0 +1,11 @@
# Canonical URL for this file (recommended for validation)
Canonical: https://mifi.dev/.well-known/security.txt
# Contact for reporting security vulnerabilities (required)
Contact: mailto:security@mifi.holdings
# Optional: link to your vulnerability disclosure policy when you have one
# Policy: https://mifi.dev/security
# Date after which this file is considered stale (required; RFC 3339; max 1 year recommended)
Expires: 2027-02-01T00:00:00.000Z

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 2134 2134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Z" style="fill:#0b0b0f;"/><g><path d="M178.684,1755.333l0,-916.655l208.714,0l0,211.244l-24.034,-34.153c16.304,-66.339 49.965,-115.812 100.984,-148.419c51.019,-32.607 110.822,-48.911 179.41,-48.911c74.772,0 140.899,19.466 198.384,58.398c57.484,38.932 94.659,90.724 111.525,155.376l-63.247,5.481c28.391,-73.928 70.625,-128.953 126.704,-165.074c56.079,-36.121 120.801,-54.181 194.167,-54.181c64.933,0 122.98,14.617 174.139,43.851c51.16,29.234 91.567,69.852 121.223,121.855c29.656,52.003 44.483,112.157 44.483,180.464l0,590.724l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.137,0 -66.128,8.082 -92.973,24.245c-26.845,16.163 -47.646,38.791 -62.403,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.418,0 -66.479,8.082 -93.183,24.245c-26.704,16.163 -47.435,38.791 -62.193,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1733.286,1755.333l0,-916.655l221.363,0l0,916.655l-221.363,0Zm0,-1020.379l0,-236.121l221.363,0l0,236.121l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></svg>
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<style>
.block { fill: #0b0b0f; }
@media (prefers-color-scheme: dark) {
.block { fill: #f2f2f2; }
}
</style>
<path class="block" d="M512,96l0,320c0,52.984 -43.016,96 -96,96l-320,0c-52.984,0 -96,-43.016 -96,-96l0,-320c0,-52.984 43.016,-96 96,-96l320,0c52.984,0 96,43.016 96,96Zm-96.011,80.389l53.127,0l0,-56.669l-53.127,0l0,56.669Zm-193.658,55.292c-4.819,-8.296 -11.558,-15.376 -20.217,-21.24c-13.796,-9.344 -29.667,-14.015 -47.612,-14.015c-16.461,0 -30.814,3.913 -43.058,11.739c-7.882,5.038 -14.038,11.753 -18.468,20.146l0,-27.027l-50.091,0l0,219.997l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.517,-12.413 14.926,-16.292c6.409,-3.879 13.864,-5.819 22.364,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.534,-12.413 14.977,-16.292c6.443,-3.879 13.881,-5.819 22.313,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-141.774c0,-16.394 -3.559,-30.831 -10.676,-43.311c-7.117,-12.481 -16.815,-22.229 -29.093,-29.245c-12.278,-7.016 -26.209,-10.524 -41.793,-10.524c-17.608,0 -33.141,4.335 -46.6,13.004c-8.624,5.555 -15.884,12.972 -21.779,22.252Zm193.658,189.599l53.127,0l0,-219.997l-53.127,0l0,219.997Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB