feat: prerender public site pages

This commit is contained in:
2026-05-04 16:29:50 -04:00
parent 55d8acef4c
commit 4fba72e99c
14 changed files with 380 additions and 5 deletions

View File

@@ -0,0 +1,59 @@
import { createSSRApp, h } from 'vue';
import { createMemoryHistory, createRouter, RouterView } from 'vue-router';
import { createHead, renderHeadToString } from '@vueuse/head';
import { renderToString } from '@vue/server-renderer';
import { createI18n } from 'vue-i18n';
import en from '@/locales/en.json';
import fr from '@/locales/fr.json';
import Landing from '@/features/landing/views/Landing.vue';
import ProductPage from '@/features/landing/views/ProductPage.vue';
import PricingPage from '@/features/landing/views/PricingPage.vue';
import BlogsPage from '@/features/landing/views/BlogsPage.vue';
import GuidesPage from '@/features/landing/views/GuidesPage.vue';
import './assets/main.css';
const publicRoutes = [
{ path: '/', component: Landing },
{ path: '/product', component: ProductPage },
{ path: '/pricing', component: PricingPage },
{ path: '/blogs', component: BlogsPage },
{ path: '/guides', component: GuidesPage },
{ path: '/login', component: { render: () => null } },
{ path: '/register', component: { render: () => null } },
];
export async function render(routePath) {
const router = createRouter({
history: createMemoryHistory(),
routes: publicRoutes,
});
const head = createHead();
const i18n = createI18n({
legacy: false,
fallbackLocale: 'en',
messages: {
en,
fr,
},
});
const app = createSSRApp({
render: () => h(RouterView),
});
app.use(router);
app.use(head);
app.use(i18n);
await router.push(routePath);
await router.isReady();
const appHtml = await renderToString(app);
const headTags = await renderHeadToString(head);
return {
appHtml,
headTags,
};
}

View File

@@ -0,0 +1,51 @@
import { useHead } from '@vueuse/head';
function getCanonicalUrl(path) {
const configuredSiteUrl = import.meta.env.VITE_PUBLIC_SITE_URL
?? (typeof process !== 'undefined' ? process.env.VITE_PUBLIC_SITE_URL : undefined)
?? (typeof process !== 'undefined' ? process.env.SITE_URL : undefined);
if (configuredSiteUrl) {
return new URL(path, `${configuredSiteUrl.replace(/\/$/, '')}/`).toString();
}
if (typeof window === 'undefined') {
return path;
}
return new URL(path, window.location.origin).toString();
}
export function usePublicPageMeta({ title, description, path }) {
useHead({
title,
meta: [
{
name: 'description',
content: description,
},
{
name: 'robots',
content: 'index,follow',
},
{
property: 'og:title',
content: title,
},
{
property: 'og:description',
content: description,
},
{
property: 'og:type',
content: 'website',
},
],
link: [
{
rel: 'canonical',
href: getCanonicalUrl(path),
},
],
});
}

View File

@@ -1,5 +1,12 @@
<script setup>
import LandingSiteMenu from '@/features/landing/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/features/landing/publicPageMeta.js';
usePublicPageMeta({
title: 'Blogs | Socialize',
description: 'Practical articles on content review workflows, client approval, revision tracking, and publication handoff.',
path: '/blogs',
});
</script>
<template>

View File

@@ -1,5 +1,12 @@
<script setup>
import LandingSiteMenu from '@/features/landing/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/features/landing/publicPageMeta.js';
usePublicPageMeta({
title: 'Guides | Socialize',
description: 'Reusable guides for content intake, review rounds, approval decisions, and delivery readiness.',
path: '/guides',
});
</script>
<template>

View File

@@ -1,6 +1,13 @@
<script setup>
import { computed } from 'vue';
import LandingSiteMenu from '@/features/landing/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/features/landing/publicPageMeta.js';
usePublicPageMeta({
title: 'Socialize | Social media approval workflow',
description: 'Socialize helps teams manage social media content review, revisions, approval decisions, and publication handoff in one workflow.',
path: '/',
});
const pillars = computed(() => [
{

View File

@@ -1,5 +1,12 @@
<script setup>
import LandingSiteMenu from '@/features/landing/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/features/landing/publicPageMeta.js';
usePublicPageMeta({
title: 'Pricing | Socialize',
description: 'Socialize workspace pricing for teams managing social media content approvals with clients.',
path: '/pricing',
});
</script>
<template>

View File

@@ -1,5 +1,12 @@
<script setup>
import LandingSiteMenu from '@/features/landing/components/LandingSiteMenu.vue';
import { usePublicPageMeta } from '@/features/landing/publicPageMeta.js';
usePublicPageMeta({
title: 'Product | Socialize',
description: 'Socialize keeps content items, assets, revisions, comments, approval decisions, and publishing handoff details in one workspace.',
path: '/product',
});
</script>
<template>

View File

@@ -34,6 +34,7 @@ import { useNotificationsStore } from '@/features/notifications/stores/notificat
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
import { i18n } from '@/plugins/i18n.js';
import config from '@/config.js';
import { createHead } from '@vueuse/head';
const vuetify = createVuetify({
components: {
@@ -78,9 +79,11 @@ const vuetify = createVuetify({
});
const pinia = createPinia();
const head = createHead();
const app = createApp(App)
.use(pinia)
.use(head)
.use(vuetify)
.use(router)
.use(i18n)