Services

Apps Blog


SEO review: apps/blog

Date: 2026-07-01

Scope: apps/blog Next.js app metadata, article/list templates, canonical/OG/Twitter tags, robots/sitemap/RSS behavior, search/filter UX, structured data, heading/content semantics, and public assets.

Summary#

apps/blog has a solid SEO foundation: central metadata helpers, static article params, blog-scoped canonical URLs, robots/sitemap/RSS routes, article/blog/breadcrumb JSON-LD, and crawlable article pages. The main risks are asset and URL correctness issues that can break social previews or route users/crawlers to blog-local paths that should resolve outside the /blog app.

Critical findings#

1. Default OG image points to a missing public asset#

Evidence

  • apps/blog/lib/seo.ts:16 sets DEFAULT_OG_IMAGE = '/images/og/assistance-og.png'.
  • apps/blog/app/layout.tsx:25-29 uses that value for default Open Graph images.
  • apps/blog/lib/seo.ts:64-67 and apps/blog/lib/seo.ts:93-107 use the same image for post/page metadata fallbacks.
  • apps/blog/components/JsonLd.tsx:76 uses the same fallback for BlogPosting.image.
  • Public asset inspection found no apps/blog/public/images/og/assistance-og.png; apps/blog/public/images/blog/assistance-og-image.png exists instead.

Impact

Pages without per-post images publish broken og:image, Twitter image, RSS enclosure, and JSON-LD image URLs. Social shares may render without an image and structured-data validation may warn about an inaccessible image.

Recommended fix

Add the expected public/images/og/assistance-og.png asset or update DEFAULT_OG_IMAGE to an existing, blog-scoped image. Then validate one home page and one article with generated metadata or rendered HTML.

High severity findings#

1. Blog CTAs and legacy service redirects use blog-local service/contact URLs#

Evidence

  • The home CTA links to /services/devops-as-a-service and /contact-us in apps/blog/app/BlogClient.tsx:40-49.
  • The post template CTA component is rendered on every article in apps/blog/components/Blog/BlogPostRenderer.tsx:305.
  • The legacy services page redirects to /services/${slug} in apps/blog/app/services/[slug]/page.tsx:23-35, while its metadata canonical is also /services/${slug} in apps/blog/app/services/[slug]/page.tsx:13-18.

Impact

Because the app is configured with basePath: '/blog' in apps/blog/next.config.mjs:38, relative URLs can resolve as /blog/services/... and /blog/contact-us. That creates noindex intermediate pages for services and likely broken or duplicate blog-local paths instead of sending users and crawlers to the main site service/contact pages.

Recommended fix

Use absolute main-site URLs for cross-app destinations, e.g. https://www.assistance.bg/services/devops-as-a-service and https://www.assistance.bg/contact-us, and make legacy service redirects/canonicals point at the canonical main-site service URLs.

2. Category and tag listing markup uses invalid children under ordered lists#

Evidence

  • Category pages render <ol> with direct <div> children in apps/blog/app/categories/[category]/page.tsx:63-71.
  • Tag pages render the same invalid pattern in apps/blog/app/tags/[tag]/page.tsx:55-63.
  • The main blog list uses valid <ol>/<li> structure in apps/blog/app/BlogClient.tsx:138-153, which is a good local reference.

Impact

Invalid list semantics weaken machine readability and accessibility for taxonomy listing pages. Search engines generally recover, but the pages are less semantically consistent than the main listing.

Recommended fix

Replace the direct <div> children with <li> wrappers matching the main blog list pattern.

Medium severity findings#

1. Taxonomy URLs are both crawlable and listed in the sitemap, but search/filter states are not canonicalized or noindexed#

Evidence

  • Sitemap includes every category and tag URL in apps/blog/app/sitemap.ts:15-27.
  • Category and tag pages have generated metadata/canonicals in apps/blog/app/categories/[category]/page.tsx:21-34 and apps/blog/app/tags/[tag]/page.tsx:21-34.
  • Client-side filters also mutate the home URL to /?q=... and /?category=... in apps/blog/components/Blog/BlogFilters.tsx:61-75 and apps/blog/components/Blog/BlogFilters.tsx:92-107.

Impact

Dedicated taxonomy pages are indexable, while query-parameter filter states on the home page can also be discovered through shared URLs. The home metadata canonical remains /, so filtered URLs consolidate to the home page, but they are still crawlable parameter variants with no explicit robots policy.

Recommended fix

Keep crawlable taxonomy pages as the primary indexable category/tag surfaces. For ?q= and ?category= UI states, either avoid exposing shareable parameter URLs, add middleware/metadata that emits noindex,follow for search-result states, or document that canonical-to-home is intentional.

2. RSS feed omits lastBuildDate and uses a generic author value#

Evidence

  • RSS route emits channel metadata and items in apps/blog/app/rss.xml/route.ts:33-43, but no channel lastBuildDate.
  • Item authors are rendered as plain display names in apps/blog/app/rss.xml/route.ts:17-28.

Impact

The feed is valid enough for many readers, but lastBuildDate helps feed clients detect updates. Some RSS consumers expect <author> to contain an email-like value; display-only names may be ignored.

Recommended fix

Add lastBuildDate from the newest post date. Consider dc:creator for display author names or a valid RSS author format if email addresses are available.

Low severity findings#

1. Homepage structured data does not expose SearchAction#

Evidence

  • getWebSiteSchema() returns WebSite with name, URL, description, and publisher in apps/blog/components/JsonLd.tsx:16-24.
  • The blog has client-side search in apps/blog/components/Blog/BlogFilters.tsx:174-200.

Impact

Search engines can understand the site, but the WebSite schema does not describe the available blog search behavior. This is not required, especially because search is client-side, but it is a missed enhancement.

Recommended fix

If the ?q= search URL is intentionally supported, add a potentialAction SearchAction to WebSite JSON-LD. If filtered query pages should not be indexed, skip this and noindex search states instead.

2. BlogPosting JSON-LD could be richer#

Evidence

  • Article JSON-LD includes headline, description, URL, mainEntityOfPage, image, dates, author, publisher, and keywords in apps/blog/components/JsonLd.tsx:58-85.
  • Article pages pass post frontmatter into that schema in apps/blog/app/[slug]/page.tsx:135-148.

Impact

The schema covers required basics. It could better match rich-result recommendations by adding fields such as isPartOf, articleSection, and author URLs/images where available.

Recommended fix

Extend getBlogPostingSchema() with available category, tag, and author profile/image data after the image URL issue is fixed.

Positive observations#

  • The app has a blog-scoped default origin (https://www.assistance.bg/blog) in apps/blog/lib/constants.ts:1, and tests assert canonical/robots defaults in apps/blog/test/seo-routes.test.ts:7-23.
  • Root metadata sets metadataBase, title templates, description, RSS alternate, Open Graph defaults, and Twitter card defaults in apps/blog/app/layout.tsx:13-34.
  • Page/article metadata is centralized via createPageMetadata() with canonical, Open Graph, Twitter, and keyword support in apps/blog/lib/seo.ts:83-116.
  • Article routes are statically enumerated and unknown slugs are disabled via generateStaticParams() and dynamicParams = false in apps/blog/app/[slug]/page.tsx:11-12 and apps/blog/app/[slug]/page.tsx:34-39.
  • Articles render a single visible <h1> and an <article> container in apps/blog/components/Blog/BlogPostRenderer.tsx:162 and apps/blog/components/Blog/BlogPostRenderer.tsx:215-251.
  • Robots and sitemap routes exist and point at the blog-scoped sitemap in apps/blog/app/robots.ts:5-12 and apps/blog/app/sitemap.ts:6-40.
  • RSS is discoverable from metadata and served with the correct content type/cache headers in apps/blog/app/layout.tsx:21-24 and apps/blog/app/rss.xml/route.ts:45-49.
  • Preview-domain _headers block indexing while production blog routes allow indexing in apps/blog/public/_headers:1-32.