diff --git a/site/app/category/[slug]/page.tsx b/site/app/category/[slug]/page.tsx index 3df6486..f94dad2 100644 --- a/site/app/category/[slug]/page.tsx +++ b/site/app/category/[slug]/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; +import HeygrcCard from '@/components/HeygrcCard'; import ListPage from '@/components/ListPage'; -import { categories, getCategory, skillsInCategory } from '@/lib/content'; +import { categories, getCategory, skillsInCategory, HEYGRC_LINE, HEYGRC_LIST_PATHS } from '@/lib/content'; import { firstSentence } from '@/lib/text'; import { pageMeta } from '@/lib/seo'; @@ -35,6 +36,7 @@ export default async function CategoryPage({ params }: Props) { crumbs={[{ name: 'Skills', href: '/skills/' }, { name: c.label, href: `/category/${c.slug}/` }]} list={list} path={`/category/${c.slug}/`} + after={HEYGRC_LIST_PATHS.includes(`/category/${c.slug}/`) ? : undefined} /> ); } diff --git a/site/app/framework/[tag]/page.tsx b/site/app/framework/[tag]/page.tsx index 29a4c65..1b49927 100644 --- a/site/app/framework/[tag]/page.tsx +++ b/site/app/framework/[tag]/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; +import HeygrcCard from '@/components/HeygrcCard'; import ListPage from '@/components/ListPage'; -import { frameworks, getFramework, skillsWithFramework } from '@/lib/content'; +import { frameworks, getFramework, skillsWithFramework, HEYGRC_LINE, HEYGRC_LIST_PATHS } from '@/lib/content'; import { firstSentence } from '@/lib/text'; import { pageMeta } from '@/lib/seo'; @@ -40,6 +41,7 @@ export default async function FrameworkPage({ params }: Props) { crumbs={[{ name: 'Skills', href: '/skills/' }, { name: f.label, href: `/framework/${f.tag}/` }]} list={list} path={`/framework/${f.tag}/`} + after={HEYGRC_LIST_PATHS.includes(`/framework/${f.tag}/`) ? : undefined} /> ); } diff --git a/site/app/globals.css b/site/app/globals.css index 72b63f7..c04d258 100644 --- a/site/app/globals.css +++ b/site/app/globals.css @@ -167,6 +167,13 @@ a.tag:hover { background: #e2e7ee; } .card p { margin: 0; } .card .quiet { font-size: 13px; color: var(--muted-2); margin-top: 10px; } .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; } +/* heyGRC, from the same team: one small touch of its violet, nothing more. */ +.heygrc { border-left: 4px solid #4D4CD7; } +.heygrc p { color: var(--ink-2); font-size: 15px; } +.heygrc .quiet { font-size: 13px; color: var(--muted-2); } +.list-card { max-width: 720px; margin-top: 40px; } +.heygrc-band { border-left: 4px solid #4D4CD7; padding-left: 28px; } +.heygrc-band p.two-jobs { margin-top: 12px; font-size: 15px; color: var(--muted-2); } .picker .tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; } .tab { border: 1px solid var(--line); background: transparent; border-radius: 999px; padding: 6px 14px; font-size: 13.5px; color: var(--muted); } @@ -268,6 +275,7 @@ a.tag:hover { background: #e2e7ee; } .prose h2 { font-size: 23px; margin-top: 36px; } .ft { margin-top: 88px; } .ft .by { margin-left: 0; width: 100%; } + .heygrc-band { padding-left: 18px; } .pills legend { float: none; width: 100%; margin-bottom: 2px; } } diff --git a/site/app/layout.tsx b/site/app/layout.tsx index 63cba95..cf4c65d 100644 --- a/site/app/layout.tsx +++ b/site/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata, Viewport } from 'next'; import { Inter_Tight, JetBrains_Mono } from 'next/font/google'; import Link from 'next/link'; import { Analytics } from '@vercel/analytics/next'; -import { COPILOT_FOOTER_URL, meta, SITE_URL } from '@/lib/content'; +import { COPILOT_FOOTER_URL, HEYGRC_URL, meta, SITE_URL } from '@/lib/content'; import { HOME_DESCRIPTION, HOME_TITLE, pageMeta } from '@/lib/seo'; import './globals.css'; @@ -70,6 +70,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) llms.txt by ISMS Copilot + Also from us: heyGRC, compliance in your pull requests

ISO, IEC, AICPA, SOC 2, PCI DSS, NIST and other framework names appear only as tags saying which framework a skill relates to; they belong to their owners. grcskills is not affiliated with, endorsed by or sponsored by any standards body or regulator, and the skills contain no text from the standards. diff --git a/site/app/page.tsx b/site/app/page.tsx index 45093ab..d83a830 100644 --- a/site/app/page.tsx +++ b/site/app/page.tsx @@ -4,7 +4,7 @@ import CopyButton from '@/components/CopyButton'; import JsonLd from '@/components/JsonLd'; import SearchBox from '@/components/SearchBox'; import SkillCard from '@/components/SkillCard'; -import { categories, featuredSkill, meta, SITE_URL, skills, toRow } from '@/lib/content'; +import { categories, featuredSkill, HEYGRC_LINE, HEYGRC_URL, meta, SITE_URL, skills, toRow } from '@/lib/content'; import { HOME_DESCRIPTION, HOME_H1, HOME_SUBLINE, HOME_TITLE, pageMeta } from '@/lib/seo'; export const metadata: Metadata = pageMeta({ @@ -51,6 +51,15 @@ export default function Home() { {featured && } +

+
+

After the skill: evidence on each pull request

+

{HEYGRC_LINE}

+

Also from us. ISMS Copilot answers framework questions while a skill runs. heyGRC reviews each pull request after it, against the frameworks you pick.

+
+ heyGRC for GRC teams +
+

Find a skill

diff --git a/site/app/skill/[slug]/page.tsx b/site/app/skill/[slug]/page.tsx index ba623dd..da51d93 100644 --- a/site/app/skill/[slug]/page.tsx +++ b/site/app/skill/[slug]/page.tsx @@ -2,10 +2,11 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import Breadcrumbs from '@/components/Breadcrumbs'; +import HeygrcCard from '@/components/HeygrcCard'; import InstallPicker from '@/components/InstallPicker'; import JsonLd from '@/components/JsonLd'; import Row from '@/components/Row'; -import { copilotUrl, frameworkLabel, getAuthor, getSkill, githubUrl, installCommands, meta, relatedTo, skills, toRow } from '@/lib/content'; +import { copilotUrl, frameworkLabel, getAuthor, getSkill, githubUrl, HEYGRC_SKILLS, installCommands, meta, relatedTo, skills, toRow } from '@/lib/content'; import { skillLd } from '@/lib/ld'; import { pageMeta } from '@/lib/seo'; @@ -39,6 +40,7 @@ export default async function SkillPage({ params }: Props) { const author = getAuthor(s.author); const cmd = installCommands(s.slug); const related = relatedTo(s); + const heygrc = Object.hasOwn(HEYGRC_SKILLS, s.slug) ? HEYGRC_SKILLS[s.slug] : undefined; return (
@@ -84,6 +86,8 @@ export default async function SkillPage({ params }: Props) {

ISMS Copilot answers framework questions for this skill. Optional; the skill works without it.

+ {heygrc && } + {author && (

Author

diff --git a/site/components/HeygrcCard.tsx b/site/components/HeygrcCard.tsx new file mode 100644 index 0000000..9872d91 --- /dev/null +++ b/site/components/HeygrcCard.tsx @@ -0,0 +1,17 @@ +import { HEYGRC_URL } from '@/lib/content'; + +// The heyGRC card: under the Install card on the skill pages in HEYGRC_SKILLS, +// and below the list on the pages in HEYGRC_LIST_PATHS. Text only: the CSP +// allows images from this origin alone. +export default function HeygrcCard({ body, className = '' }: { body: string; className?: string }) { + return ( +
+

Evidence on each pull request

+

{body}

+ +

From the same team as grcskills. You pick the frameworks it checks. Optional; the skills work without it.

+
+ ); +} diff --git a/site/components/ListPage.tsx b/site/components/ListPage.tsx index a3ab7e4..73c44c0 100644 --- a/site/components/ListPage.tsx +++ b/site/components/ListPage.tsx @@ -3,7 +3,8 @@ import JsonLd from './JsonLd'; import Row from './Row'; import { SITE_URL, toRow, type Skill } from '@/lib/content'; -export default function ListPage({ title, lead, intro, crumbs, list, path, listHeading }: { title: string; lead: React.ReactNode; intro?: string; crumbs: Crumb[]; list: Skill[]; path: string; listHeading?: string }) { +// `after` renders below the list (the heyGRC card on a few list pages). +export default function ListPage({ title, lead, intro, crumbs, list, path, listHeading, after }: { title: string; lead: React.ReactNode; intro?: string; crumbs: Crumb[]; list: Skill[]; path: string; listHeading?: string; after?: React.ReactNode }) { return (
@@ -18,6 +19,7 @@ export default function ListPage({ title, lead, intro, crumbs, list, path, listH ) : (

No skills here yet.

)} + {after} getSkill(FEATURED_SLUG) ?? skills[0]; export const copilotUrl = (slug: string) => `https://www.ismscopilot.com/mcp?ref=grcskills-${slug}`; export const COPILOT_FOOTER_URL = 'https://www.ismscopilot.com/?ref=grcskills-footer'; + +// heyGRC, from the same team: reviews each pull request against the frameworks +// the customer picks, after a skill has run. heyGRC measures by page path, so the link carries no query +// string (founder decisions X3 and X4, 2026-10-20). +export const HEYGRC_URL = 'https://heygrc.com/for/grc-teams'; +// The only skill pages that show the heyGRC card: the ones whose output lives +// in pull requests. Any other skill page shows none. +export const HEYGRC_SKILLS: Record = { + 'secure-development-policy-from-pipeline': 'Once you sign this policy off, heyGRC reviews each pull request against the frameworks you pick and cites the control behind each finding.', + 'change-management-evidence-sampling': 'Your sample shows how past changes went. heyGRC reviews each pull request as it opens and leaves its check and findings on it, so the next sample starts from records.', + 'policy-gap-review-against-practice': 'Once policy and practice match, look at the next changes too. heyGRC reviews each pull request and gives you the evidence, with the control behind each finding cited.', + 'grc-setup-review': 'Your review names what to fix. For the controls that live in code, heyGRC reviews each pull request and gives you the evidence.', +}; +// List pages that show the same card, by path. +export const HEYGRC_LIST_PATHS = ['/category/evidence/', '/category/policies/', '/framework/cra/']; +export const HEYGRC_LINE = 'Your policy says every change is reviewed. heyGRC checks each pull request and gives you the evidence, with the control behind each finding cited.'; export const githubUrl = (repoPath: string) => `${meta.repoUrl}/blob/${meta.tag}/${repoPath}`; export const githubFolderUrl = (repoPath: string) => `${meta.repoUrl}/tree/${meta.tag}/${repoPath.replace(/\/SKILL\.md$/, '')}`; export const relatedTo = (s: Skill) => s.related.map(getSkill).filter((x): x is Skill => Boolean(x)); diff --git a/site/tests/built.test.mjs b/site/tests/built.test.mjs index f24e242..7e93821 100644 --- a/site/tests/built.test.mjs +++ b/site/tests/built.test.mjs @@ -239,3 +239,43 @@ test('vercel.json CSP: strict script-src, no unsafe-inline or unsafe-eval for sc assert.equal(all.find((h) => h.key === 'X-Content-Type-Options').value, 'nosniff'); assert.equal(all.find((h) => h.key === 'Referrer-Policy').value, 'strict-origin-when-cross-origin'); }); + +// heyGRC cross-promotion (founder decisions X3 and X4, 2026-10-20). +const HEYGRC = 'https://heygrc.com/for/grc-teams'; +const HEYGRC_SKILLS = ['secure-development-policy-from-pipeline', 'change-management-evidence-sampling', 'policy-gap-review-against-practice', 'grc-setup-review']; +const HEYGRC_LISTS = ['category/evidence/index.html', 'category/policies/index.html', 'framework/cra/index.html']; +const heygrcCard = (html) => /
{ + const home = read('index.html'); + const band = home.match(/
/)?.[0]; + assert.ok(band, 'home band'); + assert.ok(band.includes(`href="${HEYGRC}"`), 'home band links to heyGRC'); + assert.ok(home.indexOf('id="back"') < home.indexOf('heygrc-band') && home.indexOf('heygrc-band') < home.indexOf('id="find"'), 'band sits after What comes back'); + for (const rel of pages()) { + const ft = read(rel).match(/