From 82d1de43b35542343c61b40b6a1879ef5da3c723 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 14:46:33 +0800 Subject: [PATCH 01/10] start working on structure --- metal/cli/seo/defaults.go | 10 +++-- metal/cli/seo/generator.go | 9 ++-- metal/cli/seo/generator_test.go | 6 ++- metal/cli/seo/manifest.go | 10 ++--- metal/cli/seo/web.go | 77 +++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 metal/cli/seo/web.go diff --git a/metal/cli/seo/defaults.go b/metal/cli/seo/defaults.go index e88c4c40..42b0ad01 100644 --- a/metal/cli/seo/defaults.go +++ b/metal/cli/seo/defaults.go @@ -1,5 +1,11 @@ package seo +const AuthorName = "Gustavo Ocanto" +const HomeSlug = "home" +const AboutSlug = "about" +const ProjectsSlug = "projects" +const ResumeSlug = "resume" + // --- Web URLs const GocantoUrl = "https://gocanto.dev/" @@ -9,10 +15,6 @@ const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" // --- Web Pages - -const WebHomeUrl = "/" -const WebHomeName = "Home" - const WebAboutName = "About" const WebAboutUrl = "/about" diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index 5129f0ad..00c7850e 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -28,6 +28,7 @@ var templatesFS embed.FS const cgoEnabled = true type Generator struct { + Web *Web Page Page Client *Client Env *env.Environment @@ -86,6 +87,7 @@ func NewGenerator(db *database.Connection, env *env.Environment, val *portal.Val Page: page, WebsiteRoutes: webRoutes, Client: NewClient(webRoutes), + Web: NewWeb(), }, nil } @@ -145,7 +147,8 @@ func (g *Generator) GenerateIndex() error { // ----- Template Parsing - tData, buildErr := g.buildForPage(WebHomeName, WebHomeUrl, html) + web := g.Web.GetHomePage() + tData, buildErr := g.buildForPage(web.Name, web.Url, html) if buildErr != nil { return fmt.Errorf("home: generating template data: %w", buildErr) } @@ -394,7 +397,7 @@ func (g *Generator) buildForPage(pageName, path string, body []template.HTML, op data.Body = body data.Title = g.TitleFor(pageName) - data.Manifest = NewManifest(g.Page, data).Render() + data.Manifest = NewManifest(g.Page, data, g.Web).Render() data.Canonical = portal.SanitiseURL(g.CanonicalFor(path)) for _, opt := range opts { @@ -452,7 +455,7 @@ func (g *Generator) CanonicalFor(path string) string { } func (g *Generator) TitleFor(pageName string) string { - if pageName == WebHomeName { + if pageName == g.Web.GetHomePage().Name { return g.Page.SiteName } diff --git a/metal/cli/seo/generator_test.go b/metal/cli/seo/generator_test.go index fc73b3dd..3423837f 100644 --- a/metal/cli/seo/generator_test.go +++ b/metal/cli/seo/generator_test.go @@ -55,8 +55,9 @@ func TestGeneratorBuildAndExport(t *testing.T) { Validator: newTestValidator(t), } + web := NewWeb().GetHomePage() body := []template.HTML{"

Profile

hello

"} - data, err := gen.buildForPage(WebHomeName, WebHomeUrl, body) + data, err := gen.buildForPage(web.Name, web.Url, body) if err != nil { t.Fatalf("build err: %v", err) } @@ -116,7 +117,8 @@ func TestGeneratorBuildRejectsInvalidTemplateData(t *testing.T) { Validator: newTestValidator(t), } - if _, err := gen.buildForPage(WebHomeName, WebHomeUrl, []template.HTML{"

hello

"}); err == nil || !strings.Contains(err.Error(), "invalid template data") { + web := NewWeb().GetHomePage() + if _, err := gen.buildForPage(web.Name, web.Url, []template.HTML{"

hello

"}); err == nil || !strings.Contains(err.Error(), "invalid template data") { t.Fatalf("expected validation error, got %v", err) } } diff --git a/metal/cli/seo/manifest.go b/metal/cli/seo/manifest.go index ace47611..69430c13 100644 --- a/metal/cli/seo/manifest.go +++ b/metal/cli/seo/manifest.go @@ -38,7 +38,7 @@ type ManifestShortcut struct { Desc string `json:"description,omitempty"` } -func NewManifest(tmpl Page, data TemplateData) *Manifest { +func NewManifest(tmpl Page, data TemplateData, web *Web) *Manifest { var icons []ManifestIcon if len(data.Favicons) > 0 { @@ -51,7 +51,7 @@ func NewManifest(tmpl Page, data TemplateData) *Manifest { b := &Manifest{ Icons: icons, Lang: tmpl.Lang, - Scope: WebHomeUrl, + Scope: web.GetHomePage().Url, BgColor: data.BgColor, StartURL: tmpl.SiteURL, Name: tmpl.SiteName, @@ -64,9 +64,9 @@ func NewManifest(tmpl Page, data TemplateData) *Manifest { Shortcuts: []ManifestShortcut{ { Icons: icons, - URL: WebHomeUrl, - Name: WebHomeName, - ShortName: WebHomeName, + URL: web.GetHomePage().Url, + Name: web.GetHomePage().Name, + ShortName: web.GetHomePage().Name, }, { Icons: icons, diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go new file mode 100644 index 00000000..599cfc56 --- /dev/null +++ b/metal/cli/seo/web.go @@ -0,0 +1,77 @@ +package seo + +type Web struct { + FoundedYear int16 + StubPath string + ThemeColor string + Robots string + ColorScheme string + Description string + Pages map[string]WebPage +} + +type WebPage struct { + Name string + Url string + Title string + Excerpt string +} + +func NewWeb() *Web { + var pages map[string]WebPage + + //const WebHomeUrl = "/" + //const WebHomeName = "Home" + home := WebPage{ + Name: "Home", + Url: "/", + Title: AuthorName + "'s Personal Website & Journal", + Excerpt: "Gus's a dedicated engineering leader with over twenty years of experience. He specialises in building high-quality, scalable systems across software development, IT infrastructure, and workplace technology. With expertise in Golang, Node.js, and PHP, He has a proven track record of leading cross-functional teams to deliver secure, compliant solutions, particularly within the financial services sector. His background combines deep technical knowledge in cloud architecture and network protocols with a strategic focus on optimizing workflows, driving innovation, and empowering teams to achieve exceptional results in fast-paced environments.", + } + + //const WebAboutName = "About" + //const WebAboutUrl = "/about" + about := WebPage{ + Name: "About", + Url: "/about", + Title: "About " + AuthorName, + Excerpt: "Gus's an engineering leader who’s passionate about building reliable and smooth software that strive to make a difference. He also has led teams in designing and delivering scalable, high-performance systems that run efficiently even in complex environments", + } + + //const WebProjectsName = "Projects" + //const WebProjectsUrl = "/projects" + projects := WebPage{ + Name: "Projects", + Url: "/projects", + Title: AuthorName + "'s Projects", + Excerpt: "Over the years, Gus’s built and shared command-line tools and frameworks to tackle real engineering challenges—complete with clear docs and automated tests—and partnered with banks, insurers, and fintech to deliver custom software that balances performance, security, and scalability.", + } + + //const WebResumeName = "Resume" + //const WebResumeUrl = "/resume" + resume := WebPage{ + Name: "Resume", + Url: "/resume", + Title: AuthorName + "'s Projects", + Excerpt: "Gus' worked closely with financial services companies, delivering secure and compliant solutions that align with industry regulations and standards. He understands the technical and operational demands of financial institutions and have implemented robust architectures that support high-availability systems, data security, and transactional integrity.", + } + + pages[HomeSlug] = home + pages[AboutSlug] = about + pages[ProjectsSlug] = projects + pages[ResumeSlug] = resume + + return &Web{ + FoundedYear: 2020, + Pages: pages, + StubPath: "stub.html", + ThemeColor: "#0E172B", + Robots: "index,follow", + ColorScheme: "light dark", + Description: "Gus is a full-stack Software Engineer leader with over two decades of experience in building complex web systems and products, specialising in areas like e-commerce, banking, cross-payment solutions, cyber security, and customer success.", + } +} + +func (w *Web) GetHomePage() WebPage { + return w.Pages[HomeSlug] +} From a651873bea650abb374ae2a4fed9f015d7a93581 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 14:53:17 +0800 Subject: [PATCH 02/10] migrate projects, resume and about --- metal/cli/seo/defaults.go | 8 -------- metal/cli/seo/generator.go | 9 ++++++--- metal/cli/seo/manifest.go | 18 +++++++++--------- metal/cli/seo/web.go | 12 ++++++++++++ 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/metal/cli/seo/defaults.go b/metal/cli/seo/defaults.go index 42b0ad01..de766017 100644 --- a/metal/cli/seo/defaults.go +++ b/metal/cli/seo/defaults.go @@ -15,19 +15,11 @@ const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" // --- Web Pages -const WebAboutName = "About" -const WebAboutUrl = "/about" const WebPostsName = "Posts" const WebPostsUrl = "/posts" const WebPostDetailUrl = "/post" -const WebResumeName = "Resume" -const WebResumeUrl = "/resume" - -const WebProjectsName = "Projects" -const WebProjectsUrl = "/projects" - // --- Web Meta const FoundedYear = 2020 diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index 00c7850e..0c2daa66 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -188,7 +188,8 @@ func (g *Generator) GenerateAbout() error { html = append(html, sections.Social(social)) html = append(html, sections.Recommendations(recommendations)) - data, buildErr := g.buildForPage(WebAboutName, WebAboutUrl, html) + web := g.Web.GetAboutPage() + data, buildErr := g.buildForPage(web.Name, web.Url, html) if buildErr != nil { return fmt.Errorf("about: generating template data: %w", buildErr) } @@ -213,7 +214,8 @@ func (g *Generator) GenerateProjects() error { sections := NewSections() body := []template.HTML{sections.Projects(projects)} - data, buildErr := g.buildForPage(WebProjectsName, WebProjectsUrl, body) + web := g.Web.GetProjectsPage() + data, buildErr := g.buildForPage(web.Name, web.Url, body) if buildErr != nil { return fmt.Errorf("projects: generating template data: %w", buildErr) } @@ -254,7 +256,8 @@ func (g *Generator) GenerateResume() error { html = append(html, sections.Experience(experience)) html = append(html, sections.Recommendations(recommendations)) - data, buildErr := g.buildForPage(WebResumeName, WebResumeUrl, html) + web := g.Web.GetResumePage() + data, buildErr := g.buildForPage(web.Name, web.Url, html) if buildErr != nil { return fmt.Errorf("resume: generating template data: %w", buildErr) } diff --git a/metal/cli/seo/manifest.go b/metal/cli/seo/manifest.go index 69430c13..e9ce5a17 100644 --- a/metal/cli/seo/manifest.go +++ b/metal/cli/seo/manifest.go @@ -70,9 +70,9 @@ func NewManifest(tmpl Page, data TemplateData, web *Web) *Manifest { }, { Icons: icons, - URL: WebProjectsUrl, - Name: WebProjectsName, - ShortName: WebProjectsName, + URL: web.GetProjectsPage().Url, + Name: web.GetProjectsPage().Name, + ShortName: web.GetProjectsPage().Name, }, { Icons: icons, @@ -82,15 +82,15 @@ func NewManifest(tmpl Page, data TemplateData, web *Web) *Manifest { }, { Icons: icons, - URL: WebAboutUrl, - Name: WebAboutName, - ShortName: WebAboutName, + URL: web.GetAboutPage().Url, + Name: web.GetAboutPage().Name, + ShortName: web.GetAboutPage().Name, }, { Icons: icons, - URL: WebResumeUrl, - Name: WebResumeName, - ShortName: WebResumeName, + URL: web.GetResumePage().Url, + Name: web.GetResumePage().Name, + ShortName: web.GetResumePage().Name, }, }, } diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index 599cfc56..58fc5216 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -75,3 +75,15 @@ func NewWeb() *Web { func (w *Web) GetHomePage() WebPage { return w.Pages[HomeSlug] } + +func (w *Web) GetAboutPage() WebPage { + return w.Pages[AboutSlug] +} + +func (w *Web) GetResumePage() WebPage { + return w.Pages[ResumeSlug] +} + +func (w *Web) GetProjectsPage() WebPage { + return w.Pages[ProjectsSlug] +} From 2b57de0909d2db5853f8460f1bf2b558474a22f9 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 14:59:02 +0800 Subject: [PATCH 03/10] migrate posts info --- metal/cli/seo/defaults.go | 8 ++------ metal/cli/seo/generator.go | 5 +++-- metal/cli/seo/manifest.go | 6 +++--- metal/cli/seo/web.go | 23 +++++++++++++++++++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/metal/cli/seo/defaults.go b/metal/cli/seo/defaults.go index de766017..fe15447f 100644 --- a/metal/cli/seo/defaults.go +++ b/metal/cli/seo/defaults.go @@ -5,6 +5,8 @@ const HomeSlug = "home" const AboutSlug = "about" const ProjectsSlug = "projects" const ResumeSlug = "resume" +const PostsSlug = "posts" +const PostDetailsSlug = "post-details" // --- Web URLs @@ -14,12 +16,6 @@ const RepoWebUrl = "https://github.com/oullin/web" const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" -// --- Web Pages - -const WebPostsName = "Posts" -const WebPostsUrl = "/posts" -const WebPostDetailUrl = "/post" - // --- Web Meta const FoundedYear = 2020 diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index 0c2daa66..e280f92f 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -517,12 +517,13 @@ func (g *Generator) BuildForPost(post payload.PostResponse, body []template.HTML func (g *Generator) CanonicalPostPath(slug string) string { cleaned := strings.TrimSpace(slug) cleaned = strings.Trim(cleaned, "/") + web := g.Web.GetPostsDetailPage() if cleaned == "" { - return WebPostDetailUrl + return web.Url } - return WebPostDetailUrl + "/" + cleaned + return web.Url + "/" + cleaned } func (g *Generator) SanitizeMetaDescription(raw, fallback string) string { diff --git a/metal/cli/seo/manifest.go b/metal/cli/seo/manifest.go index e9ce5a17..c9aa507d 100644 --- a/metal/cli/seo/manifest.go +++ b/metal/cli/seo/manifest.go @@ -76,9 +76,9 @@ func NewManifest(tmpl Page, data TemplateData, web *Web) *Manifest { }, { Icons: icons, - URL: WebPostsUrl, - Name: WebPostsName, - ShortName: WebPostsName, + URL: web.GetPostsPage().Url, + Name: web.GetPostsPage().Name, + ShortName: web.GetPostsPage().Name, }, { Icons: icons, diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index 58fc5216..bfc5885d 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -56,10 +56,25 @@ func NewWeb() *Web { Excerpt: "Gus' worked closely with financial services companies, delivering secure and compliant solutions that align with industry regulations and standards. He understands the technical and operational demands of financial institutions and have implemented robust architectures that support high-availability systems, data security, and transactional integrity.", } + //const WebPostsName = "Posts" + //const WebPostsUrl = "/posts" + //const WebPostDetailUrl = "/post" + posts := WebPage{ + Name: "Posts", + Url: "/posts", + } + + postsD := WebPage{ + Name: "Posts", + Url: "/post", + } + pages[HomeSlug] = home pages[AboutSlug] = about pages[ProjectsSlug] = projects pages[ResumeSlug] = resume + pages[PostsSlug] = posts + pages[PostDetailsSlug] = postsD return &Web{ FoundedYear: 2020, @@ -87,3 +102,11 @@ func (w *Web) GetResumePage() WebPage { func (w *Web) GetProjectsPage() WebPage { return w.Pages[ProjectsSlug] } + +func (w *Web) GetPostsPage() WebPage { + return w.Pages[PostsSlug] +} + +func (w *Web) GetPostsDetailPage() WebPage { + return w.Pages[PostDetailsSlug] +} From 4215ec0b1387b9f3ed8264488f64e8163c20ed02 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 15:50:55 +0800 Subject: [PATCH 04/10] migrate theme --- metal/cli/seo/defaults.go | 11 ++--------- metal/cli/seo/generator.go | 14 +++++++------- metal/cli/seo/jsonld.go | 4 ++-- metal/cli/seo/web.go | 4 ++-- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/metal/cli/seo/defaults.go b/metal/cli/seo/defaults.go index fe15447f..9e8a7542 100644 --- a/metal/cli/seo/defaults.go +++ b/metal/cli/seo/defaults.go @@ -1,5 +1,7 @@ package seo +const StubPath = "stub.html" + const AuthorName = "Gustavo Ocanto" const HomeSlug = "home" const AboutSlug = "about" @@ -15,12 +17,3 @@ const RepoApiUrl = "https://github.com/oullin/api" const RepoWebUrl = "https://github.com/oullin/web" const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" - -// --- Web Meta - -const FoundedYear = 2020 -const StubPath = "stub.html" -const ThemeColor = "#0E172B" -const Robots = "index,follow" -const ColorScheme = "light dark" -const Description = "Gustavo is a full-stack Software Engineer leader with over two decades of experience in building complex web systems and products, specialising in areas like e-commerce, banking, cross-payment solutions, cyber security, and customer success." diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index e280f92f..0a42f35f 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -372,15 +372,15 @@ func (g *Generator) buildForPage(pageName, path string, body []template.HTML, op data := TemplateData{ OGTagOg: og, - Robots: Robots, + Robots: g.Web.Robots, Twitter: twitter, - ThemeColor: ThemeColor, - ColorScheme: ColorScheme, - BgColor: ThemeColor, + ThemeColor: g.Web.ThemeColor, + ColorScheme: g.Web.ColorScheme, + BgColor: g.Web.ThemeColor, Lang: g.Page.Lang, - Description: Description, + Description: g.Web.Description, Categories: g.Page.Categories, - JsonLD: NewJsonID(g.Page).Render(), + JsonLD: NewJsonID(g.Page, g.Web).Render(), AppleTouchIcon: portal.SanitiseURL(g.Page.LogoURL), HrefLang: []HrefLangData{ { @@ -484,7 +484,7 @@ func truncateForLog(value string) string { func (g *Generator) BuildForPost(post payload.PostResponse, body []template.HTML) (TemplateData, error) { path := g.CanonicalPostPath(post.Slug) imageAlt := g.SanitizeAltText(post.Title, g.Page.SiteName) - description := g.SanitizeMetaDescription(post.Excerpt, Description) + description := g.SanitizeMetaDescription(post.Excerpt, g.Web.Description) image := g.PreferredImageURL(post.CoverImageURL, g.Page.AboutPhotoUrl) imageType := "image/png" diff --git a/metal/cli/seo/jsonld.go b/metal/cli/seo/jsonld.go index 1b1225b6..fa0d6009 100644 --- a/metal/cli/seo/jsonld.go +++ b/metal/cli/seo/jsonld.go @@ -24,7 +24,7 @@ type JsonID struct { WebName string } -func NewJsonID(tmpl Page) *JsonID { +func NewJsonID(tmpl Page, web *Web) *JsonID { return &JsonID{ Lang: tmpl.Lang, SiteURL: tmpl.SiteURL, @@ -35,7 +35,7 @@ func NewJsonID(tmpl Page) *JsonID { SameAs: tmpl.SameAsURL, APIRepoURL: tmpl.APIRepoURL, WebRepoURL: tmpl.WebRepoURL, - FoundedYear: fmt.Sprintf("%d", FoundedYear), + FoundedYear: fmt.Sprintf("%d", web.FoundedYear), Now: func() time.Time { return time.Now().UTC() }, } } diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index bfc5885d..ac694f5f 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -2,7 +2,7 @@ package seo type Web struct { FoundedYear int16 - StubPath string + //StubPath string ThemeColor string Robots string ColorScheme string @@ -79,7 +79,7 @@ func NewWeb() *Web { return &Web{ FoundedYear: 2020, Pages: pages, - StubPath: "stub.html", + //StubPath: "stub.html", ThemeColor: "#0E172B", Robots: "index,follow", ColorScheme: "light dark", From f66e412638fd9369e54a624d5e2d1584c6d3d9ad Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 15:58:51 +0800 Subject: [PATCH 05/10] migrate urls --- metal/cli/seo/defaults.go | 9 --------- metal/cli/seo/generator.go | 18 ++++++++++-------- metal/cli/seo/web.go | 32 +++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/metal/cli/seo/defaults.go b/metal/cli/seo/defaults.go index 9e8a7542..03a0f9c5 100644 --- a/metal/cli/seo/defaults.go +++ b/metal/cli/seo/defaults.go @@ -1,7 +1,6 @@ package seo const StubPath = "stub.html" - const AuthorName = "Gustavo Ocanto" const HomeSlug = "home" const AboutSlug = "about" @@ -9,11 +8,3 @@ const ProjectsSlug = "projects" const ResumeSlug = "resume" const PostsSlug = "posts" const PostDetailsSlug = "post-details" - -// --- Web URLs - -const GocantoUrl = "https://gocanto.dev/" -const RepoApiUrl = "https://github.com/oullin/api" -const RepoWebUrl = "https://github.com/oullin/web" -const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" -const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index 0a42f35f..ee8c1b6d 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -49,6 +49,8 @@ func NewGenerator(db *database.Connection, env *env.Environment, val *portal.Val return nil, fmt.Errorf("initialising categories: %w", err) } + web := NewWeb() + page := Page{ StubPath: StubPath, Categories: categories, @@ -56,15 +58,15 @@ func NewGenerator(db *database.Connection, env *env.Environment, val *portal.Val Lang: env.App.Lang(), OutputDir: env.Seo.SpaDir, Template: &template.Template{}, - LogoURL: portal.SanitiseURL(LogoUrl), - WebRepoURL: portal.SanitiseURL(RepoWebUrl), - APIRepoURL: portal.SanitiseURL(RepoApiUrl), + LogoURL: portal.SanitiseURL(web.Urls.LogoUrl), + WebRepoURL: portal.SanitiseURL(web.Urls.RepoWebUrl), + APIRepoURL: portal.SanitiseURL(web.Urls.RepoApiUrl), SiteURL: portal.SanitiseURL(env.App.URL), - AboutPhotoUrl: portal.SanitiseURL(AboutPhotoUrl), + AboutPhotoUrl: portal.SanitiseURL(web.Urls.AboutPhotoUrl), SameAsURL: []string{ - portal.SanitiseURL(RepoApiUrl), - portal.SanitiseURL(RepoWebUrl), - portal.SanitiseURL(GocantoUrl), + portal.SanitiseURL(web.Urls.RepoApiUrl), + portal.SanitiseURL(web.Urls.RepoWebUrl), + portal.SanitiseURL(web.Urls.GocantoUrl), }, } @@ -83,11 +85,11 @@ func NewGenerator(db *database.Connection, env *env.Environment, val *portal.Val return &Generator{ DB: db, Env: env, + Web: web, Validator: val, Page: page, WebsiteRoutes: webRoutes, Client: NewClient(webRoutes), - Web: NewWeb(), }, nil } diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index ac694f5f..d2d15be4 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -1,13 +1,18 @@ package seo +// const GocantoUrl = "https://gocanto.dev/" +// const RepoApiUrl = "https://github.com/oullin/api" +// const RepoWebUrl = "https://github.com/oullin/web" +// const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" +// const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" type Web struct { FoundedYear int16 - //StubPath string ThemeColor string Robots string ColorScheme string Description string Pages map[string]WebPage + Urls WebPageUrls } type WebPage struct { @@ -17,6 +22,19 @@ type WebPage struct { Excerpt string } +type WebPageUrls struct { + GocantoUrl string + RepoApiUrl string + RepoWebUrl string + LogoUrl string + AboutPhotoUrl string + // const GocantoUrl = "https://gocanto.dev/" + // const RepoApiUrl = "https://github.com/oullin/api" + // const RepoWebUrl = "https://github.com/oullin/web" + // const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" + // const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" +} + func NewWeb() *Web { var pages map[string]WebPage @@ -83,6 +101,18 @@ func NewWeb() *Web { ThemeColor: "#0E172B", Robots: "index,follow", ColorScheme: "light dark", + Urls: WebPageUrls{ + // const GocantoUrl = "https://gocanto.dev/" + // const RepoApiUrl = "https://github.com/oullin/api" + // const RepoWebUrl = "https://github.com/oullin/web" + // const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" + // const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" + GocantoUrl: "https://gocanto.dev/", + RepoApiUrl: "https://github.com/oullin/api", + RepoWebUrl: "https://github.com/oullin/web", + LogoUrl: "https://oullin.io/assets/001-BBig3EFt.png", + AboutPhotoUrl: "https://oullin.io/images/profile/about-seo.png", + }, Description: "Gus is a full-stack Software Engineer leader with over two decades of experience in building complex web systems and products, specialising in areas like e-commerce, banking, cross-payment solutions, cyber security, and customer success.", } } From 07f6a48f0abef149b8dcd0578009bdbea329564c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 16:02:30 +0800 Subject: [PATCH 06/10] wip --- metal/cli/seo/web.go | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index d2d15be4..f4fdcbe6 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -1,18 +1,13 @@ package seo -// const GocantoUrl = "https://gocanto.dev/" -// const RepoApiUrl = "https://github.com/oullin/api" -// const RepoWebUrl = "https://github.com/oullin/web" -// const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" -// const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" type Web struct { FoundedYear int16 ThemeColor string Robots string ColorScheme string Description string - Pages map[string]WebPage Urls WebPageUrls + Pages map[string]WebPage } type WebPage struct { @@ -28,15 +23,10 @@ type WebPageUrls struct { RepoWebUrl string LogoUrl string AboutPhotoUrl string - // const GocantoUrl = "https://gocanto.dev/" - // const RepoApiUrl = "https://github.com/oullin/api" - // const RepoWebUrl = "https://github.com/oullin/web" - // const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" - // const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" } func NewWeb() *Web { - var pages map[string]WebPage + pages := make(map[string]WebPage, 6) //const WebHomeUrl = "/" //const WebHomeName = "Home" @@ -94,25 +84,21 @@ func NewWeb() *Web { pages[PostsSlug] = posts pages[PostDetailsSlug] = postsD + urls := WebPageUrls{ + GocantoUrl: "https://gocanto.dev/", + RepoApiUrl: "https://github.com/oullin/api", + RepoWebUrl: "https://github.com/oullin/web", + LogoUrl: "https://oullin.io/assets/001-BBig3EFt.png", + AboutPhotoUrl: "https://oullin.io/images/profile/about-seo.png", + } + return &Web{ FoundedYear: 2020, + Urls: urls, Pages: pages, - //StubPath: "stub.html", ThemeColor: "#0E172B", Robots: "index,follow", ColorScheme: "light dark", - Urls: WebPageUrls{ - // const GocantoUrl = "https://gocanto.dev/" - // const RepoApiUrl = "https://github.com/oullin/api" - // const RepoWebUrl = "https://github.com/oullin/web" - // const LogoUrl = "https://oullin.io/assets/001-BBig3EFt.png" - // const AboutPhotoUrl = "https://oullin.io/images/profile/about-seo.png" - GocantoUrl: "https://gocanto.dev/", - RepoApiUrl: "https://github.com/oullin/api", - RepoWebUrl: "https://github.com/oullin/web", - LogoUrl: "https://oullin.io/assets/001-BBig3EFt.png", - AboutPhotoUrl: "https://oullin.io/images/profile/about-seo.png", - }, Description: "Gus is a full-stack Software Engineer leader with over two decades of experience in building complex web systems and products, specialising in areas like e-commerce, banking, cross-payment solutions, cyber security, and customer success.", } } From be5e21c10ef71c6765fd09843d731cdd9e97a3df Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 16:07:11 +0800 Subject: [PATCH 07/10] tests --- metal/cli/seo/manifest_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metal/cli/seo/manifest_test.go b/metal/cli/seo/manifest_test.go index 1824851a..61195876 100644 --- a/metal/cli/seo/manifest_test.go +++ b/metal/cli/seo/manifest_test.go @@ -55,7 +55,7 @@ func TestManifestRenderUsesFavicons(t *testing.T) { Body: []template.HTML{"

body

"}, } - manifest := NewManifest(tmpl, data) + manifest := NewManifest(tmpl, data, NewWeb()) manifest.Now = func() time.Time { return time.Unix(0, 0).UTC() } rendered := manifest.Render() @@ -123,7 +123,7 @@ func TestManifestRenderFallsBackToLogo(t *testing.T) { Body: []template.HTML{"

body

"}, } - manifest := NewManifest(tmpl, data) + manifest := NewManifest(tmpl, data, NewWeb()) rendered := manifest.Render() var got map[string]any From ef100496afcb595b32ebbc19713c89cfb96047ff Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 16:16:31 +0800 Subject: [PATCH 08/10] tests --- metal/cli/seo/generator_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/metal/cli/seo/generator_test.go b/metal/cli/seo/generator_test.go index 3423837f..226117ba 100644 --- a/metal/cli/seo/generator_test.go +++ b/metal/cli/seo/generator_test.go @@ -53,9 +53,10 @@ func TestGeneratorBuildAndExport(t *testing.T) { gen := &Generator{ Page: page, Validator: newTestValidator(t), + Web: NewWeb(), } - web := NewWeb().GetHomePage() + web := gen.Web.GetHomePage() body := []template.HTML{"

Profile

hello

"} data, err := gen.buildForPage(web.Name, web.Url, body) if err != nil { @@ -115,9 +116,10 @@ func TestGeneratorBuildRejectsInvalidTemplateData(t *testing.T) { Categories: []string{"golang"}, }, Validator: newTestValidator(t), + Web: NewWeb(), } - web := NewWeb().GetHomePage() + web := gen.Web.GetHomePage() if _, err := gen.buildForPage(web.Name, web.Url, []template.HTML{"

hello

"}); err == nil || !strings.Contains(err.Error(), "invalid template data") { t.Fatalf("expected validation error, got %v", err) } @@ -260,6 +262,7 @@ func TestGeneratorPreparePostImage(t *testing.T) { OutputDir: outputDir, }, Env: &env.Environment{Seo: env.SeoEnvironment{SpaDir: outputDir, SpaImagesDir: imagesDir}}, + Web: NewWeb(), } post := payload.PostResponse{Slug: "awesome-post", CoverImageURL: fileURL.String()} @@ -341,6 +344,7 @@ func TestGeneratorPreparePostImageRemote(t *testing.T) { OutputDir: outputDir, }, Env: &env.Environment{Seo: env.SeoEnvironment{SpaDir: outputDir, SpaImagesDir: imagesDir}}, + Web: NewWeb(), } post := payload.PostResponse{Slug: "remote-post", CoverImageURL: server.URL + "/cover.png"} From 7e987a5e51c43cdc750c1aee10e53752d7651dce Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 16:29:13 +0800 Subject: [PATCH 09/10] typo --- metal/cli/seo/generator.go | 18 ++++++++++-------- metal/cli/seo/web.go | 17 +++-------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index ee8c1b6d..bc92f736 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -490,19 +490,21 @@ func (g *Generator) BuildForPost(post payload.PostResponse, body []template.HTML image := g.PreferredImageURL(post.CoverImageURL, g.Page.AboutPhotoUrl) imageType := "image/png" - cli.Grayln(fmt.Sprintf("Preparing post metadata")) - cli.Grayln(fmt.Sprintf(" Canonical path: %s", path)) - cli.Grayln(fmt.Sprintf(" Sanitised alt text: %s", imageAlt)) - cli.Grayln(fmt.Sprintf(" Description preview: %s", truncateForLog(description))) - cli.Grayln(fmt.Sprintf(" Preferred image candidate: %s", image)) + cli.Grayln("\n----------------- [POST BUILD] ----------------- ") + + cli.Grayln(fmt.Sprintf("Preparing POSTS metadata")) + cli.Magentaln(fmt.Sprintf(" .......... Canonical path: %s", path)) + cli.Blueln(fmt.Sprintf(" .......... Sanitised alt text: %s", imageAlt)) + cli.Successln(fmt.Sprintf(" .......... Description preview: %s", truncateForLog(description))) + cli.Cyanln(fmt.Sprintf(" .......... Preferred image candidate: %s", image)) if prepared, err := g.preparePostImage(post); err == nil && prepared.URL != "" { - cli.Grayln(fmt.Sprintf(" Post image prepared at: %s (%s)", prepared.URL, prepared.Mime)) + cli.Successln(fmt.Sprintf(" Post image prepared at: %s (%s)", prepared.URL, prepared.Mime)) image = prepared.URL imageType = prepared.Mime } else if err != nil { - cli.Errorln(fmt.Sprintf("failed to prepare post image for %s: %v", post.Slug, err)) - cli.Grayln(fmt.Sprintf(" Falling back to preferred image URL: %s", image)) + cli.Errorln(fmt.Sprintf("Failed to prepare post image for %s: %v", post.Slug, err)) + cli.Warningln(fmt.Sprintf(" .......... Falling back to preferred image URL: %s", image)) } return g.buildForPage(post.Title, path, body, func(data *TemplateData) { diff --git a/metal/cli/seo/web.go b/metal/cli/seo/web.go index f4fdcbe6..3f7842ee 100644 --- a/metal/cli/seo/web.go +++ b/metal/cli/seo/web.go @@ -28,8 +28,6 @@ type WebPageUrls struct { func NewWeb() *Web { pages := make(map[string]WebPage, 6) - //const WebHomeUrl = "/" - //const WebHomeName = "Home" home := WebPage{ Name: "Home", Url: "/", @@ -37,8 +35,6 @@ func NewWeb() *Web { Excerpt: "Gus's a dedicated engineering leader with over twenty years of experience. He specialises in building high-quality, scalable systems across software development, IT infrastructure, and workplace technology. With expertise in Golang, Node.js, and PHP, He has a proven track record of leading cross-functional teams to deliver secure, compliant solutions, particularly within the financial services sector. His background combines deep technical knowledge in cloud architecture and network protocols with a strategic focus on optimizing workflows, driving innovation, and empowering teams to achieve exceptional results in fast-paced environments.", } - //const WebAboutName = "About" - //const WebAboutUrl = "/about" about := WebPage{ Name: "About", Url: "/about", @@ -46,34 +42,27 @@ func NewWeb() *Web { Excerpt: "Gus's an engineering leader who’s passionate about building reliable and smooth software that strive to make a difference. He also has led teams in designing and delivering scalable, high-performance systems that run efficiently even in complex environments", } - //const WebProjectsName = "Projects" - //const WebProjectsUrl = "/projects" projects := WebPage{ Name: "Projects", Url: "/projects", - Title: AuthorName + "'s Projects", + Title: AuthorName + "'s Projects & Tools", Excerpt: "Over the years, Gus’s built and shared command-line tools and frameworks to tackle real engineering challenges—complete with clear docs and automated tests—and partnered with banks, insurers, and fintech to deliver custom software that balances performance, security, and scalability.", } - //const WebResumeName = "Resume" - //const WebResumeUrl = "/resume" resume := WebPage{ Name: "Resume", Url: "/resume", - Title: AuthorName + "'s Projects", + Title: AuthorName + "'s professional experience", Excerpt: "Gus' worked closely with financial services companies, delivering secure and compliant solutions that align with industry regulations and standards. He understands the technical and operational demands of financial institutions and have implemented robust architectures that support high-availability systems, data security, and transactional integrity.", } - //const WebPostsName = "Posts" - //const WebPostsUrl = "/posts" - //const WebPostDetailUrl = "/post" posts := WebPage{ Name: "Posts", Url: "/posts", } postsD := WebPage{ - Name: "Posts", + Name: "Post", Url: "/post", } From 5479dac3176cac0f03f610d3e0ae0eee885e8428 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 14 Oct 2025 16:37:41 +0800 Subject: [PATCH 10/10] guards --- metal/cli/seo/jsonld.go | 4 ++++ metal/cli/seo/manifest.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/metal/cli/seo/jsonld.go b/metal/cli/seo/jsonld.go index fa0d6009..392d0fbd 100644 --- a/metal/cli/seo/jsonld.go +++ b/metal/cli/seo/jsonld.go @@ -25,6 +25,10 @@ type JsonID struct { } func NewJsonID(tmpl Page, web *Web) *JsonID { + if web == nil { + web = NewWeb() + } + return &JsonID{ Lang: tmpl.Lang, SiteURL: tmpl.SiteURL, diff --git a/metal/cli/seo/manifest.go b/metal/cli/seo/manifest.go index c9aa507d..c7d85b7b 100644 --- a/metal/cli/seo/manifest.go +++ b/metal/cli/seo/manifest.go @@ -48,6 +48,10 @@ func NewManifest(tmpl Page, data TemplateData, web *Web) *Manifest { icons = []ManifestIcon{{Src: tmpl.LogoURL, Sizes: "512x512", Type: "image/png", Purpose: "any"}} } + if web == nil { + web = NewWeb() + } + b := &Manifest{ Icons: icons, Lang: tmpl.Lang,