Skip to content
Merged
32 changes: 25 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Personal Website of Gustavo Ocanto, Engineering Leader, AI Architect, and Software Engineer." />
<meta name="keywords" content="Gustavo Ocanto, Software Engineer, Engineering Leader, AI Architect, Technical Blog, Vue.js, TypeScript" />
<meta name="robots" content="index,follow" />
<meta name="theme-color" content="#ffffff" />
<meta name="author" content="Gustavo Ocanto" />
<link rel="canonical" href="%VITE_SITE_URL%/" />
<link rel="alternate" href="%VITE_SITE_URL%/" hreflang="en" />
<link rel="alternate" href="%VITE_SITE_URL%/" hreflang="x-default" />
<title>Home - Gustavo Ocanto</title>

<!-- Open Graph -->
Expand All @@ -16,22 +20,36 @@
<meta property="og:type" content="website" />
<meta property="og:url" content="%VITE_SITE_URL%/" />
<meta property="og:image" content="/favicon.ico" />
<meta property="og:locale" content="en_US" />

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Home - Gustavo Ocanto" />
<meta name="twitter:description" content="Personal Website of Gustavo Ocanto, Engineering Leader, AI Architect, and Software Engineer." />
<meta name="twitter:image" content="/favicon.ico" />
<meta name="twitter:creator" content="@gocanto" />
<meta name="twitter:site" content="@gocanto" />

<!-- Structured Data for AI and crawlers -->
<script id="seo-jsonld" type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "%VITE_SITE_URL%/",
"name": "Gustavo Ocanto",
"description": "Personal Website of Gustavo Ocanto, Engineering Leader, AI Architect, and Software Engineer."
}
[
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "%VITE_SITE_URL%/",
"name": "Gustavo Ocanto",
"description": "Personal Website of Gustavo Ocanto, Engineering Leader, AI Architect, and Software Engineer."
},
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Gustavo Ocanto",
"url": "%VITE_SITE_URL%/",
"jobTitle": "Engineering Leader, AI Architect, Software Engineer",
"description": "Engineering leader and AI architect sharing expertise in software, leadership, and product building.",
"sameAs": ["https://x.com/gocanto", "https://www.linkedin.com/in/gocanto/", "https://github.com/gocanto"]
}
]
</script>

<script defer>
Expand Down
7 changes: 5 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { createApp, App as VueApp } from 'vue';
import { createPinia, Pinia } from 'pinia';

import router from '@/router';
import App from '@/App.vue';
import '@css/style.css';
import App from '@/App.vue';
import router from '@/router';
import { lazyLinkDirective } from '@/support/lazy-loading.ts';

const app: VueApp = createApp(App);
const pinia: Pinia = createPinia();

app.use(router);
app.use(pinia);

app.directive('lazy-link', lazyLinkDirective);

app.mount('#app');
41 changes: 25 additions & 16 deletions src/pages/AboutPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@
<!-- Page title -->
<h1 class="h1 blog-h1">I'm {{ formattedNickname }}. I live in Singapore, where I enjoy the present.</h1>

<img class="rounded-lg w-full mb-5" :src="aboutPicture" :alt="`Portrait of: ${formattedNickname}`" />
<img class="rounded-lg w-full mb-5" :src="aboutPicture" :alt="`Portrait of: ${formattedNickname}`" loading="lazy" decoding="async" fetchpriority="low" />

<!-- Page content -->
<div class="space-y-8 text-slate-500">
<div class="space-y-4">
<p class="block mb-3">
I am an engineering leader who’s passionate about building reliable and smooth software that strive to make a difference. With over twenty years in
software development and architecture, I’ve worked extensively with
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://go.dev/">GO</a>,
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://nodejs.org/en">Node.js</a>,
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://www.typescriptlang.org/">TypeScript</a>, and
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://www.php.net/">PHP</a>. I’m also comfortable with frameworks/libraries such
as <a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://laravel.com/">Laravel</a>,
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://vuejs.org/">Vue</a>,
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://symfony.com/">Symfony</a>, and
<a class="blog-link" target="_blank" rel="noopener noreferrer" href="https://nextjs.org/">Next.js</a>.
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://go.dev/">GO</a>,
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://nodejs.org/en">Node.js</a>,
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://www.typescriptlang.org/">TypeScript</a>, and
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://www.php.net/">PHP</a>. I’m also comfortable with
frameworks/libraries such as <a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://laravel.com/">Laravel</a>,
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://vuejs.org/">Vue</a>,
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://symfony.com/">Symfony</a>, and
<a v-lazy-link class="blog-link" target="_blank" rel="noopener noreferrer" href="https://nextjs.org/">Next.js</a>.
</p>
<p class="block mb-3">
I’ve led teams in designing and delivering scalable, high-performance systems that run efficiently even in complex environments. Beyond writing code, I
Expand All @@ -53,8 +53,9 @@
<div class="mt-5 space-y-5">
<h2 class="h2 font-aspekta text-slate-700 dark:text-slate-300">Let's Connect</h2>
<p v-if="profile">
I’m happy to connect by <a class="blog-link" title="send me an email" aria-label="send me an email" :href="`mailto:${profile.email}`">email</a> to
discuss projects and ideas. While I’m not always available for freelance or long-term work, please don’t hesitate to reach out anytime.
I’m happy to connect by
<a v-lazy-link class="blog-link" title="send me an email" aria-label="send me an email" :href="`mailto:${profile.email}`"> email </a>
to discuss projects and ideas. While I’m not always available for freelance or long-term work, please don’t hesitate to reach out anytime.
</p>
</div>
</div>
Expand Down Expand Up @@ -87,7 +88,7 @@ import HeaderPartial from '@partials/HeaderPartial.vue';
import SideNavPartial from '@partials/SideNavPartial.vue';
import WidgetSocialPartial from '@partials/WidgetSocialPartial.vue';
import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor } from '@/support/seo';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo';

import { useApiStore } from '@api/store.ts';
import { debugError } from '@api/http-error.ts';
Expand All @@ -111,11 +112,19 @@ useSeo({
title: 'About',
image: ABOUT_IMAGE,
url: siteUrlFor('/about'),
imageAlt: `${SITE_NAME} portrait`,
keywords: buildKeywords('engineering leadership', 'software architecture expertise', 'tech mentoring'),
description: `${SITE_NAME} is an engineering leader who’s passionate about building reliable and smooth software.`,
jsonLd: {
'@type': 'AboutPage',
name: 'About',
},
jsonLd: [
{
name: 'About',
'@type': 'AboutPage',
url: siteUrlFor('/about'),
'@context': 'https://schema.org',
description: `${SITE_NAME} is an engineering leader focused on building reliable, people-first software.`,
},
PERSON_JSON_LD,
],
});

onMounted(async () => {
Expand Down
18 changes: 13 additions & 5 deletions src/pages/HomePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { onMounted, ref } from 'vue';
import { useApiStore } from '@api/store.ts';
import { debugError } from '@api/http-error.ts';
import type { ProfileResponse } from '@api/response/index.ts';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor } from '@/support/seo';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo';

const apiStore = useApiStore();
const profile = ref<ProfileResponse | null>(null);
Expand All @@ -62,11 +62,19 @@ useSeo({
title: 'Home',
image: ABOUT_IMAGE,
url: siteUrlFor('/'),
imageAlt: `${SITE_NAME} profile portrait`,
keywords: buildKeywords('software engineering leadership', 'technology articles', 'engineering management insights'),
description: `${SITE_NAME} is a full-stack Software Engineer leader & architect with over two decades of experience in building complex web systems and products.`,
jsonLd: {
'@type': 'WebPage',
name: 'Home',
},
jsonLd: [
{
name: 'Home',
'@type': 'WebPage',
url: siteUrlFor('/'),
'@context': 'https://schema.org',
description: `${SITE_NAME} shares articles about software engineering, leadership, AI, and architecture.`,
},
PERSON_JSON_LD,
],
});

onMounted(async () => {
Expand Down
14 changes: 9 additions & 5 deletions src/pages/PostPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<!-- Back -->
<div class="mb-3">
<router-link
v-lazy-link
class="inline-flex text-fuchsia-500 dark:text-slate-500 dark:hover:text-teal-600 rounded-full border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30"
to="/"
>
Expand All @@ -40,6 +41,7 @@
<ul class="inline-flex">
<li>
<a
v-lazy-link
class="flex justify-center items-center text-slate-400 dark:text-slate-500 hover:text-fuchsia-500 dark:hover:text-teal-600 transition duration-150 ease-in-out"
:href="xURLFor(post)"
aria-label="Twitter"
Expand All @@ -55,6 +57,7 @@
</li>
<li>
<a
v-lazy-link
class="flex justify-center items-center text-slate-400 dark:text-slate-500 hover:text-fuchsia-500 dark:hover:text-teal-600 transition duration-150 ease-in-out"
:href="`https://www.linkedin.com/sharing/share-offsite/?url=${fullURLFor(post)}`"
aria-label="LinkedIn"
Expand All @@ -70,6 +73,7 @@
</li>
<li>
<a
v-lazy-link
class="flex justify-center items-center text-slate-400 dark:text-slate-500 hover:text-fuchsia-500 dark:hover:text-teal-600 transition duration-150 ease-in-out"
href="#"
aria-label="Share"
Expand All @@ -89,7 +93,7 @@
<!-- Post content -->
<div class="text-slate-500 dark:text-slate-400 space-y-8">
<p>{{ post.excerpt }}</p>
<img class="w-full" :src="post.cover_image_url" width="692" height="390" :alt="post.title" fetchpriority="high" aria-hidden="true" />
<img class="w-full" :src="post.cover_image_url" width="692" height="390" :alt="post.title" decoding="async" fetchpriority="high" />
<div ref="postContainer" class="post-markdown" v-html="htmlContent"></div>
</div>
</article>
Expand All @@ -113,28 +117,28 @@

<script setup lang="ts">
import DOMPurify from 'dompurify';
import { siteUrlFor, useSeoFromPost } from '@/support/seo';
import { useRoute } from 'vue-router';
import { useApiStore } from '@api/store.ts';
import { useDarkMode } from '@/dark-mode.ts';
import highlight from 'highlight.js/lib/core';
import { debugError } from '@api/http-error.ts';
import { date, getReadingTime } from '@/public.ts';
import FooterPartial from '@partials/FooterPartial.vue';
import HeaderPartial from '@partials/HeaderPartial.vue';
import SideNavPartial from '@partials/SideNavPartial.vue';
import type { PostResponse } from '@api/response/index.ts';
import { siteUrlFor, useSeoFromPost } from '@/support/seo';
import WidgetSponsorPartial from '@partials/WidgetSponsorPartial.vue';
import { date, getReadingTime } from '@/public.ts';
import { initializeHighlighter, renderMarkdown } from '@/support/markdown.ts';
import { onMounted, ref, computed, watch, nextTick, watchEffect } from 'vue';
import { initializeHighlighter, renderMarkdown } from '@/support/markdown.ts';

// --- Component
const route = useRoute();
const apiStore = useApiStore();
const { isDark } = useDarkMode();
const post = ref<PostResponse>();
const slug = ref<string>(route.params.slug as string);
const postContainer = ref<HTMLElement | null>(null);
const slug = ref<string>(route.params.slug as string);

useSeoFromPost(post);

Expand Down
18 changes: 13 additions & 5 deletions src/pages/ProjectsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ import SideNavPartial from '@partials/SideNavPartial.vue';
import ProjectCardPartial from '@partials/ProjectCardPartial.vue';
import WidgetSkillsPartial from '@partials/WidgetSkillsPartial.vue';
import WidgetSponsorPartial from '@partials/WidgetSponsorPartial.vue';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor } from '@/support/seo';
import type { ProfileResponse, ProjectsResponse } from '@api/response/index.ts';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo';

const apiStore = useApiStore();
const projects = ref<ProjectsResponse[]>([]);
Expand All @@ -76,11 +76,19 @@ useSeo({
title: 'Projects',
image: ABOUT_IMAGE,
url: siteUrlFor('/projects'),
imageAlt: `${SITE_NAME} presenting a project`,
keywords: buildKeywords('open source projects', 'software engineering portfolio', 'client project case studies'),
description: `Explore some of ${SITE_NAME} open source and client projects built to solve real engineering challenges.`,
jsonLd: {
'@type': 'CollectionPage',
name: 'Projects',
},
jsonLd: [
{
name: 'Projects',
'@type': 'CollectionPage',
url: siteUrlFor('/projects'),
'@context': 'https://schema.org',
description: `A curated list of ${SITE_NAME} projects that highlight engineering leadership and architecture skills.`,
},
PERSON_JSON_LD,
],
});

onMounted(async () => {
Expand Down
18 changes: 13 additions & 5 deletions src/pages/ResumePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import RecommendationPartial from '@partials/RecommendationPartial.vue';
import { ref, onMounted } from 'vue';
import { useApiStore } from '@api/store.ts';
import { debugError } from '@api/http-error.ts';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor } from '@/support/seo';
import { useSeo, SITE_NAME, ABOUT_IMAGE, siteUrlFor, buildKeywords, PERSON_JSON_LD } from '@/support/seo';
import type { ProfileResponse, EducationResponse, ExperienceResponse, RecommendationsResponse } from '@api/response/index.ts';

const apiStore = useApiStore();
Expand All @@ -68,11 +68,19 @@ useSeo({
title: 'Resume',
image: ABOUT_IMAGE,
url: siteUrlFor('/resume'),
imageAlt: `${SITE_NAME} professional portrait`,
description: `Explore the experience, education, and recommendations of ${SITE_NAME}.`,
jsonLd: {
'@type': 'ProfilePage',
name: 'Resume',
},
keywords: buildKeywords('software engineering resume', 'technology leadership experience', 'engineering manager CV'),
jsonLd: [
{
name: 'Resume',
'@type': 'ProfilePage',
url: siteUrlFor('/resume'),
'@context': 'https://schema.org',
description: `${SITE_NAME} resume showcasing education, experience, and recommendations.`,
},
PERSON_JSON_LD,
],
});

onMounted(async () => {
Expand Down
Loading