Compare commits

...

22 Commits

Author SHA1 Message Date
ddcfc8d8d2 Pop semvar up
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 19:54:49 -03:00
dac44e1b12 Update Trusted Type loading
Some checks failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/push/ci Pipeline failed
2026-03-12 19:54:15 -03:00
5eb34a0c59 Trusted Types for Clarity
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 18:13:55 -03:00
ef40d25e6a Use consentv2 API
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 18:01:59 -03:00
718165aa23 Updates e2e tests
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 17:36:18 -03:00
c15adf8e3c More font fixes... Kill the flash. 2026-03-12 17:32:12 -03:00
72f0eab718 Script updates
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 16:46:41 -03:00
beffd5f4e8 CSS and @font-face fixes 2026-03-12 16:31:58 -03:00
0c4823d263 More tweaks to pipeline and some script tweaks
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 15:56:47 -03:00
c912cde7f5 Deploy pipeline fix
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline failed
2026-03-12 15:47:22 -03:00
c8d7c168c8 Pipeline fixes
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline failed
2026-03-12 15:38:38 -03:00
a2242809b2 Add version to docker tags 2026-03-12 15:35:53 -03:00
4d43018773 Update package version (we've got GDPR compliance now) 2026-03-12 15:28:48 -03:00
1f8e5c4c3e Updates to the legalese 2026-03-12 15:27:01 -03:00
a5989b03b1 Add GDPR compliant cookie banner and update footer/privacy policy to include GA and Clarity; added e2e and unit tests for cookie handling; updated snapshots
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 15:04:49 -03:00
4ad45d5625 Use external clarity script
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 12:54:06 -03:00
e6f2e92083 Inline subpage critical CSS
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 12:39:50 -03:00
4503298213 Add MS Clarity to site
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 12:32:05 -03:00
80a4717b14 JSON-LD same-as tweaks (#8)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #8
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>
2026-03-12 03:47:44 +00:00
cf6ff70cfb JSON-LD Improvements
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-12 00:24:28 -03:00
d7e427f164 Fixes so services page comes up (hopefully)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-03-09 22:02:28 -03:00
a9c25917f0 Update package.json 2026-03-09 21:54:10 -03:00
63 changed files with 1449 additions and 444 deletions

View File

@@ -1 +1,4 @@
# Only run CI tests when files under src/ are changed
if git diff --cached --name-only | grep -q '^src/'; then
pnpm run test:ci pnpm run test:ci
fi

View File

@@ -20,13 +20,17 @@ steps:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
commands: commands:
- set -e - set -e
- apk add --no-cache jq
- APP_VERSION=$(jq -r .version package.json | tr -d '\r\n' | sed 's/^v//')
- echo "=== Building Docker image ===" - echo "=== Building Docker image ==="
- 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"' - 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"'
- 'echo "Registry repo: $REGISTRY_REPO"' - 'echo "Registry repo: $REGISTRY_REPO"'
- 'echo "App version: $APP_VERSION"'
- | - |
docker build \ docker build \
--tag $REGISTRY_REPO:${CI_COMMIT_SHA} \ --tag "$REGISTRY_REPO:$CI_COMMIT_SHA" \
--tag $REGISTRY_REPO:latest \ --tag "$REGISTRY_REPO:latest" \
--tag "$REGISTRY_REPO:$APP_VERSION" \
--label "git.commit=${CI_COMMIT_SHA}" \ --label "git.commit=${CI_COMMIT_SHA}" \
--label "git.branch=${CI_COMMIT_BRANCH}" \ --label "git.branch=${CI_COMMIT_BRANCH}" \
. .
@@ -82,15 +86,19 @@ steps:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
commands: commands:
- set -e - set -e
- apk add --no-cache jq
- APP_VERSION=$(jq -r .version package.json | tr -d '\r\n' | sed 's/^v//')
- echo "=== Pushing to registry ===" - echo "=== Pushing to registry ==="
- 'echo "Registry: $REGISTRY_URL"' - 'echo "Registry: $REGISTRY_URL"'
- 'echo "Repository: $REGISTRY_REPO"' - 'echo "Repository: $REGISTRY_REPO"'
- 'echo "App version: $APP_VERSION"'
- | - |
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \
-u "$REGISTRY_USERNAME" \ -u "$REGISTRY_USERNAME" \
--password-stdin --password-stdin
- docker push $REGISTRY_REPO:${CI_COMMIT_SHA} - 'docker push $REGISTRY_REPO:$CI_COMMIT_SHA'
- docker push $REGISTRY_REPO:latest - 'docker push $REGISTRY_REPO:latest'
- 'docker push $REGISTRY_REPO:$APP_VERSION'
- echo "✓ Images pushed successfully" - echo "✓ Images pushed successfully"
depends_on: depends_on:
- 'Docker image build' - 'Docker image build'

View File

@@ -103,9 +103,9 @@ http {
access_log off; access_log off;
} }
# Default location # Default location: try $uri.html so /services serves services.html, /services/foo serves services/foo.html
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri.html $uri/ /index.html;
} }
# 410 Gone: permanently removed URLs (tells crawlers to deindex) # 410 Gone: permanently removed URLs (tells crawlers to deindex)

View File

@@ -1,6 +1,6 @@
{ {
"name": "mifi-ventures-landing", "name": "mifi-ventures-landing",
"version": "3.0.0", "version": "4.0.3",
"private": true, "private": true,
"repository": "https://git.mifi.dev/mifi-ventures/landing.git", "repository": "https://git.mifi.dev/mifi-ventures/landing.git",
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268", "packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
@@ -12,18 +12,18 @@
"dev": "vite dev", "dev": "vite dev",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write \"src/**/*.{ts,js,svelte,css,json}\"", "format": "prettier --write \"src/**/*.{ts,js,svelte,css,json}\" \"static/**/*.{css,html,js}\"",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"lint:css": "stylelint \"src/**/*.css\" \"src/**/*.svelte\"", "lint:css": "stylelint \"src/**/*.css\" \"src/**/*.svelte\" \"static/**/*.css\"",
"lint:css:fix": "stylelint \"src/**/*.css\" \"src/**/*.svelte\" --fix", "lint:css:fix": "stylelint \"src/**/*.css\" \"src/**/*.svelte\" \"static/**/*.css\" --fix",
"preview": "serve dist -p 4173", "preview": "serve dist -p 4173",
"test": "vitest run", "test": "vitest run",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:e2e": "bash scripts/run-e2e.sh", "test:e2e": "bash scripts/run-e2e.sh",
"test:e2e:update-snapshots": "bash scripts/update-e2e-snapshots.sh", "test:e2e:update-snapshots": "bash scripts/update-e2e-snapshots.sh",
"test:all": "vitest run && playwright test", "test:all": "vitest run && playwright test",
"test:ci": "pnpm run lint && pnpm run lint:css && pnpm exec svelte-kit sync && pnpm test && pnpm run test:e2e", "test:ci": "pnpm run lint && pnpm run lint:css && pnpm exec svelte-kit sync && pnpm test && pnpm run test:e2e && pnpm install --config.confirmModulesPurge=false",
"test:watch": "vitest", "test:watch": "vitest",
"prepare": "husky" "prepare": "husky"
}, },

View File

@@ -29,13 +29,35 @@ async function main() {
logLevel: 'warn', logLevel: 'warn',
}); });
const files = fs.readdirSync(DIST).filter((f) => f.endsWith('.html')); const rootFiles = fs.readdirSync(DIST)
for (const file of files) { .filter((f) => f.endsWith('.html'))
const filePath = path.join(DIST, file); .map((f) => path.join(DIST, f));
const servicesDir = path.join(DIST, 'services');
const serviceFiles = [];
if (fs.existsSync(servicesDir)) {
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.html')) {
serviceFiles.push(fullPath);
}
}
};
walk(servicesDir);
}
const files = [...rootFiles, ...serviceFiles];
for (const filePath of files) {
let html = fs.readFileSync(filePath, 'utf8'); let html = fs.readFileSync(filePath, 'utf8');
html = await beasties.process(html); html = await beasties.process(html);
fs.writeFileSync(filePath, html, 'utf8'); fs.writeFileSync(filePath, html, 'utf8');
console.log('✓ Critical CSS inlined → dist/' + file); console.log('✓ Critical CSS inlined → dist/' + path.relative(DIST, filePath));
} }
console.log('Critical CSS step complete.'); console.log('Critical CSS step complete.');

View File

@@ -24,4 +24,5 @@ docker run --rm \
npx serve dist -p 4173 & npx serve dist -p 4173 &
sleep 2 sleep 2
pnpm exec playwright test pnpm exec playwright test
pnpm install --config.confirmModulesPurge=false
' '

View File

@@ -11,7 +11,9 @@ if [ -n "$CI" ]; then
fi fi
if command -v docker >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
exec bash "$(dirname "$0")/run-e2e-in-docker.sh" bash "$(dirname "$0")/run-e2e-in-docker.sh"
pnpm install --config.confirmModulesPurge=false
exit 0
fi fi
# No Docker: run in current environment (e.g. devcontainer; same image as CI) # No Docker: run in current environment (e.g. devcontainer; same image as CI)

View File

@@ -28,6 +28,7 @@ if command -v docker >/dev/null 2>&1; then
sleep 2 sleep 2
pnpm exec playwright test --update-snapshots pnpm exec playwright test --update-snapshots
' '
pnpm install --config.confirmModulesPurge=false
else else
echo "Updating snapshots in the current environment (matches CI when using the devcontainer)." echo "Updating snapshots in the current environment (matches CI when using the devcontainer)."
echo "" echo ""

View File

@@ -32,6 +32,7 @@
--font-family-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif; --font-family-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
--font-size-base: 18px; --font-size-base: 18px;
--font-size-xs: 14px;
--font-size-small: 15px; --font-size-small: 15px;
--font-size-medium: 16px; --font-size-medium: 16px;
--font-size-large: 20px; --font-size-large: 20px;
@@ -130,6 +131,50 @@
} }
} }
/* ========================================
Local Font Faces
======================================== */
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-700.woff2') format('woff2');
}
@font-face {
font-family: Fraunces;
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-600.woff2') format('woff2');
}
@font-face {
font-family: Fraunces;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-700.woff2') format('woff2');
}
/* ======================================== /* ========================================
Base Styles Base Styles
======================================== */ ======================================== */
@@ -177,6 +222,11 @@ body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
strong,
b {
font-weight: var(--font-weight-bold);
}
/* ======================================== /* ========================================
Skip Link (Accessibility) Skip Link (Accessibility)
======================================== */ ======================================== */
@@ -382,6 +432,14 @@ a {
&:active { &:active {
transform: translateY(0); transform: translateY(0);
} }
&.small {
border-radius: var(--border-radius-small);
font-size: var(--font-size-xs);
min-height: 36px;
min-width: fit-content;
padding: 0.5rem 1rem;
}
} }
.icon-button { .icon-button {

View File

@@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state';
import type { FaqList } from '$lib/types/faq'; import type { FaqList } from '$lib/types/faq';
const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}${page.url?.pathname ?? '/'}`;
const { const {
faqList, faqList,
title = 'FAQ', title = 'FAQ',
@@ -8,6 +12,32 @@
faqList: FaqList; faqList: FaqList;
title?: string; title?: string;
} = $props(); } = $props();
const faqPageJsonLd = $derived(
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': `${PAGE_URL}#faq`,
mainEntity: faqList.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
}),
);
const jsonLdHtml = $derived(
faqPageJsonLd
? '<scr' +
'ipt type="application/ld+json">' +
faqPageJsonLd +
'</scr' +
'ipt>'
: '',
);
</script> </script>
<section id="faq" class="section faq" aria-labelledby="faq-heading"> <section id="faq" class="section faq" aria-labelledby="faq-heading">
@@ -20,6 +50,10 @@
{/each} {/each}
</dl> </dl>
</div> </div>
{#if jsonLdHtml}
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD -->
{@html jsonLdHtml}
{/if}
</section> </section>
<style> <style>

View File

@@ -53,6 +53,13 @@
Terms of Service Terms of Service
</a> </a>
</nav> </nav>
<p class="legal-notice">
We improve our products and advertising by using Google Analytics and
Microsoft Clarity to see how you use our website. By using our site, you agree
that we and Microsoft can collect and use this data. Our <a
href="/privacy-policy">privacy policy</a
> has more details.
</p>
</div> </div>
</footer> </footer>
@@ -82,4 +89,22 @@
justify-content: center; justify-content: center;
gap: var(--space-xs); gap: var(--space-xs);
} }
.legal-notice {
margin-top: var(--space-md);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
color: var(--color-text-tertiary);
max-width: 100%;
}
.legal-notice a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 0.2em;
}
.legal-notice a:hover {
color: var(--color-primary-hover);
}
</style> </style>

View File

@@ -32,7 +32,10 @@
<span <span
class={['mobile nav-header-logo', { 'page-home': bodyClass === 'page-home' }]} class={['mobile nav-header-logo', { 'page-home': bodyClass === 'page-home' }]}
> >
<a href="/" class="logo-link">
<Wordmark /> <Wordmark />
<span class="sr-only">mifi Ventures home page</span>
</a>
</span> </span>
<button <button
type="button" type="button"
@@ -64,7 +67,7 @@
]} ]}
> >
{#if page !== 'home'} {#if page !== 'home'}
<a href="/"> <a href="/" class="logo-link">
<Wordmark /> <Wordmark />
<span class="sr-only">mifi Ventures home page</span> <span class="sr-only">mifi Ventures home page</span>
</a> </a>
@@ -155,6 +158,15 @@
display: inline-block; display: inline-block;
} }
} }
& .logo-link {
text-decoration: none;
&:hover {
text-decoration: none;
border-bottom: none;
}
}
} }
/* Hamburger toggle: mobile only, animates to X when open */ /* Hamburger toggle: mobile only, animates to X when open */

View File

@@ -1,9 +1,11 @@
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from './json-ld'; import { baseJsonLdGraph, catalogJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { homepageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
export const homeMeta: PageMeta = { export const homeMeta: PageMeta = {
title: 'mifi Ventures — Hands-On Product Architecture for Early-Stage SaaS | Boston, MA', title: 'mifi Ventures — Hands-On Product Architecture for Early-Stage SaaS | Boston, MA',
description: description:
'Product architecture and senior software engineering for early-stage SaaS teams. Mike Fitzpatrick works inside your codebase to build foundations that ship fast without structural debt—frontend systems, MVP launch, fractional CTO, and stage-aligned infrastructure.', 'Product architecture and senior software engineering for early-stage SaaS teams. Mike Fitzpatrick works inside your codebase to build foundations that ship fast without structural debt—frontend systems, MVP launch, fractional CTO, and stage-aligned infrastructure.',
jsonLd: defaultJsonLdGraph, canonical: 'https://mifi.ventures',
jsonLd: [...baseJsonLdGraph, homepageJsonLdGraph, ...catalogJsonLdGraph],
}; };

View File

@@ -1,166 +0,0 @@
/**
* Default JSON-LD graph nodes (Organization, Person, WebSite, WebPage, OfferCatalog).
* Used for the home page; other pages can add or override via meta.jsonLd.
*/
const BASE = 'https://mifi.ventures';
export const defaultJsonLdGraph: Record<string, unknown>[] = [
{
'@type': 'Organization',
'@id': `${BASE}/#organization`,
name: 'mifi Ventures, LLC',
legalName: 'mifi Ventures, LLC',
url: `${BASE}/`,
logo: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
description:
'Hands-on product architecture for early-stage SaaS: frontend systems, MVP launch support, fractional CTO guidance, and stage-aligned infrastructure strategy.',
founder: { '@id': `${BASE}/#principal` },
address: {
'@type': 'PostalAddress',
addressLocality: 'Boston',
addressRegion: 'MA',
addressCountry: 'US',
},
geo: { '@type': 'GeoCoordinates', latitude: 42.360082, longitude: -71.05888 },
areaServed: { '@type': 'Country', name: 'United States' },
hasOfferCatalog: { '@id': `${BASE}/#services` },
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
},
{
'@type': 'Person',
'@id': `${BASE}/#principal`,
name: 'Mike Fitzpatrick',
jobTitle: 'Product Architect and Senior Software Engineer',
description:
'Hands-on technical partner for early-stage SaaS teams; works inside codebases to build foundations that ship fast without structural debt.',
url: `${BASE}/`,
worksFor: { '@id': `${BASE}/#organization` },
knowsAbout: [
'Frontend Architecture',
'UI Architecture',
'React Development',
'Web Performance Optimization',
'Core Web Vitals',
'Technical SEO',
'Web Accessibility (WCAG)',
'Component Libraries',
'Design Systems',
'JavaScript',
'TypeScript',
'Modern Web Development',
'Greenfield Product Development',
'Legacy System Modernization',
'Code Refactoring',
],
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
},
{
'@type': 'WebSite',
'@id': `${BASE}/#website`,
url: `${BASE}/`,
name: 'mifi Ventures',
description: 'Hands-on product architecture for early-stage SaaS — Boston, MA',
publisher: { '@id': `${BASE}/#organization` },
potentialAction: {
'@type': 'ReserveAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://cal.mifi.ventures/the-mifi',
},
name: 'Schedule a 30-minute intro call',
},
},
{
'@type': 'WebPage',
'@id': `${BASE}/#webpage`,
url: `${BASE}/`,
name: 'mifi Ventures — Hands-On Product Architecture for Early-Stage SaaS | Boston, MA',
description:
'Product architecture and senior software engineering for early-stage SaaS. Ship fast without structural debt—frontend systems, MVP launch, fractional CTO, stage-aligned infrastructure.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
mainEntity: { '@id': `${BASE}/#organization` },
primaryImageOfPage: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
inLanguage: 'en-US',
},
{
'@type': 'ProfessionalService',
'@id': `${BASE}/#professional-service`,
name: 'mifi Ventures',
url: `${BASE}/`,
description:
'Hands-on product architecture for early-stage SaaS teams: product architecture, MVP launch support, fractional CTO, and stage-aligned infrastructure.',
serviceType: [
'Product Architecture',
'SaaS Engineering Consulting',
'Fractional CTO Services',
'Startup Infrastructure Strategy',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
{
'@type': 'OfferCatalog',
'@id': `${BASE}/#services`,
name: 'Software Engineering Consulting Services',
description: 'Consulting services offered by mifi Ventures',
numberOfItems: 6,
itemListElement: [
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'Frontend and UI Architecture',
description:
'Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability.',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'Greenfield Product Development',
description:
'Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten.',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'Performance Optimization',
description:
'Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys.',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'Accessibility Engineering',
description:
'Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns.',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'System Modernization',
description:
'Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases.',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: 'End-to-End Feature Delivery',
description:
'End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity.',
},
},
],
},
];

View File

@@ -0,0 +1,190 @@
import { BASE, PHONE, WORDMARK } from './constants';
/**
* Default JSON-LD graph nodes (Organization, Person, WebSite, WebPage,
* ProfessionalService, OfferCatalog).
*
* Used for the home page; other pages can add or override via meta.jsonLd.
*/
export const baseJsonLdGraph: Record<string, unknown>[] = [
{
'@type': 'Organization',
'@id': `${BASE}/#organization`,
name: 'mifi Ventures',
legalName: 'mifi Ventures LLC',
url: `${BASE}/`,
logo: {
'@type': 'ImageObject',
url: WORDMARK,
},
image: {
'@type': 'ImageObject',
url: WORDMARK,
},
telephone: PHONE,
description:
'Hands-on product architecture for early-stage SaaS teams: SaaS product architecture, MVP architecture and launch support, fractional CTO guidance, and stage-aligned infrastructure strategy.',
founder: { '@id': `${BASE}/#principal` },
address: {
'@type': 'PostalAddress',
addressLocality: 'Boston',
addressRegion: 'MA',
addressCountry: 'US',
},
areaServed: { '@type': 'Country', name: 'United States' },
hasOfferCatalog: { '@id': `${BASE}/#services` },
sameAs: [
'https://www.linkedin.com/company/mifi-ventures',
'https://github.com/mifi-ventures',
],
contactPoint: [
{
'@type': 'ContactPoint',
contactType: 'sales',
telephone: PHONE,
areaServed: 'US',
availableLanguage: ['en'],
},
],
},
{
'@type': 'Person',
'@id': `${BASE}/#principal`,
name: 'Mike Fitzpatrick',
jobTitle: 'Product Architect and Senior Software Engineer',
description:
'Hands-on technical partner for early-stage SaaS teams; works inside codebases to build strong product foundations that ship fast without structural debt.',
url: `${BASE}/`,
worksFor: { '@id': `${BASE}/#organization` },
knowsAbout: [
'Product Architecture',
'Frontend Architecture',
'UI Architecture',
'React Development',
'JavaScript',
'TypeScript',
'Component Libraries',
'Design Systems',
'Design Tokens',
'CSS Architecture',
'Web Accessibility (WCAG)',
'Technical SEO',
'Core Web Vitals',
'MVP Architecture',
'Greenfield Product Development',
'Fractional CTO',
'Startup Infrastructure Strategy',
'Code Refactoring',
],
sameAs: [
'https://www.linkedin.com/in/the-mifi',
'https://github.com/the-mifi',
'https://mifi.dev',
],
},
{
'@type': 'WebSite',
'@id': `${BASE}/#website`,
url: `${BASE}/`,
name: 'mifi Ventures',
description: 'Hands-on product architecture for early-stage SaaS teams.',
publisher: { '@id': `${BASE}/#organization` },
image: {
'@type': 'ImageObject',
url: WORDMARK,
},
potentialAction: {
'@type': 'ReserveAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://cal.mifi.ventures/the-mifi',
},
name: 'Schedule a 30-minute intro call',
},
},
];
export const catalogJsonLdGraph: Record<string, unknown>[] = [
{
'@type': 'ProfessionalService',
'@id': `${BASE}/#professional-service`,
name: 'mifi Ventures',
url: `${BASE}/`,
image: {
'@type': 'ImageObject',
url: WORDMARK,
},
logo: {
'@type': 'ImageObject',
url: WORDMARK,
},
telephone: PHONE,
description:
'Hands-on product architecture and senior software engineering for early-stage SaaS teams: SaaS product architecture, MVP architecture and launch, fractional CTO guidance, and stage-aligned infrastructure.',
serviceType: [
'SaaS Product Architecture',
'MVP Architecture and Launch Consulting',
'Fractional CTO Services',
'Startup Infrastructure Strategy',
],
provider: { '@id': `${BASE}/#organization` },
address: {
'@type': 'PostalAddress',
addressLocality: 'Boston',
addressRegion: 'MA',
addressCountry: 'US',
},
areaServed: { '@type': 'Country', name: 'United States' },
},
{
'@type': 'OfferCatalog',
'@id': `${BASE}/#services`,
name: 'SaaS Architecture Services',
description:
'Consulting services offered by mifi Ventures for early-stage SaaS teams.',
numberOfItems: 4,
itemListElement: [
{
'@type': 'Offer',
url: `${BASE}/hands-on-saas-architecture-consultant`,
itemOffered: {
'@type': 'Service',
name: 'Hands-On SaaS Architecture',
description:
'Hands-on product architecture for early-stage SaaS teams, focused on frontend systems, reusable components, design tokens, accessibility, and scalable foundations for fast iteration.',
},
},
{
'@type': 'Offer',
url: `${BASE}/mvp-architecture-and-launch`,
itemOffered: {
'@type': 'Service',
name: 'MVP Architecture & Launch',
description:
'Architecture and implementation support for early-stage SaaS MVPs that need to ship quickly without creating structural debt, fragile CSS, or difficult-to-extend product foundations.',
},
},
{
'@type': 'Offer',
url: `${BASE}/fractional-cto-for-early-stage-saas`,
itemOffered: {
'@type': 'Service',
name: 'Fractional CTO / Technical Partner',
description:
'Part-time technical leadership for early-stage SaaS teams that need architectural guidance, tradeoff evaluation, and hands-on technical oversight without hiring a full-time CTO.',
},
},
{
'@type': 'Offer',
url: `${BASE}/stage-aligned-infrastructure`,
itemOffered: {
'@type': 'Service',
name: 'Stage-Aligned Infrastructure',
description:
'Infrastructure strategy for early-stage SaaS teams that aligns cloud, SaaS, and deployment decisions with company stage, reducing unnecessary complexity and preserving runway.',
},
},
],
},
];

View File

@@ -0,0 +1,3 @@
export const BASE = 'https://mifi.ventures';
export const WORDMARK = `${BASE}/assets/wordmark.svg`;
export const PHONE = '+1-888-991-6434';

View File

@@ -0,0 +1,68 @@
import { BASE } from './constants';
export const handsOnSaaSArchitectureConsultantServiceJsonLdGraph: Record<
string,
unknown
> = {
'@type': 'Service',
'@id': `${BASE}/hands-on-saas-architecture-consultant#service`,
name: 'Hands-On SaaS Architecture',
url: `${BASE}/hands-on-saas-architecture-consultant`,
description:
'Hands-on product architecture for early-stage SaaS teams, focused on frontend systems, reusable components, design tokens, accessibility, and scalable foundations for fast iteration.',
serviceType: 'SaaS Product Architecture',
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
audience: {
'@type': 'Audience',
audienceType: 'Early-stage SaaS founders and engineering teams',
},
};
export const mvpArchitectureAndLaunchServiceJsonLdGraph: Record<string, unknown> = {
'@type': 'Service',
'@id': `${BASE}/mvp-architecture-and-launch#service`,
name: 'MVP Architecture & Launch',
url: `${BASE}/mvp-architecture-and-launch`,
description:
'Architecture and implementation support for early-stage SaaS MVPs that need to ship quickly without structural debt, fragile CSS, or difficult-to-extend product foundations.',
serviceType: 'MVP Architecture and Launch Consulting',
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
audience: {
'@type': 'Audience',
audienceType: 'Founder-led SaaS startups and early product teams',
},
};
export const fractionalCtoForEarlyStageSaaSServiceJsonLdGraph: Record<string, unknown> = {
'@type': 'Service',
'@id': `${BASE}/fractional-cto-for-early-stage-saas#service`,
name: 'Fractional CTO / Technical Partner',
url: `${BASE}/fractional-cto-for-early-stage-saas`,
description:
'Part-time technical leadership for early-stage SaaS teams that need architectural guidance, tradeoff evaluation, and hands-on technical oversight without hiring a full-time CTO.',
serviceType: 'Fractional CTO Services',
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
audience: {
'@type': 'Audience',
audienceType: 'Early-stage SaaS founders and growing engineering teams',
},
};
export const stageAlignedInfrastructureServiceJsonLdGraph: Record<string, unknown> = {
'@type': 'Service',
'@id': `${BASE}/stage-aligned-infrastructure#service`,
name: 'Stage-Aligned Infrastructure',
url: `${BASE}/stage-aligned-infrastructure`,
description:
'Infrastructure strategy for early-stage SaaS teams that aligns cloud, SaaS, and deployment decisions with company stage, reducing unnecessary complexity and preserving runway.',
serviceType: 'Startup Infrastructure Strategy',
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
audience: {
'@type': 'Audience',
audienceType: 'Early-stage SaaS founders and small engineering teams',
},
};

View File

@@ -0,0 +1,130 @@
import { BASE, WORDMARK } from './constants';
export const homepageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/#webpage`,
url: `${BASE}/`,
name: 'mifi Ventures | Hands-On Product Architecture for Early-Stage SaaS',
description:
'Hands-on product architecture for early-stage SaaS teams—SaaS architecture, MVP launch support, fractional CTO guidance, and stage-aligned infrastructure strategy.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
mainEntity: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const servicesPageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/services#webpage`,
url: `${BASE}/services`,
name: 'SaaS Architecture Services | mifi Ventures',
description:
'Hands-on SaaS architecture consulting for early-stage teams. Services include product architecture, MVP launch support, fractional CTO guidance, and stage-aligned infrastructure strategy.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const handsOnSaaSArchitectureConsultantPageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/hands-on-saas-architecture-consultant#webpage`,
url: `${BASE}/hands-on-saas-architecture-consultant`,
name: 'SaaS Product Architecture Consultant | mifi Ventures',
description:
'Hands-on product architecture for early-stage SaaS teams. Build frontend foundations that scale with reusable components, design tokens, accessibility, and clean architecture.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const mvpArchitectureAndLaunchPageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/mvp-architecture-and-launch#webpage`,
url: `${BASE}/mvp-architecture-and-launch`,
name: 'MVP Architecture & Launch Consultant | mifi Ventures',
description:
'Architecture and implementation support for early-stage SaaS MVPs that need to ship quickly without structural debt, fragile CSS, or difficult-to-extend product foundations.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const fractionalCtoForEarlyStageSaaSPageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/fractional-cto-for-early-stage-saas#webpage`,
url: `${BASE}/fractional-cto-for-early-stage-saas`,
name: 'Fractional CTO for Early-Stage SaaS | mifi Ventures',
description:
'Hands-on technical leadership for early-stage SaaS teams that need architectural guidance, technical tradeoff evaluation, and senior oversight without hiring a full-time CTO.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const stageAlignedInfrastructurePageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/stage-aligned-infrastructure#webpage`,
url: `${BASE}/stage-aligned-infrastructure`,
name: 'Stage-Aligned Infrastructure for Startups | mifi Ventures',
description:
'Infrastructure strategy for early-stage SaaS teams that aligns cloud, SaaS, and deployment decisions with company stage, reducing unnecessary complexity and preserving runway.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const privacyPolicyPageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/privacy-policy#webpage`,
url: `${BASE}/privacy-policy`,
name: 'Privacy Policy | mifi Ventures',
description:
'Read the mifi Ventures privacy policy, including how personal information is collected, used, stored, and protected.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};
export const termsOfServicePageJsonLdGraph: Record<string, unknown> = {
'@type': 'WebPage',
'@id': `${BASE}/terms-of-service#webpage`,
url: `${BASE}/terms-of-service`,
name: 'Terms of Service | mifi Ventures',
description:
'Read the mifi Ventures terms of service, including the terms governing use of the site and consulting services.',
isPartOf: { '@id': `${BASE}/#website` },
about: { '@id': `${BASE}/#organization` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: WORDMARK,
},
inLanguage: 'en-US',
};

View File

@@ -1,20 +1,27 @@
/** /**
* Privacy Policy content for mifi Ventures. Used by /privacy-policy. * Privacy Policy content for mifi Ventures. Used by /privacy-policy.
* Last updated: March 5, 2026. Includes Messaging Policy (SMS) for OpenPhone / A2P compliance. * Last updated: March 12, 2026. Includes Messaging Policy (SMS) for OpenPhone / A2P compliance; Microsoft Clarity and Google Analytics.
*/ */
export interface LegalSectionLink {
href: string;
label: string;
}
export interface LegalSection { export interface LegalSection {
id: string; id: string;
heading: string; heading: string;
body: string[]; body: string[];
list?: string[]; list?: string[];
/** Optional links to inject into body paragraphs (URLs in text become <a> with this label) */
links?: LegalSectionLink[];
/** Numbered sub-sections (e.g. Messaging Policy 16) */ /** Numbered sub-sections (e.g. Messaging Policy 16) */
subsections?: { title: string; body: string[]; list?: string[] }[]; subsections?: { title: string; body: string[]; list?: string[] }[];
} }
export const privacyPolicy = { export const privacyPolicy = {
title: 'Privacy Policy', title: 'Privacy Policy',
lastUpdated: 'March 5, 2026', lastUpdated: 'March 12, 2026',
intro: [ intro: [
'mifi Ventures LLC respects your privacy and is committed to protecting personal information shared through this website and related communications.', 'mifi Ventures LLC respects your privacy and is committed to protecting personal information shared through this website and related communications.',
'This policy explains what information we collect, how it is used, and how it is protected.', 'This policy explains what information we collect, how it is used, and how it is protected.',
@@ -56,6 +63,33 @@ export const privacyPolicy = {
'This information is used only to maintain the website, improve performance, and monitor security.', 'This information is used only to maintain the website, improve performance, and monitor security.',
], ],
}, },
{
id: 'analytics-and-tracking',
heading: 'Analytics and Tracking Technologies',
body: [
'We use a mix of first-party and third-party analytics tools to understand how visitors use this website and to improve our services.',
'We use Umami as a first-party analytics tool to measure aggregate usage of this site for internal reporting and performance insights; Umami data is not sold or shared for third-party advertising.',
'We partner with Microsoft Clarity and Microsoft Advertising to capture how you use and interact with our website through behavioral metrics, heatmaps, and session replay to improve and market our products and services. Website usage data is captured using first- and third-party cookies and similar tracking technologies to determine the popularity of content and online activity. We also use this information for site optimization, security and fraud detection, and advertising. These third-party tools only run if you consent to non-essential analytics via our cookie banner.',
'For more information about how Microsoft collects and uses your data, see the Microsoft Privacy Statement: https://www.microsoft.com/privacy/privacystatement.',
'We also use Google Analytics (only if you consent) to collect information about website usage, such as pages visited, time on site, and browser and device information. Google Analytics uses cookies and similar technologies to help us analyze how visitors use the site and to compile aggregated statistics.',
'You can learn more about how Google handles data in Google Analytics at: https://policies.google.com/privacy and https://policies.google.com/technologies/partner-sites.',
'We store your analytics preference (for example, whether you accepted or rejected non-essential analytics) in a small piece of first-party device storage so we can remember your choice on future visits.',
],
links: [
{
href: 'https://www.microsoft.com/privacy/privacystatement',
label: 'Microsoft Privacy Statement',
},
{
href: 'https://policies.google.com/privacy',
label: 'Google Privacy Policy',
},
{
href: 'https://policies.google.com/technologies/partner-sites',
label: 'How Google uses data from sites and apps',
},
],
},
{ {
id: 'how-we-use', id: 'how-we-use',
heading: 'How We Use Information', heading: 'How We Use Information',
@@ -165,6 +199,7 @@ export const privacyPolicy = {
heading: 'Your Rights', heading: 'Your Rights',
body: [ body: [
'Depending on your jurisdiction, you may have the right to: request access to personal data; request correction or deletion of data; withdraw consent for communications.', 'Depending on your jurisdiction, you may have the right to: request access to personal data; request correction or deletion of data; withdraw consent for communications.',
'If you are located in a region with specific data protection laws (such as the European Economic Area or the United Kingdom), you may have additional rights under those laws; we will handle such requests in line with applicable legal requirements.',
'Requests may be submitted using the contact information below.', 'Requests may be submitted using the contact information below.',
], ],
list: [ list: [

View File

@@ -1,6 +1,6 @@
/** /**
* Terms of Service content for mifi Ventures. Used by /terms-of-service. * Terms of Service content for mifi Ventures. Used by /terms-of-service.
* Last updated: March 5, 2026. OpenPhone / A2P messaging compliance. * Last updated: March 12, 2026. OpenPhone / A2P messaging compliance; data protection positioning.
*/ */
export interface LegalSection { export interface LegalSection {
@@ -12,7 +12,7 @@ export interface LegalSection {
export const termsOfService = { export const termsOfService = {
title: 'Terms of Service', title: 'Terms of Service',
lastUpdated: 'March 5, 2026', lastUpdated: 'March 12, 2026',
intro: [ intro: [
'These Terms of Service govern the use of the website operated by mifi Ventures LLC ("mifi Ventures", "we", "our", or "us").', 'These Terms of Service govern the use of the website operated by mifi Ventures LLC ("mifi Ventures", "we", "our", or "us").',
'By accessing or using this website, you agree to these terms. If you do not agree, you should not use this website.', 'By accessing or using this website, you agree to these terms. If you do not agree, you should not use this website.',
@@ -71,6 +71,14 @@ export const termsOfService = {
'This website may reference or integrate with third-party platforms or services. mifi Ventures is not responsible for the privacy practices or content of external services.', 'This website may reference or integrate with third-party platforms or services. mifi Ventures is not responsible for the privacy practices or content of external services.',
], ],
}, },
{
id: 'data-protection-and-privacy',
heading: 'Data Protection and Privacy',
body: [
'Your use of this website is also subject to our Privacy Policy, which explains what information we collect, how it is used, and your choices. By using this website, you acknowledge that you have reviewed the Privacy Policy.',
'mifi Ventures is based in the United States and primarily serves U.S.-based clients. However, if you are located in a region with specific data protection laws (such as the European Economic Area or the United Kingdom), we will handle personal data in accordance with applicable data protection requirements to the extent they apply and as described in the Privacy Policy.',
],
},
{ {
id: 'limitation-of-liability', id: 'limitation-of-liability',
heading: 'Limitation of Liability', heading: 'Limitation of Liability',
@@ -78,6 +86,14 @@ export const termsOfService = {
'To the fullest extent permitted by law, mifi Ventures LLC shall not be liable for indirect, incidental, or consequential damages arising from use of this website or related services.', 'To the fullest extent permitted by law, mifi Ventures LLC shall not be liable for indirect, incidental, or consequential damages arising from use of this website or related services.',
], ],
}, },
{
id: 'governing-law',
heading: 'Governing Law and Jurisdiction',
body: [
'These Terms of Service are governed by the laws of the Commonwealth of Massachusetts and applicable federal law of the United States, without regard to conflict of law principles.',
'Any disputes arising out of or relating to these terms or your use of this website shall be brought exclusively in the state or federal courts located in Massachusetts, except where applicable data protection laws provide you with mandatory rights to bring claims in another jurisdiction.',
],
},
{ {
id: 'changes', id: 'changes',
heading: 'Changes to These Terms', heading: 'Changes to These Terms',

View File

@@ -20,17 +20,15 @@
}) })
: '', : '',
); );
const jsonLdHtml = $derived( const jsonLdHtml = $derived(
jsonLdScript jsonLdScript
? '<script type="application/ld+json">' + jsonLdScript + '</scr' + 'ipt>' ? '<scr' + 'ipt type="application/ld+json">' + jsonLdScript + '</scr' + 'ipt>'
: '', : '',
); );
</script> </script>
<svelte:head> <svelte:head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"></script>
<script defer src="/assets/js/ga-init.js"></script>
<script <script
defer defer
src="https://analytics.mifi.holdings/script.js" src="https://analytics.mifi.holdings/script.js"
@@ -49,13 +47,6 @@
type="font/woff2" type="font/woff2"
crossorigin="anonymous" crossorigin="anonymous"
/> />
<link
rel="preload"
href="/assets/fonts/fraunces-v38-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link <link
rel="preload" rel="preload"
href="/assets/fonts/inter-v20-latin-regular.woff2" href="/assets/fonts/inter-v20-latin-regular.woff2"
@@ -63,13 +54,6 @@
type="font/woff2" type="font/woff2"
crossorigin="anonymous" crossorigin="anonymous"
/> />
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-italic.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link <link
rel="preload" rel="preload"
href="/assets/fonts/inter-v20-latin-500.woff2" href="/assets/fonts/inter-v20-latin-500.woff2"
@@ -148,6 +132,7 @@
<script src="/assets/js/copyright-year.js" defer></script> <script src="/assets/js/copyright-year.js" defer></script>
<script src="/assets/js/mobile-menu-helper.js" defer></script> <script src="/assets/js/mobile-menu-helper.js" defer></script>
<script src="/assets/js/cookie-consent.js" defer></script>
</svelte:head> </svelte:head>
<a href="#main" class="skip-link" data-umami-event="skip to main content" <a href="#main" class="skip-link" data-umami-event="skip to main content"
@@ -155,6 +140,24 @@
> >
{@render children()} {@render children()}
<Footer /> <Footer />
<div id="cookie-banner" class="cookie-banner" role="region" aria-label="Cookie consent">
<div class="cookie-banner-content">
<p class="cookie-notification-text">
We use first-party analytics and, if you accept, third-party tools (e.g.
Google, Microsoft) to understand usage and improve this site. You can accept
all or reject non-essential analytics.
<a href="/privacy-policy#analytics-and-tracking">Learn more</a>.
</p>
<div class="cookie-banner-actions">
<button type="button" class="btn btn-primary small" data-consent="accept"
>Accept all</button
>
<button type="button" class="btn btn-secondary small" data-consent="reject"
>Reject non-essential</button
>
</div>
</div>
</div>
<img <img
src="https://analytics.mifi.holdings/p/wQ9GYnLIg" src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
alt="" alt=""
@@ -163,3 +166,105 @@
role="presentation" role="presentation"
loading="eager" loading="eager"
/> />
<style>
.cookie-banner {
position: fixed;
inset-inline: 0;
bottom: 0;
z-index: 1000;
display: none;
background-color: var(--color-bg-elevated, var(--color-bg));
color: var(--color-text);
border-top: 1px solid var(--color-border);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.16);
&:global(.is-visible) {
display: block;
}
}
.cookie-banner-content {
container: cookie-banner / inline-size;
max-width: var(--max-narrow-width);
margin: 0 auto;
padding: var(--space-md) var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-sm);
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.cookie-notification-text {
margin: 0;
font-size: var(--font-size-small);
line-height: var(--line-height-base);
@container cookie-banner (width >= 644px) {
flex: 1 1 auto;
}
@container cookie-banner (width < 644px) {
flex: 0 0 100%;
text-align: center;
width: 100%;
}
& a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 0.16em;
&:hover {
color: var(--color-primary-hover);
}
}
}
.cookie-banner-actions {
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
@container cookie-banner (width >= 644px) {
flex: 0 0 auto;
gap: var(--space-xs);
}
@container cookie-banner (width < 644px) {
flex: 0 0 100%;
gap: var(--space-sm);
justify-content: center;
width: 100%;
& .btn {
max-width: 100%;
width: 100%;
}
}
& [data-consent='accept'] {
background-color: var(--color-primary);
color: var(--color-bg);
&:hover {
background-color: var(--color-primary-hover);
}
}
& [data-consent='reject'] {
background-color: transparent;
color: var(--color-text);
border-color: var(--color-primary);
&:hover {
background-color: var(--color-surface-subtle, rgba(148, 163, 184, 0.16));
}
}
}
</style>

View File

@@ -1,8 +1,30 @@
<script lang="ts"> <script lang="ts">
import Navigation from '$lib/components/Navigation.svelte'; import Navigation from '$lib/components/Navigation.svelte';
import { privacyPolicy } from '$lib/data/privacy-policy'; import { privacyPolicy } from '$lib/data/privacy-policy';
import type { LegalSectionLink } from '$lib/data/privacy-policy';
const navItems = [{ label: 'Home', href: '/', umamiEventLabel: 'home' }]; const navItems = [{ label: 'Home', href: '/', umamiEventLabel: 'home' }];
type BodySegment = string | { type: 'link'; href: string; label: string };
/** Splits a paragraph by link URLs and returns text/link segments for rendering. */
function linkify(para: string, links: LegalSectionLink[]): BodySegment[] {
const matches: { index: number; link: LegalSectionLink }[] = [];
for (const link of links) {
const idx = para.indexOf(link.href);
if (idx !== -1) matches.push({ index: idx, link });
}
matches.sort((a, b) => a.index - b.index);
const result: BodySegment[] = [];
let last = 0;
for (const { index, link } of matches) {
if (index > last) result.push(para.slice(last, index));
result.push({ type: 'link', href: link.href, label: link.label });
last = index + link.href.length;
}
if (last < para.length) result.push(para.slice(last));
return result.length ? result : [para];
}
</script> </script>
<Navigation items={navItems} page="privacy-policy" /> <Navigation items={navItems} page="privacy-policy" />
@@ -28,7 +50,9 @@
<h2 id={`${section.id}-heading`}>{section.heading}</h2> <h2 id={`${section.id}-heading`}>{section.heading}</h2>
{#each section.body as para} {#each section.body as para}
<p> <p>
{#if section.id === 'contact' && para === 'legal@mifi.ventures'} {#if section.id === 'contact' && para === 'mifi Ventures LLC'}
<strong>{para}</strong>
{:else if section.id === 'contact' && para === 'legal@mifi.ventures'}
<a href="mailto:legal@mifi.ventures" <a href="mailto:legal@mifi.ventures"
>legal@mifi.ventures</a >legal@mifi.ventures</a
> >
@@ -36,6 +60,16 @@
<a href="https://mifi.ventures" rel="noopener noreferrer" <a href="https://mifi.ventures" rel="noopener noreferrer"
>https://mifi.ventures</a >https://mifi.ventures</a
> >
{:else if section.links}
{#each linkify(para, section.links) as segment}
{#if typeof segment === 'string'}
{segment}
{:else}
<a href={segment.href} rel="noopener noreferrer"
>{segment.label}</a
>
{/if}
{/each}
{:else} {:else}
{para} {para}
{/if} {/if}
@@ -116,6 +150,16 @@
margin: 0 0 var(--space-md) 0; margin: 0 0 var(--space-md) 0;
} }
.legal-content a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 0.2em;
}
.legal-content a:hover {
color: var(--color-primary-hover);
}
.legal-section { .legal-section {
margin-bottom: var(--space-xl); margin-bottom: var(--space-xl);
} }

View File

@@ -1,5 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { privacyPolicyPageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PATH = '/privacy-policy'; const PATH = '/privacy-policy';
@@ -10,17 +12,7 @@ const privacyPageMeta: PageMeta = {
description: description:
'Privacy Policy for mifi Ventures LLC. Describes information we collect, how we use it, messaging policy (SMS), data security, and your rights. Last updated March 5, 2026.', 'Privacy Policy for mifi Ventures LLC. Describes information we collect, how we use it, messaging policy (SMS), data security, and your rights. Last updated March 5, 2026.',
canonical: PAGE_URL, canonical: PAGE_URL,
jsonLd: [ jsonLd: [...baseJsonLdGraph, privacyPolicyPageJsonLdGraph],
{
'@type': 'WebPage',
'@id': `${PAGE_URL}#webpage`,
name: 'Privacy Policy | mifi Ventures',
url: PAGE_URL,
dateModified: '2026-03-05',
publisher: { '@id': `${BASE}/#organization` },
inLanguage: 'en-US',
},
],
}; };
export const load: PageLoad = () => { export const load: PageLoad = () => {

View File

@@ -1,34 +1,21 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from '$lib/data/home/json-ld'; import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { pageContent } from '$lib/data/services/landing/content'; import { pageContent } from '$lib/data/services/landing/content';
import { servicesPageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
import { catalogJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}/services/`; const PATH = '/services';
const PAGE_URL = `${BASE}${PATH}`;
export const load: PageLoad = () => { export const load: PageLoad = () => {
const { meta } = pageContent; const { meta } = pageContent;
const servicePageMeta: PageMeta = { const servicePageMeta: PageMeta = {
title: meta.title, title: meta.title,
description: meta.description, description: meta.description,
jsonLd: [ canonical: PAGE_URL,
...defaultJsonLdGraph, jsonLd: [...baseJsonLdGraph, ...catalogJsonLdGraph, servicesPageJsonLdGraph],
{
'@type': 'ProfessionalService',
'@id': `${BASE}/services/#service`,
name: 'mifi Ventures',
url: PAGE_URL,
description: meta.jsonLdServiceDescription ?? meta.description,
serviceType: [
'SaaS Architecture Consulting',
'MVP Development Consulting',
'Fractional CTO Services',
'Startup Infrastructure Strategy',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
],
}; };
return { meta: servicePageMeta }; return { meta: servicePageMeta };
}; };

View File

@@ -1,49 +1,24 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from '$lib/data/home/json-ld'; import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { pageContent } from '$lib/data/services/fractional-cto-for-early-stage-saas/content'; import { pageContent } from '$lib/data/services/fractional-cto-for-early-stage-saas/content';
import { faqItems } from '$lib/data/services/fractional-cto-for-early-stage-saas/faq'; import { fractionalCtoForEarlyStageSaaSServiceJsonLdGraph } from '$lib/data/json-ld/servicesJsonLdGraphs';
import { fractionalCtoForEarlyStageSaaSPageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}/services/fractional-cto-for-early-stage-saas/`; const PATH = '/services/fractional-cto-for-early-stage-saas';
const PAGE_URL = `${BASE}${PATH}`;
function buildFaqPageJsonLd(): Record<string, unknown> {
return {
'@type': 'FAQPage',
'@id': `${PAGE_URL}#faq`,
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
}
export const load: PageLoad = () => { export const load: PageLoad = () => {
const { meta } = pageContent; const { meta } = pageContent;
const servicePageMeta: PageMeta = { const servicePageMeta: PageMeta = {
title: meta.title, title: meta.title,
description: meta.description, description: meta.description,
canonical: PAGE_URL,
jsonLd: [ jsonLd: [
...defaultJsonLdGraph, ...baseJsonLdGraph,
{ fractionalCtoForEarlyStageSaaSPageJsonLdGraph,
'@type': 'ProfessionalService', fractionalCtoForEarlyStageSaaSServiceJsonLdGraph,
'@id': `${BASE}/services/fractional-cto-for-early-stage-saas/#service`,
name: 'mifi Ventures',
url: PAGE_URL,
description: meta.jsonLdServiceDescription ?? meta.description,
serviceType: [
'Fractional CTO',
'Technical Leadership',
'SaaS Engineering Consulting',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
buildFaqPageJsonLd(),
], ],
}; };
return { meta: servicePageMeta }; return { meta: servicePageMeta };

View File

@@ -1,49 +1,24 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from '$lib/data/home/json-ld';
import { pageContent } from '$lib/data/services/hands-on-saas-architecture-consultant/content'; import { pageContent } from '$lib/data/services/hands-on-saas-architecture-consultant/content';
import { faqItems } from '$lib/data/services/hands-on-saas-architecture-consultant/faq'; import { handsOnSaaSArchitectureConsultantPageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
import { handsOnSaaSArchitectureConsultantServiceJsonLdGraph } from '$lib/data/json-ld/servicesJsonLdGraphs';
import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}/services/hands-on-saas-architecture-consultant/`; const PATH = '/services/hands-on-saas-architecture-consultant';
const PAGE_URL = `${BASE}${PATH}`;
function buildFaqPageJsonLd(): Record<string, unknown> {
return {
'@type': 'FAQPage',
'@id': `${PAGE_URL}#faq`,
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
}
export const load: PageLoad = () => { export const load: PageLoad = () => {
const { meta } = pageContent; const { meta } = pageContent;
const servicePageMeta: PageMeta = { const servicePageMeta: PageMeta = {
title: meta.title, title: meta.title,
description: meta.description, description: meta.description,
canonical: PAGE_URL,
jsonLd: [ jsonLd: [
...defaultJsonLdGraph, ...baseJsonLdGraph,
{ handsOnSaaSArchitectureConsultantPageJsonLdGraph,
'@type': 'ProfessionalService', handsOnSaaSArchitectureConsultantServiceJsonLdGraph,
'@id': `${BASE}/services/hands-on-saas-architecture-consultant/#service`,
name: 'mifi Ventures',
url: PAGE_URL,
description: meta.jsonLdServiceDescription ?? meta.description,
serviceType: [
'Product Architecture',
'SaaS Engineering Consulting',
'Frontend Systems Architecture',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
buildFaqPageJsonLd(),
], ],
}; };
return { meta: servicePageMeta }; return { meta: servicePageMeta };

View File

@@ -1,49 +1,24 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from '$lib/data/home/json-ld'; import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { pageContent } from '$lib/data/services/mvp-architecture-and-launch/content'; import { pageContent } from '$lib/data/services/mvp-architecture-and-launch/content';
import { faqItems } from '$lib/data/services/mvp-architecture-and-launch/faq'; import { mvpArchitectureAndLaunchServiceJsonLdGraph } from '$lib/data/json-ld/servicesJsonLdGraphs';
import { mvpArchitectureAndLaunchPageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}/services/mvp-architecture-and-launch/`; const PATH = '/services/mvp-architecture-and-launch';
const PAGE_URL = `${BASE}${PATH}`;
function buildFaqPageJsonLd(): Record<string, unknown> {
return {
'@type': 'FAQPage',
'@id': `${PAGE_URL}#faq`,
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
}
export const load: PageLoad = () => { export const load: PageLoad = () => {
const { meta } = pageContent; const { meta } = pageContent;
const servicePageMeta: PageMeta = { const servicePageMeta: PageMeta = {
title: meta.title, title: meta.title,
description: meta.description, description: meta.description,
canonical: PAGE_URL,
jsonLd: [ jsonLd: [
...defaultJsonLdGraph, ...baseJsonLdGraph,
{ mvpArchitectureAndLaunchPageJsonLdGraph,
'@type': 'ProfessionalService', mvpArchitectureAndLaunchServiceJsonLdGraph,
'@id': `${BASE}/services/mvp-architecture-and-launch/#service`,
name: 'mifi Ventures',
url: PAGE_URL,
description: meta.jsonLdServiceDescription ?? meta.description,
serviceType: [
'MVP Architecture',
'SaaS Engineering Consulting',
'Product Architecture',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
buildFaqPageJsonLd(),
], ],
}; };
return { meta: servicePageMeta }; return { meta: servicePageMeta };

View File

@@ -1,49 +1,24 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { defaultJsonLdGraph } from '$lib/data/home/json-ld'; import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
import { pageContent } from '$lib/data/services/stage-aligned-infrastructure/content'; import { pageContent } from '$lib/data/services/stage-aligned-infrastructure/content';
import { faqItems } from '$lib/data/services/stage-aligned-infrastructure/faq'; import { stageAlignedInfrastructureServiceJsonLdGraph } from '$lib/data/json-ld/servicesJsonLdGraphs';
import { stageAlignedInfrastructurePageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PAGE_URL = `${BASE}/services/stage-aligned-infrastructure/`; const PATH = '/services/stage-aligned-infrastructure';
const PAGE_URL = `${BASE}${PATH}`;
function buildFaqPageJsonLd(): Record<string, unknown> {
return {
'@type': 'FAQPage',
'@id': `${PAGE_URL}#faq`,
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
}
export const load: PageLoad = () => { export const load: PageLoad = () => {
const { meta } = pageContent; const { meta } = pageContent;
const servicePageMeta: PageMeta = { const servicePageMeta: PageMeta = {
title: meta.title, title: meta.title,
description: meta.description, description: meta.description,
canonical: PAGE_URL,
jsonLd: [ jsonLd: [
...defaultJsonLdGraph, ...baseJsonLdGraph,
{ stageAlignedInfrastructurePageJsonLdGraph,
'@type': 'ProfessionalService', stageAlignedInfrastructureServiceJsonLdGraph,
'@id': `${BASE}/services/stage-aligned-infrastructure/#service`,
name: 'mifi Ventures',
url: PAGE_URL,
description: meta.jsonLdServiceDescription ?? meta.description,
serviceType: [
'SaaS Architecture Consulting',
'Startup Infrastructure Strategy',
'Technical Leadership Consulting',
],
provider: { '@id': `${BASE}/#organization` },
areaServed: { '@type': 'Country', name: 'United States' },
},
buildFaqPageJsonLd(),
], ],
}; };
return { meta: servicePageMeta }; return { meta: servicePageMeta };

View File

@@ -28,7 +28,9 @@
<h2 id={`${section.id}-heading`}>{section.heading}</h2> <h2 id={`${section.id}-heading`}>{section.heading}</h2>
{#each section.body as para} {#each section.body as para}
<p> <p>
{#if section.id === 'contact' && para === 'legal@mifi.ventures'} {#if section.id === 'contact' && para === 'mifi Ventures LLC'}
<strong>{para}</strong>
{:else if section.id === 'contact' && para === 'legal@mifi.ventures'}
<a href="mailto:legal@mifi.ventures" <a href="mailto:legal@mifi.ventures"
>legal@mifi.ventures</a >legal@mifi.ventures</a
> >

View File

@@ -1,5 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { PageMeta } from '$lib/seo'; import type { PageMeta } from '$lib/seo';
import { termsOfServicePageJsonLdGraph } from '$lib/data/json-ld/webpageJsonLdGraphs';
import { baseJsonLdGraph } from '$lib/data/json-ld/baseJsonLdGraphs';
const BASE = 'https://mifi.ventures'; const BASE = 'https://mifi.ventures';
const PATH = '/terms-of-service'; const PATH = '/terms-of-service';
@@ -10,17 +12,7 @@ const termsPageMeta: PageMeta = {
description: description:
'Terms of Service for mifi Ventures LLC. Covers use of the website, services, client communications including SMS, acceptable use, and contact information.', 'Terms of Service for mifi Ventures LLC. Covers use of the website, services, client communications including SMS, acceptable use, and contact information.',
canonical: PAGE_URL, canonical: PAGE_URL,
jsonLd: [ jsonLd: [...baseJsonLdGraph, termsOfServicePageJsonLdGraph],
{
'@type': 'WebPage',
'@id': `${PAGE_URL}#webpage`,
name: 'Terms of Service | mifi Ventures',
url: PAGE_URL,
dateModified: '2026-03-05',
publisher: { '@id': `${BASE}/#organization` },
inLanguage: 'en-US',
},
],
}; };
export const load: PageLoad = () => { export const load: PageLoad = () => {

View File

@@ -0,0 +1,5 @@
{
"name": "mifi Ventures",
"description": "mifi Ventures is a consulting firm that helps early-stage SaaS companies build and scale their businesses.",
"url": "https://mifi.ventures"
}

View File

@@ -1,18 +1,64 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="preload"
href="/assets/fonts/fraunces-v38-latin-600.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="preload"
href="/assets/fonts/inter-v20-latin-500.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-600.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/fraunces-v38-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<title>404 Not Found — mifi Ventures</title> <title>404 Not Found — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)"> <meta
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)"> name="theme-color"
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> content="#0052cc"
<link rel="stylesheet" href="/assets/error-pages.css"> media="(prefers-color-scheme: light)"
/>
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/assets/error-pages.css" />
</head> </head>
<body class="error-page"> <body class="error-page">
<main> <main>
<div class="emoji" aria-hidden="true">🔍</div> <div class="emoji" aria-hidden="true">🔍</div>
<h1>404 Not Found</h1> <h1 data-testid="404-title">404 Not Found</h1>
<p>This page went off to find itself. Were not sure its coming back.</p> <p>This page went off to find itself. Were not sure its coming back.</p>
<p><a href="/">Back to mifi Ventures →</a></p> <p><a href="/">Back to mifi Ventures →</a></p>
</main> </main>

View File

@@ -1,18 +1,64 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="preload"
href="/assets/fonts/fraunces-v38-latin-600.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="preload"
href="/assets/fonts/inter-v20-latin-500.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-600.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/inter-v20-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/assets/fonts/fraunces-v38-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<title>410 Gone — mifi Ventures</title> <title>410 Gone — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)"> <meta
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)"> name="theme-color"
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> content="#0052cc"
<link rel="stylesheet" href="/assets/error-pages.css"> media="(prefers-color-scheme: light)"
/>
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/assets/error-pages.css" />
</head> </head>
<body class="error-page"> <body class="error-page">
<main> <main>
<div class="emoji" aria-hidden="true">👋</div> <div class="emoji" aria-hidden="true">👋</div>
<h1>410 Gone</h1> <h1 data-testid="410-title">410 Gone</h1>
<p>This page has left the building. Weve moved on—and so should you.</p> <p>This page has left the building. Weve moved on—and so should you.</p>
<p><a href="/">Back to mifi Ventures →</a></p> <p><a href="/">Back to mifi Ventures →</a></p>
</main> </main>

View File

@@ -32,28 +32,47 @@
} }
/* Local fonts — same paths as +layout.svelte preloads */ /* Local fonts — same paths as +layout.svelte preloads */
@font-face { @font-face {
font-family: 'Inter'; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/assets/fonts/inter-v20-latin-regular.woff2') format('woff2'); src: url('/assets/fonts/inter-v20-latin-regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2'); src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Fraunces'; font-family: Inter;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-700.woff2') format('woff2');
}
@font-face {
font-family: Fraunces;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-600.woff2') format('woff2'); src: url('/assets/fonts/fraunces-v38-latin-600.woff2') format('woff2');
} }
@font-face {
font-family: Fraunces;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-700.woff2') format('woff2');
}
/* Error page layout */ /* Error page layout */
.error-page { .error-page {
margin: 0; margin: 0;
@@ -67,15 +86,18 @@
color: var(--ep-text); color: var(--ep-text);
background-color: var(--ep-bg-alt); background-color: var(--ep-bg-alt);
} }
.error-page main { .error-page main {
text-align: center; text-align: center;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
max-width: 28rem; max-width: 28rem;
} }
.error-page .emoji { .error-page .emoji {
font-size: 4rem; font-size: 4rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.error-page h1 { .error-page h1 {
font-family: var(--ep-font-heading); font-family: var(--ep-font-heading);
font-size: 2rem; font-size: 2rem;
@@ -83,19 +105,23 @@
margin: 0 0 0.75rem; margin: 0 0 0.75rem;
color: var(--ep-text); color: var(--ep-text);
} }
.error-page p { .error-page p {
margin: 0 0 1.5rem; margin: 0 0 1.5rem;
color: var(--ep-text-secondary); color: var(--ep-text-secondary);
} }
.error-page a { .error-page a {
color: var(--ep-primary); color: var(--ep-primary);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
} }
.error-page a:hover { .error-page a:hover {
color: var(--ep-primary-hover); color: var(--ep-primary-hover);
text-decoration: underline; text-decoration: underline;
} }
.error-page a:focus-visible { .error-page a:focus-visible {
outline: 2px solid var(--ep-primary); outline: 2px solid var(--ep-primary);
outline-offset: 2px; outline-offset: 2px;

View File

@@ -0,0 +1,168 @@
// Cookie consent banner & third-party analytics loader
// - Stores preference in localStorage
// - Shows a bottom banner until user accepts or rejects
// - Loads Google Analytics and Microsoft Clarity only when accepted
(function () {
'use strict';
var STORAGE_KEY = 'mifi-ventures-cookie-consent';
var BANNER_ID = 'cookie-banner';
var BANNER_VISIBLE_CLASS = 'is-visible';
var hasLoadedThirdParty = false;
// Trusted Types support (for CSP `require-trusted-types-for 'script'`)
// Clarity's tag script looks up window.trustedTypePolicies[policyName] to load its inner script;
// we must expose the policy there for Safari (and other browsers) to avoid TT violations.
var ttPolicy = null;
try {
if (
window.trustedTypes &&
typeof window.trustedTypes.createPolicy === 'function'
) {
ttPolicy = window.trustedTypes.createPolicy('mifi-ventures-policy', {
createScriptURL: function (url) {
return url;
},
});
if (ttPolicy && !window.trustedTypePolicies) {
window.trustedTypePolicies = {};
}
if (ttPolicy) {
window.trustedTypePolicies['mifi-ventures-policy'] = ttPolicy;
}
}
} catch (_) {
ttPolicy = null;
}
function setScriptSrc(el, url) {
if (!el) return;
if (ttPolicy) {
// When Trusted Types are enforced, wrap URLs via our policy
el.src = ttPolicy.createScriptURL(url);
} else {
el.src = url;
}
}
function loadThirdPartyAnalytics() {
if (hasLoadedThirdParty) return;
hasLoadedThirdParty = true;
// Google Analytics (gtag.js + ga-init.js)
try {
var gtagScript = document.createElement('script');
gtagScript.async = true;
setScriptSrc(
gtagScript,
'https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT',
);
gtagScript.onload = function () {
// Load the existing ga-init.js helper once gtag is ready
var gaInit = document.createElement('script');
gaInit.defer = true;
setScriptSrc(gaInit, '/assets/js/ga-init.js');
document.head.appendChild(gaInit);
};
document.head.appendChild(gtagScript);
} catch (e) {
// Fail silently analytics are non-essential
console.error('Failed to load Google Analytics', e);
}
// Microsoft Clarity use the official loader snippet so that
// window.clarity is defined before the tag script runs.
try {
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
setScriptSrc(
t,
`https://www.clarity.ms/tag/${i}?trustedTypes=mifi-ventures-policy`,
);
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, 'clarity', 'script', 'vuo5q3yf79');
window.clarity('consentv2', {
ad_Storage: 'granted',
analytics_Storage: 'granted',
});
} catch (e2) {
console.error('Failed to load Microsoft Clarity', e2);
}
}
function hideBanner(banner) {
if (!banner) return;
banner.classList.remove(BANNER_VISIBLE_CLASS);
}
function showBanner(banner) {
if (!banner) return;
banner.classList.add(BANNER_VISIBLE_CLASS);
}
function init() {
var banner = document.getElementById(BANNER_ID);
if (!banner) return;
var acceptBtn = banner.querySelector('[data-consent="accept"]');
var rejectBtn = banner.querySelector('[data-consent="reject"]');
var pref;
try {
pref = window.localStorage.getItem(STORAGE_KEY);
} catch (_) {
pref = null;
}
if (pref === 'accept') {
hideBanner(banner);
loadThirdPartyAnalytics();
} else if (pref === 'reject') {
hideBanner(banner);
} else {
showBanner(banner);
}
if (acceptBtn) {
acceptBtn.addEventListener('click', function () {
try {
window.localStorage.setItem(STORAGE_KEY, 'accept');
} catch (_) {
// ignore
}
hideBanner(banner);
loadThirdPartyAnalytics();
});
}
if (rejectBtn) {
rejectBtn.addEventListener('click', function () {
try {
window.localStorage.setItem(STORAGE_KEY, 'reject');
} catch (_) {
// ignore
}
hideBanner(banner);
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -1,8 +1,10 @@
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){ window.dataLayer.push(arguments); } function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date()); gtag('js', new Date());
gtag("config", "G-36F29PDKRT", { gtag('config', 'G-36F29PDKRT', {
// optional, but often helpful: // optional, but often helpful:
anonymize_ip: true, anonymize_ip: true,
}); });

View File

@@ -1,8 +1,7 @@
const MOBILE_BREAKPOINT_PX = 768; const MOBILE_BREAKPOINT_PX = 768;
/** All focusable elements inside the menu (links). */ /** All focusable elements inside the menu (links). */
const getMenuFocusables = (menu) => const getMenuFocusables = (menu) => menu.querySelectorAll('a[href]');
menu.querySelectorAll('a[href]');
const mobileMenuHelper = () => { const mobileMenuHelper = () => {
const mobileMenu = document.getElementById('nav-menu'); const mobileMenu = document.getElementById('nav-menu');
@@ -15,10 +14,7 @@ const mobileMenuHelper = () => {
const syncMenuAriaHidden = () => { const syncMenuAriaHidden = () => {
if (isMobile()) { if (isMobile()) {
const hidden = !mobileMenuToggle.checked; const hidden = !mobileMenuToggle.checked;
mobileMenu.setAttribute( mobileMenu.setAttribute('aria-hidden', hidden ? 'true' : 'false');
'aria-hidden',
hidden ? 'true' : 'false',
);
// inert removes the subtree from the a11y tree and makes descendants non-focusable // inert removes the subtree from the a11y tree and makes descendants non-focusable
if (hidden) { if (hidden) {
mobileMenu.setAttribute('inert', ''); mobileMenu.setAttribute('inert', '');

View File

@@ -1,9 +1,6 @@
// Umami: safe track (no-op if script blocked or not loaded) // Umami: safe track (no-op if script blocked or not loaded)
function umamiTrack(name, data) { function umamiTrack(name, data) {
if ( if (typeof window.umami !== 'undefined' && typeof window.umami.track === 'function') {
typeof window.umami !== 'undefined' &&
typeof window.umami.track === 'function'
) {
if (data != null) window.umami.track(name, data); if (data != null) window.umami.track(name, data);
else window.umami.track(name); else window.umami.track(name);
} }

View File

@@ -14,6 +14,12 @@ export default {
'no-descending-specificity': null, 'no-descending-specificity': null,
'media-feature-name-value-no-unknown': null, 'media-feature-name-value-no-unknown': null,
'selector-pseudo-element-colon-notation': null, 'selector-pseudo-element-colon-notation': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global'],
},
],
}, },
overrides: [ overrides: [
{ {

View File

@@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
test.describe('visual regression', () => {
test('cookie banner is visible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#cookie-banner')).toBeVisible();
});
test('cookie banner is not visible when choices are accepted', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'accept');
});
await page.goto('/');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
});
test('cookie banner is not visible when choices are rejected', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
});
test('cookie banner is hidden when user selects accept', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.locator('[data-consent="accept"]').click();
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(await page.evaluate(() => localStorage.getItem('mifi-ventures-cookie-consent'))).toBe('accept');
});
test('cookie banner is hidden when user selects reject', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.locator('[data-consent="reject"]').click();
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(await page.evaluate(() => localStorage.getItem('mifi-ventures-cookie-consent'))).toBe('reject');
});
});

View File

@@ -0,0 +1,116 @@
/**
* @vitest-environment jsdom
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = join(__dirname, '../../static/assets/js/cookie-consent.js');
function createBannerDOM() {
const banner = document.createElement('div');
banner.id = 'cookie-banner';
const acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
acceptBtn.dataset.consent = 'accept';
const rejectBtn = document.createElement('button');
rejectBtn.type = 'button';
rejectBtn.dataset.consent = 'reject';
banner.appendChild(acceptBtn);
banner.appendChild(rejectBtn);
document.body.appendChild(banner);
return { banner, acceptBtn, rejectBtn };
}
describe('cookie-consent.js', () => {
const STORAGE_KEY = 'mifi-ventures-cookie-consent';
let dom: ReturnType<typeof createBannerDOM>;
beforeEach(() => {
// Ensure a fresh DOM and storage for each test
document.body.innerHTML = '';
window.localStorage.clear();
dom = createBannerDOM();
const code = readFileSync(SCRIPT_PATH, 'utf8');
eval(code);
document.dispatchEvent(new Event('DOMContentLoaded'));
});
afterEach(() => {
dom.banner.remove();
vi.restoreAllMocks();
window.localStorage.clear();
});
it('shows banner when no preference is stored', () => {
expect(dom.banner.classList.contains('is-visible')).toBe(true);
});
it('hides banner and loads analytics when preference is "accept"', () => {
document.body.innerHTML = '';
window.localStorage.setItem(STORAGE_KEY, 'accept');
dom = createBannerDOM();
const appendChildSpy = vi.spyOn(document.head, 'appendChild');
const code = readFileSync(SCRIPT_PATH, 'utf8');
eval(code);
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(dom.banner.classList.contains('is-visible')).toBe(false);
expect(appendChildSpy).toHaveBeenCalled();
const urls = appendChildSpy.mock.calls
.map((args) => (args[0] as HTMLScriptElement).src)
.filter(Boolean);
expect(urls.some((src) => src.includes('googletagmanager.com'))).toBe(true);
});
it('hides banner when preference is "reject" and does not load analytics', () => {
document.body.innerHTML = '';
window.localStorage.setItem(STORAGE_KEY, 'reject');
dom = createBannerDOM();
const appendChildSpy = vi.spyOn(document.head, 'appendChild');
const code = readFileSync(SCRIPT_PATH, 'utf8');
eval(code);
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(dom.banner.classList.contains('is-visible')).toBe(false);
const urls = appendChildSpy.mock.calls
.map((args) => (args[0] as HTMLScriptElement).src)
.filter(Boolean);
expect(urls.some((src) => src.includes('googletagmanager.com'))).toBe(false);
});
it('stores "accept" in localStorage, hides banner, and loads analytics on accept click', () => {
const appendChildSpy = vi.spyOn(document.head, 'appendChild');
dom.acceptBtn.click();
expect(window.localStorage.getItem(STORAGE_KEY)).toBe('accept');
expect(dom.banner.classList.contains('is-visible')).toBe(false);
expect(appendChildSpy).toHaveBeenCalled();
});
it('stores "reject" in localStorage and hides banner on reject click', () => {
const appendChildSpy = vi.spyOn(document.head, 'appendChild');
dom.rejectBtn.click();
expect(window.localStorage.getItem(STORAGE_KEY)).toBe('reject');
expect(dom.banner.classList.contains('is-visible')).toBe(false);
const urls = appendChildSpy.mock.calls
.map((args) => (args[0] as HTMLScriptElement).src)
.filter(Boolean);
expect(urls.some((src) => src.includes('googletagmanager.com'))).toBe(false);
});
});

View File

@@ -7,6 +7,14 @@ test.describe('visual regression', () => {
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible(); await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('home.png', { fullPage: true }); await expect(page).toHaveScreenshot('home.png', { fullPage: true });
}); });
@@ -15,6 +23,15 @@ test.describe('visual regression', () => {
await expect(page).toHaveTitle(/SaaS Architecture Services | mifi Ventures/); await expect(page).toHaveTitle(/SaaS Architecture Services | mifi Ventures/);
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/services');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('services.png', { fullPage: true }); await expect(page).toHaveScreenshot('services.png', { fullPage: true });
}); });
@@ -23,6 +40,16 @@ test.describe('visual regression', () => {
await expect(page).toHaveTitle(/SaaS Product Architecture Consultant | mifi Ventures/); await expect(page).toHaveTitle(/SaaS Product Architecture Consultant | mifi Ventures/);
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('#faq')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/services/hands-on-saas-architecture-consultant');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('services-hands-on-saas-architecture-consultant.png', { fullPage: true }); await expect(page).toHaveScreenshot('services-hands-on-saas-architecture-consultant.png', { fullPage: true });
}); });
@@ -31,6 +58,16 @@ test.describe('visual regression', () => {
await expect(page).toHaveTitle(/MVP Architecture & Launch Consultant | mifi Ventures/); await expect(page).toHaveTitle(/MVP Architecture & Launch Consultant | mifi Ventures/);
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('#faq')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/services/mvp-architecture-and-launch');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('services-mvp-architecture-and-launch.png', { fullPage: true }); await expect(page).toHaveScreenshot('services-mvp-architecture-and-launch.png', { fullPage: true });
}); });
@@ -39,6 +76,16 @@ test.describe('visual regression', () => {
await expect(page).toHaveTitle(/Fractional CTO for Early-Stage SaaS | mifi Ventures/); await expect(page).toHaveTitle(/Fractional CTO for Early-Stage SaaS | mifi Ventures/);
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('#faq')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/services/fractional-cto-for-early-stage-saas');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('services-fractional-cto-for-early-stage-saas.png', { fullPage: true }); await expect(page).toHaveScreenshot('services-fractional-cto-for-early-stage-saas.png', { fullPage: true });
}); });
@@ -47,6 +94,16 @@ test.describe('visual regression', () => {
await expect(page).toHaveTitle(/Startup Infrastructure Strategy | mifi Ventures/); await expect(page).toHaveTitle(/Startup Infrastructure Strategy | mifi Ventures/);
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('#faq')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/services/stage-aligned-infrastructure');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('services-stage-aligned-infrastructure.png', { fullPage: true }); await expect(page).toHaveScreenshot('services-stage-aligned-infrastructure.png', { fullPage: true });
}); });
@@ -56,6 +113,14 @@ test.describe('visual regression', () => {
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible(); await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/privacy-policy');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('privacy-policy.png', { fullPage: true }); await expect(page).toHaveScreenshot('privacy-policy.png', { fullPage: true });
}); });
@@ -65,6 +130,26 @@ test.describe('visual regression', () => {
await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('#main')).toBeVisible(); await expect(page.locator('#main')).toBeVisible();
await expect(page.locator('.footer')).toBeVisible(); await expect(page.locator('.footer')).toBeVisible();
await expect(page.locator('#cookie-banner')).toBeVisible();
await page.evaluate(() => {
localStorage.setItem('mifi-ventures-cookie-consent', 'reject');
});
await page.goto('/terms-of-service');
await expect(page.locator('#cookie-banner')).not.toBeVisible();
await expect(page).toHaveScreenshot('terms-of-service.png', { fullPage: true }); await expect(page).toHaveScreenshot('terms-of-service.png', { fullPage: true });
}); });
test('404 page matches snapshot', async ({ page }) => {
await page.goto('/404');
await expect(page).toHaveTitle(/404 | mifi Ventures/);
await expect(page).toHaveScreenshot('404.png', { fullPage: true });
});
test('410 page matches snapshot', async ({ page }) => {
await page.goto('/410');
await expect(page).toHaveTitle(/410 | mifi Ventures/);
await expect(page).toHaveScreenshot('410.png', { fullPage: true });
});
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 KiB

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 KiB

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 KiB

After

Width:  |  Height:  |  Size: 683 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 918 KiB

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

After

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 532 KiB