Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: CI
on: [push, pull_request]
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ cache.bin
.devin/logs/
.agent-logs/
.codevetter/

# Private agent qualification evidence
.fleet-local/
57 changes: 40 additions & 17 deletions landing-astro/src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,32 @@
const year = new Date().getFullYear();
---

<footer class="site-footer">
<fleet-footer-extension
data-fleet-footer-project="rolepatch"
product-name="RolePatch"
art-src="https://sassmaker.com/footer-art/rolepatch.webp"
class="precise-footer"
signature-name="RolePatch"
font-base="https://sassmaker.com/fonts/fleet-footer-precise-v1/"
theme="dark"
surface={Astro.url.pathname === '/' ? 'landing' : 'app'}
art-alt="RolePatch: A careful application-tailoring desk centers a plain resume sheet beside a job-description card, joined by removable revision strips. A source folder and separate review tray occupy the sides, making approval visible before action."
art-width="2170"
art-height="725"
art-position="50% 50%"
art-credit="Original illustration for RolePatch"
>
<saas-maker-newsletter-capture slot="capture"
catalog-id="rolepatch"
product-name="RolePatch"
kind="newsletter"
source="footer"
privacy-url="https://rolepatch.com/privacy"
theme="dark"
layout="compact"
integrated
></saas-maker-newsletter-capture>
<footer slot="navigation" data-fleet-footer-navigation class="site-footer">
<div class="site-shell">
<div class="footer-state">
<div>
Expand Down Expand Up @@ -62,30 +87,28 @@ const year = new Date().getFullYear();
</div>

<div class="footer-tools">
<p>Explore the Fleet project strip or ask AI about RolePatch below.</p>
<p>Explore more from the studio or ask AI about RolePatch.</p>
<p>© {year} Sarthak Agrawal · No unattended bulk apply · No CAPTCHA bypass</p>
</div>

<saas-maker-newsletter-capture
catalog-id="rolepatch"
product-name="RolePatch"
kind="newsletter"
source="footer"
privacy-url="https://rolepatch.com/privacy"
theme="dark"
></saas-maker-newsletter-capture>
<script is:inline type="module" src="https://sassmaker.com/newsletter-capture.js"></script>
<script is:inline type="module" src="https://sassmaker.com/newsletter-capture.js?v=precise-b0adaa67"></script>

<portfolio-project-strip
current-project="rolepatch"
label="Other Fleet projects"
></portfolio-project-strip>
<script is:inline src="/project-strip.js" data-project="rolepatch" defer></script>
<script is:inline src="https://sassmaker.com/project-strip.js?v=precise-b0adaa67" data-project="rolepatch" defer data-theme="dark"
data-host-only="true"
></script>
<script
is:inline
src="https://sassmaker.com/ai-chat-footer.js"
src="https://sassmaker.com/ai-chat-footer.js?v=precise-b0adaa67"
data-name="RolePatch"
defer
data-theme="dark"
data-host-only="true"
data-project="rolepatch"
></script>
</div>
</footer>
<portfolio-project-strip slot="projects" layout="studio" theme="dark"
current-project="rolepatch"
label="Other Fleet projects"
></portfolio-project-strip>
</fleet-footer-extension>
51 changes: 51 additions & 0 deletions landing-astro/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,54 @@ mark, ins { background: #dce5ff; color: var(--blue-dark); text-decoration: none;
html { scroll-behavior: auto; }
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
}

/* Owner-selected Precise C: closing regions only; native product UI is unchanged. */
fleet-footer-extension.precise-footer {
--fleet-footer-ui-font: var(--fleet-footer-loaded-ui, system-ui, sans-serif);
--fleet-footer-mono-font: var(--fleet-footer-loaded-mono, ui-monospace, monospace);
--fleet-footer-label-font: var(--fleet-footer-mono-font);
--fleet-footer-signature-font: var(--fleet-footer-ui-font);
color: var(--precise-ink);
background: var(--fleet-footer-canvas);
}
.precise-footer [slot="capture"] {
min-width: 0;
--newsletter-capture-font: var(--fleet-footer-ui-font);
--newsletter-capture-text: var(--precise-ink);
--newsletter-capture-muted: var(--precise-muted);
--newsletter-capture-border: var(--precise-muted);
--newsletter-capture-surface: var(--fleet-footer-canvas);
--newsletter-capture-focus: var(--fleet-footer-focus);
}
.precise-footer [slot="navigation"] { width: auto; margin: 0; padding: 0; border: 0; background: transparent; }
.precise-footer [slot="navigation"] p { color: var(--precise-muted); font: 400 12px/1.65 var(--fleet-footer-ui-font); }
.precise-footer [slot="navigation"] a { min-height: 44px; text-decoration: none; }
.precise-footer [slot="navigation"] a:hover { text-decoration: underline; text-underline-offset: 4px; }
.precise-footer .precise-routes { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 16px; justify-content: start; }
.precise-footer .footer-route-group { display: flex; min-width: 0; flex-direction: column; align-items: start; }
.precise-footer .footer-route-group h2 { margin: 0 0 8px; color: var(--precise-muted); font: 400 12px/1.5 var(--fleet-footer-label-font); }
.precise-footer .footer-route-group a { max-width: 100%; font: 400 14px/1.45 var(--fleet-footer-ui-font); overflow-wrap: anywhere; }
@media(max-width:640px) { .precise-footer .precise-routes { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px 24px; } }

fleet-footer-extension.precise-footer {
--fleet-footer-canvas: #10100f;
--fleet-footer-lower: #10100f;
--precise-ink: #fffdf8;
--precise-muted: #bbb7ad;
--fleet-footer-muted: #bbb7ad;
--fleet-footer-border: #4b4944;
--fleet-footer-focus: #9fb1ff;
}
.precise-footer [slot="navigation"] .site-shell { width: auto; margin: 0; }
.precise-footer .footer-state { display: block; padding: 0 0 20px; border: 0; }
.precise-footer .footer-state h2 { margin: 8px 0; font: 600 16px/1.4 var(--fleet-footer-ui-font); letter-spacing: 0; }
.precise-footer .footer-state p { max-width: 56ch; }
.precise-footer .footer-grid { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 16px; padding: 0; }
.precise-footer .footer-brand { grid-column: 1 / -1; }
.precise-footer .footer-brand p { margin-top: 10px; }
.precise-footer .footer-grid nav { gap: 0; }
.precise-footer .footer-heading { margin-bottom: 8px; font: 400 12px/1.5 var(--fleet-footer-label-font); color: #bbb7ad; }
.precise-footer .footer-grid nav a { color: #d1cdc3; font: 400 14px/1.45 var(--fleet-footer-ui-font); }
.precise-footer .footer-tools { display: block; margin-top: 20px; padding: 0; border: 0; font: 400 12px/1.6 var(--fleet-footer-ui-font); color: #bbb7ad; }
.precise-footer [slot="capture"] { --newsletter-capture-action: #9fb1ff; --newsletter-capture-action-text: #10100f; }
@media(max-width:640px) { .precise-footer .footer-grid { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px 24px; } }
4 changes: 2 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const securityHeaders = [
// unsafe-eval required by Next.js dev + some libs.
// static.cloudflareinsights.com is the Cloudflare Web Analytics beacon
// that Cloudflare injects automatically on the rolepatch.com zone.
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://us-assets.i.posthog.com https://www.clarity.ms https://scripts.clarity.ms https://challenges.cloudflare.com",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://us-assets.i.posthog.com https://www.clarity.ms https://scripts.clarity.ms https://challenges.cloudflare.com https://sassmaker.com https://health.sassmaker.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"font-src 'self' https://sassmaker.com",
// https: is broad but already in use for AI/scraping/saas-maker traffic;
// explicit cloudflareinsights.com keeps the beacon working if the policy is tightened later.
"connect-src 'self' https: https://cloudflareinsights.com",
Expand Down
66 changes: 66 additions & 0 deletions src/__tests__/footer-csp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';

import nextConfig from '../../next.config';

const expectedPolicy = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://us-assets.i.posthog.com https://www.clarity.ms https://scripts.clarity.ms https://challenges.cloudflare.com https://sassmaker.com https://health.sassmaker.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://sassmaker.com",
"connect-src 'self' https: https://cloudflareinsights.com",
"frame-src 'self' https://challenges.cloudflare.com",
"frame-ancestors 'none'",
].join('; ');

const securityHeaders = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
];

const expectedRoutes = [
{
source: '/',
headers: [
...securityHeaders,
{
key: 'Cache-Control',
value: 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800',
},
{ key: 'CDN-Cache-Control', value: 'public, s-maxage=86400, stale-while-revalidate=604800' },
],
},
...['/tools/:path*', '/blog/:path*'].map((source) => ({
source,
headers: [
...securityHeaders,
{ key: 'Cache-Control', value: 'public, s-maxage=3600, stale-while-revalidate=86400' },
],
})),
{ source: '/(.*)', headers: securityHeaders },
];

describe('First-party footer content security policy', () => {
it('permits only the approved script/font origins while preserving every other directive', async () => {
const routes = await nextConfig.headers?.();
expect(routes).toHaveLength(4);
for (const route of routes ?? []) {
expect(route.headers.filter((header) => header.key === 'Content-Security-Policy')).toEqual([
{ key: 'Content-Security-Policy', value: expectedPolicy },
]);
}
});

it('preserves all non-CSP security headers, cache headers and route rules', async () => {
const routes = await nextConfig.headers?.();
expect(
routes?.map((route) => ({
source: route.source,
headers: route.headers.filter((header) => header.key !== 'Content-Security-Policy'),
}))
).toEqual(expectedRoutes);
});
});
79 changes: 70 additions & 9 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import './globals.css';

import type { Metadata, Viewport } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { createElement } from 'react';
import Script from 'next/script';

import { AuthProvider } from '@/components/auth-provider';
import { AnalyticsProvider } from '@/components/posthog-provider';
Expand Down Expand Up @@ -99,6 +101,18 @@ export const metadata: Metadata = {
// and de-indexed free tools. Homepage and routes set self-canonicals.
};

function footerCustomElement(
tag: string,
attributes: Record<string, string>,
...children: React.ReactNode[]
) {
return createElement(
tag,
attributes as unknown as React.HTMLAttributes<HTMLElement>,
...children
);
}

export default function RootLayout({
children,
}: Readonly<{
Expand Down Expand Up @@ -133,15 +147,62 @@ export default function RootLayout({
<AuthProvider>
<SiteNav />
{children}
<footer className="border-t border-border/60 px-6 py-6 text-sm text-muted-foreground">
<SaaSMakerFeedback />
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4">
<span>RolePatch</span>
<a href="/proof" className="transition-colors hover:text-foreground">
TrueHire proof
</a>
</div>
</footer>
{footerCustomElement(
'fleet-footer-extension',
{
'data-fleet-footer-project': 'rolepatch',
'product-name': 'RolePatch',
'art-src': 'https://sassmaker.com/footer-art/rolepatch.webp',
class: 'precise-footer',
'signature-name': 'RolePatch',
'font-base': 'https://sassmaker.com/fonts/fleet-footer-precise-v1/',
theme: 'dark',
surface: 'app',
'art-alt':
'RolePatch: A careful application-tailoring desk centers a plain resume sheet beside a job-description card, joined by removable revision strips. A source folder and separate review tray occupy the sides, making approval visible before action.',
'art-width': '2170',
'art-height': '725',
'art-position': '50% 50%',
'art-credit': 'Original illustration for RolePatch',
},
<>
<footer
slot="navigation"
data-fleet-footer-navigation
className="text-sm text-muted-foreground"
>
<SaaSMakerFeedback />
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4">
<span>RolePatch</span>
<a href="/proof" className="transition-colors hover:text-foreground">
TrueHire proof
</a>
</div>
</footer>
{footerCustomElement('portfolio-project-strip', {
slot: 'projects',
layout: 'studio',
theme: 'dark',
'current-project': 'rolepatch',
label: 'Other Fleet projects',
})}
</>
)}
<Script
src="https://sassmaker.com/project-strip.js?v=precise-b0adaa67"
data-project="rolepatch"
data-host-only="true"
data-theme="dark"
strategy="afterInteractive"
/>
<Script
src="https://sassmaker.com/ai-chat-footer.js?v=precise-b0adaa67"
data-name="RolePatch"
data-theme="dark"
data-host-only="true"
data-project="rolepatch"
strategy="afterInteractive"
/>
</AuthProvider>
<VitalsReporter />
</AnalyticsProvider>
Expand Down
Loading