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
7 changes: 6 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@
"logo": "https://oullin.io/brand/logo-touch.png",
"image": "https://oullin.io/brand/logo-touch.png",
"description": "Oullin is a boutique software engineering and architecture consultancy for startups and scale-ups navigating the AI era. AI architecture, modernisation, and resilient systems for regulated and high-trust environments.",
"sameAs": ["https://github.com/oullin"]
"sameAs": ["https://github.com/oullin"],
"founder": {
"@type": "Person",
"name": "Gustavo Ocanto",
"url": "https://gocanto.sh"
}
}
]
</script>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/AboutPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<div class="page-panel-title">{{ sidebar.founder.name }}</div>
<p class="page-panel-copy">{{ sidebar.founder.copy }}</p>
<div class="mt-4">
<a v-lazy-link :href="sidebar.founder.linkUrl" target="_blank" rel="noopener noreferrer" class="blog-link">{{ sidebar.founder.linkLabel }}</a>
<a v-lazy-link :href="sidebar.founder.linkUrl" target="_blank" rel="me noopener noreferrer" class="blog-link">{{ sidebar.founder.linkLabel }}</a>
</div>
</div>
<div class="page-side-block">
Expand Down
5 changes: 3 additions & 2 deletions src/pages/WorkWithUsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@
<span v-for="item in engagement.includes" :key="item">{{ item }}</span>
</div>
<p class="page-panel-copy mt-5"><strong>You leave with:</strong> {{ engagement.outcome }}</p>
<p class="page-panel-copy mt-2">{{ engagement.price }}</p>
<RouterLink :to="engagement.cta.to" class="btn-primary mt-5 inline-block">{{ engagement.cta.label }}</RouterLink>
<RouterLink :to="engagement.cta.to" class="blog-link mt-5 inline-block" :aria-label="`${engagement.cta.label} about ${engagement.label}`">{{
engagement.cta.label
}}</RouterLink>
</div>
</div>
<div class="page-editorial-sep"></div>
Expand Down
5 changes: 4 additions & 1 deletion src/support/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export interface SiteContent {
organization: {
description: string;
sameAs: string[];
founder: {
name: string;
url: string;
};
};
nav: {
links: RouteLink[];
Expand Down Expand Up @@ -204,7 +208,6 @@ export interface WorkWithUsPageContent {
includesLabel: string;
includes: string[];
outcome: string;
price: string;
cta: RouteLink;
}>;
faq: {
Expand Down
5 changes: 5 additions & 0 deletions src/support/seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const ORGANIZATION_JSON_LD = {
image: siteUrlFor(SITE_LOGO),
description: siteContent.organization.description,
sameAs: siteContent.organization.sameAs,
founder: {
'@type': 'Person',
name: siteContent.organization.founder.name,
url: siteContent.organization.founder.url,
},
};

type TwitterCard = 'summary' | 'summary_large_image' | 'app' | 'player';
Expand Down
4 changes: 2 additions & 2 deletions storage/fixtures/about-page.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"label": "// founder & lead architect",
"name": "Gustavo Ocanto",
"copy": "Engineering leader with 20+ years across software engineering, consulting, architecture, AI-first products and companies, and technical management. 10+ years in banking and fintech.",
"linkLabel": "LinkedIn Profile →",
"linkUrl": "https://www.linkedin.com/in/gocanto/"
"linkLabel": "Founder bio →",
"linkUrl": "https://gocanto.sh"
},
"proof": {
"label": "// proof",
Expand Down
6 changes: 5 additions & 1 deletion storage/fixtures/site.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
},
"organization": {
"description": "Oullin is a boutique software engineering and architecture consultancy for startups and scale-ups navigating the AI era. AI architecture, modernisation, and resilient systems for regulated and high-trust environments.",
"sameAs": ["https://github.com/oullin"]
"sameAs": ["https://github.com/oullin"],
"founder": {
"name": "Gustavo Ocanto",
"url": "https://gocanto.sh"
}
},
"nav": {
"links": [
Expand Down
12 changes: 4 additions & 8 deletions storage/fixtures/work-with-us-page.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
"— Two 90-minute working sessions with the founding/engineering team"
],
"outcome": "A clear, honest answer to where AI fits in your stack — and an architectural plan that survives contact with production.",
"price": "From $8,000 · Fixed fee",
"cta": { "label": "START A CONVERSATION →", "to": "/contact" }
"cta": { "label": "More info", "to": "/contact" }
},
{
"label": "[ 02 ] Fractional AI Architect",
Expand All @@ -59,8 +58,7 @@
"— Quarterly roadmap refresh aligned to business goals"
],
"outcome": "Senior architectural oversight without a full-time hire. A team that gets better at building as the engagement runs.",
"price": "From $8,000/mo · 3-month minimum",
"cta": { "label": "START A CONVERSATION →", "to": "/contact" }
"cta": { "label": "More info", "to": "/contact" }
},
{
"label": "[ 03 ] Production Hardening",
Expand All @@ -79,8 +77,7 @@
"— 2-week async support window post-engagement"
],
"outcome": "AI features that hold under real conditions. Documentation your team owns. No dependency on us to keep it running.",
"price": "From $15,000 · Scoped after a paid discovery session ($2,500)",
"cta": { "label": "START A CONVERSATION →", "to": "/contact" }
"cta": { "label": "More info", "to": "/contact" }
},
{
"label": "[ 04 ] AI-Era Team Enablement",
Expand All @@ -97,8 +94,7 @@
"— A written playbook the team owns after we leave"
],
"outcome": "Engineers who can build with AI and review what it produces with real judgment. The capability stays in your team.",
"price": "From $6,000 · 4-week programme",
"cta": { "label": "START A CONVERSATION →", "to": "/contact" }
"cta": { "label": "More info", "to": "/contact" }
}
],
"faq": {
Expand Down
4 changes: 3 additions & 1 deletion tests/pages/AboutPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ describe('AboutPage', () => {
expect(wrapper.text()).toContain(aboutPageContent.sidebar.founder.copy);
expect(wrapper.text()).toContain(aboutPageContent.sidebar.proof.items[1]);
expect(wrapper.find('[data-testid="recommendation-partial"]').text()).toContain('Recommendations partial');
expect(wrapper.html()).toContain(aboutPageContent.sidebar.founder.linkUrl);
const founderLink = wrapper.find(`a[href="${aboutPageContent.sidebar.founder.linkUrl}"]`);
expect(founderLink.text()).toBe('Founder bio →');
expect(founderLink.attributes('rel')).toContain('me');
});

it('renders the about footer without the skills marquee band', async () => {
Expand Down
12 changes: 12 additions & 0 deletions tests/pages/WorkWithUsPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,16 @@ describe('WorkWithUsPage', () => {
expect(wrapper.text()).toContain(workWithUsPageContent.faq.items[0].question);
expect(wrapper.text()).toContain(workWithUsPageContent.cta.button.label);
});

it('replaces engagement prices with contact links', () => {
const wrapper = mount(WorkWithUsPage, { global });
const moreInfoLinks = wrapper.findAll('a').filter((link) => link.text() === 'More info');

expect(moreInfoLinks).toHaveLength(workWithUsPageContent.engagements.length);
moreInfoLinks.forEach((link, index) => {
expect(link.attributes('to')).toBe('/contact');
expect(link.attributes('aria-label')).toBe(`${workWithUsPageContent.engagements[index]?.cta.label} about ${workWithUsPageContent.engagements[index]?.label}`);
});
expect(wrapper.text()).not.toMatch(/(?:[$€£]\s*\d[\d,.]*(?:[km])?|\b\d[\d,.]*(?:[km])?\s*(?:USD|EUR|GBP)\b)/i);
});
});
16 changes: 16 additions & 0 deletions tests/support/seo-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { siteContent } from '@/support/content.ts';
import { termsAndPoliciesPageContent } from '@/support/content/terms-and-policies-page.ts';
import { workWithUsPageContent } from '@/support/content/work-with-us-page.ts';
import { writingPageContent } from '@/support/content/writing-page.ts';
import { ORGANIZATION_JSON_LD } from '@/support/seo.ts';

const indexHtml = readFileSync(resolve(process.cwd(), 'index.html'), 'utf8');
const staleGustavoTitle = 'Gustavo Ocanto - Engineering, Leadership, Fintech & eCommerce | Software Engineer, Architect, AI , AI Architect & Manager';
Expand Down Expand Up @@ -46,6 +47,21 @@ describe('SEO content fixtures', () => {
expect(writingPageContent.seo.description).toContain('engineering judgment');
expect(termsAndPoliciesPageContent.seo.description).toContain("Oullin's terms and policies");
});

it('connects the organization founder metadata to the canonical bio site', () => {
expect(ORGANIZATION_JSON_LD.founder).toEqual({
'@type': 'Person',
name: 'Gustavo Ocanto',
url: 'https://gocanto.sh',
});

const jsonLdMatch = indexHtml.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/);
expect(jsonLdMatch?.[1]).toBeDefined();

const indexJsonLd = JSON.parse(jsonLdMatch?.[1] ?? '[]') as Array<Record<string, unknown>>;
const indexOrganization = indexJsonLd.find((entry) => entry['@type'] === 'Organization');
expect(indexOrganization?.founder).toEqual(ORGANIZATION_JSON_LD.founder);
});
Comment thread
gocanto marked this conversation as resolved.
});

describe('index.html seo defaults', () => {
Expand Down
Loading