xiaohongshu
xiaohongshu
composable-svelte-ssr
Server-side rendering patterns for Composable Svelte. Use when implementing SSR, hydration, server rendering, isomorphic code, or working with meta tags and SEO. Covers renderToHTML, hydrateStore, server-side routing, state serialization, and avoiding common SSR pitfalls.
Full skill instructions
This skill covers server-side rendering (SSR) patterns for Composable Svelte applications.
import { renderToHTML } from '@composable-svelte/core/ssr';
import { createStore } from '@composable-svelte/core';
import App from './App.svelte';
app.get('/', async (req, res) => {
// 1. Load data for this request
const data = await loadData(req.user);
// 2. Create store with pre-populated data
const store = createStore({
initialState: data,
reducer: appReducer,
dependencies: {} // Empty on server - effects won't run
});
// 3. Render to HTML
const html = renderToHTML(App, { store }, {
head: `<link rel="stylesheet" href="/assets/index.css">`,
clientScript: '/assets/index.js'
});
// 4. Send response
res.send(html);
});
import { hydrateStore } from '@composable-svelte/core/ssr';
import { mount } from 'svelte';
import App from './App.svelte';
// 1. Read state from script tag
const stateJSON = document.getElementById('__COMPOSABLE_SVELTE_STATE__')?.textContent;
// 2. Hydrate with client dependencies
const store = hydrateStore(stateJSON, {
reducer: appReducer,
dependencies: {
api: createAPIClient(),
storage: createLocalStorage()
}
});
// 3. Mount app (reuses existing DOM from SSR)
mount(App, { target: document.body, props: { store } });
Key Pattern: Server has empty dependencies, client has real implementations.
// server.ts
const store = createStore({
initialState: data,
reducer: appReducer,
dependencies: {} as AppDependencies // Empty - effects won't run
// ssr.deferEffects defaults to true, so effects are automatically skipped
});
// client.ts
const store = hydrateStore(stateJSON, {
reducer: appReducer,
dependencies: {
api: createAPIClient(), // Real API client
storage: localStorage, // Real storage
clock: new SystemClock() // Real clock
}
});
Why: Server doesn't need to execute effects - it just renders initial state. Client needs real dependencies for interactivity.
Key Pattern: Use router's pure functions (parseDestination, matchPath, serializeDestination) on both server and client.
// routing.ts (shared between server and client)
export function parsePostFromURL(path: string, defaultId: number): number {
const match = path.match(/^\/posts\/(\d+)$/);
return match ? parseInt(match[1], 10) : defaultId;
}
// server.ts
async function renderApp(request: any, reply: any) {
const posts = await loadPosts();
const path = request.url;
const requestedPostId = parsePostFromURL(path, posts[0]?.id || 1);
const store = createStore({
initialState: {
posts,
selectedPostId: requestedPostId,
meta: computeMetaForPost(posts.find(p => p.id === requestedPostId))
},
reducer: appReducer,
dependencies: {}
});
const html = renderToHTML(App, { store });
reply.type('text/html').send(html);
}
// server.ts
import { parseDestination } from './shared/routing';
app.get('/posts/:id', async (req, res) => {
const postId = parseInt(req.params.id, 10);
// Load data based on URL
const posts = await loadPosts();
const selectedPost = posts.find(p => p.id === postId) || posts[0];
// Initialize state with URL-driven selection
const store = createStore({
initialState: {
posts,
selectedPostId: selectedPost?.id || null,
// Set initial meta based on URL-selected post
meta: selectedPost
? {
title: `${selectedPost.title} - Blog`,
description: selectedPost.content.slice(0, 160),
ogImage: `/og/post-${selectedPost.id}.jpg`,
canonical: `https://example.com/posts/${selectedPost.id}`
}
: initialState.meta
},
reducer: appReducer,
dependencies: {}
});
const html = renderToHTML(App, { store });
res.send(html);
});
When you call renderToHTML, the store state is automatically serialized and embedded in the HTML:
const html = renderToHTML(App, { store });
// HTML contains: <script id="__COMPOSABLE_SVELTE_STATE__" type="application/json">...</script>
When you call hydrateStore, the state is automatically deserialized:
const stateJSON = document.getElementById('__COMPOSABLE_SVELTE_STATE__')?.textContent;
const store = hydrateStore(stateJSON, { reducer, dependencies });
For complex types (Date, Map, Set), provide custom serializers:
import { serializeState, parseState } from '@composable-svelte/core/ssr';
// Server
const serialized = serializeState(store.state, {
customSerializers: {
Date: (date) => ({ __type: 'Date', value: date.toISOString() }),
Map: (map) => ({ __type: 'Map', entries: Array.from(map.entries()) })
}
});
// Client
const state = parseState(serialized, {
customParsers: {
Date: (obj) => new Date(obj.value),
Map: (obj) => new Map(obj.entries)
}
});
Best Practice: Meta tags should be computed from state in the reducer, then rendered via <svelte:head> in components.
// State
interface AppState {
posts: Post[];
selectedPostId: number | null;
meta: MetaTags;
}
interface MetaTags {
title: string;
description: string;
ogImage?: string;
canonical?: string;
}
// Reducer computes meta tags
case 'selectPost': {
const post = state.posts.find(p => p.id === action.postId);
return [
{
...state,
selectedPostId: action.postId,
meta: post
? {
title: `${post.title} - Blog`,
description: post.content.slice(0, 160),
ogImage: `/og/post-${post.id}.jpg`,
canonical: `https://example.com/posts/${post.id}`
}
: state.meta
},
Effect.none()
];
}
// Component renders meta tags
<svelte:head>
<title>{$store.meta.title}</title>
<meta name="description" content={$store.meta.description} />
{#if $store.meta.ogImage}
<meta property="og:title" content={$store.meta.title} />
<meta property="og:description" content={$store.meta.description} />
<meta property="og:image" content={$store.meta.ogImage} />
{/if}
{#if $store.meta.canonical}
<link rel="canonical" href={$store.meta.canonical} />
{/if}
</svelte:head>
Why: Meta tags are part of application state. Computing them in the reducer ensures they're consistent on server and client, and testable with TestStore.
// server/index.ts
import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import { createStore } from '@composable-svelte/core';
import { renderToHTML } from '@composable-svelte/core/ssr';
import App from '../shared/App.svelte';
import { appReducer } from '../shared/reducer';
import { initialState } from '../shared/types';
import { loadPosts } from './data';
import { parsePostFromURL } from '../shared/routing';
const app = Fastify({
logger: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
}
});
// Serve static files (client bundle)
app.register(fastifyStatic, {
root: join(__dirname, '../client'),
prefix: '/assets/'
});
// Main SSR route handler
async function renderAppRoute(request: any, reply: any) {
try {
// 1. Parse URL using router (same logic as client!)
const posts = await loadPosts();
const path = request.url;
const requestedPostId = parsePostFromURL(path, posts[0]?.id || 1);
// Find the requested post
const selectedPost = posts.find((p) => p.id === requestedPostId) || posts[0];
// 2. Create store with URL-driven state
const store = createStore({
initialState: {
...initialState,
posts,
selectedPostId: selectedPost?.id || null,
// Set initial meta based on URL-selected post
meta: selectedPost
? {
title: `${selectedPost.title} - Blog`,
description: selectedPost.content.slice(0, 160),
ogImage: `/og/post-${selectedPost.id}.jpg`,
canonical: `https://example.com/posts/${selectedPost.id}`
}
: initialState.meta
},
reducer: appReducer,
dependencies: {}
// ssr.deferEffects defaults to true, so effects are automatically skipped
});
// 3. Render component to HTML
const html = renderToHTML(App, { store }, {
head: `
<link rel="stylesheet" href="/assets/index.css">
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; }
</style>
`,
clientScript: '/assets/index.js'
});
// 4. Send response
reply.type('text/html').send(html);
} catch (error) {
request.log.error(error);
reply.status(500).send({
error: 'Internal Server Error',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// Register routes
app.get('/', renderAppRoute);
app.get('/posts/:id', renderAppRoute);
// Start server
const start = async () => {
try {
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const host = process.env.HOST || '0.0.0.0';
await app.listen({ port, host });
console.log(`Server running at http://localhost:${port}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
// client/index.ts
import { hydrate as hydrateComponent } from 'svelte';
import { hydrateStore } from '@composable-svelte/core/ssr';
import { syncBrowserHistory } from '@composable-svelte/core/routing';
import App from '../shared/App.svelte';
import { appReducer } from '../shared/reducer';
import type { AppDependencies, AppState, AppAction } from '../shared/types';
import { parserConfig, serializerConfig } from '../shared/routing';
// Client-side dependencies
const clientDependencies: AppDependencies = {
fetchPosts: async () => {
// In a real app, this would fetch from an API
// For this example, we'll just return empty array
// (the data is already loaded via SSR)
return [];
}
};
// Hydrate the application
function hydrate() {
try {
// 1. Read serialized state from the server
const stateElement = document.getElementById('__COMPOSABLE_SVELTE_STATE__');
if (!stateElement || !stateElement.textContent) {
throw new Error('No hydration data found. Server-side rendering may have failed.');
}
// 2. Hydrate the store with client dependencies
const store = hydrateStore<AppState, AppAction, AppDependencies>(
stateElement.textContent,
{
reducer: appReducer,
dependencies: clientDependencies
}
);
// 3. Sync browser history with state (URL routing!)
syncBrowserHistory(store, {
serializers: serializerConfig.serializers,
parsers: parserConfig.parsers,
// Map state → destination for URL serialization
getDestination: (state) => {
if (state.selectedPostId !== null) {
return { type: 'post' as const, state: { postId: state.selectedPostId } };
}
return null;
},
// Map destination → action for back/forward navigation
destinationToAction: (dest) => {
if (dest?.type === 'post') {
return { type: 'selectPost', postId: dest.state.postId };
}
return null;
}
});
// 4. Hydrate the app (reuse existing DOM from SSR)
const app = hydrateComponent(App, {
target: document.body,
props: { store }
});
console.log('✅ Composable Svelte hydrated successfully with URL routing');
// Cleanup on unmount (for HMR during development)
if (import.meta.hot) {
import.meta.hot.dispose(() => {
app.$destroy?.();
});
}
} catch (error) {
console.error('❌ Hydration failed:', error);
// Show error to user
document.body.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #fee; color: #c00; font-family: monospace; padding: 2rem;">
<div>
<h1>Hydration Error</h1>
<p>${error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
</div>
`;
}
}
// Start hydration when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hydrate);
} else {
hydrate();
}
// server.ts
// ❌ BAD: Shared store across requests
const globalStore = createStore({
initialState: {},
reducer: appReducer,
dependencies: {}
});
app.get('/', (req, res) => {
// ❌ State leaks between requests!
const html = renderToHTML(App, { store: globalStore });
res.send(html);
});
// server.ts
// ✅ GOOD: Create new store for each request
app.get('/', async (req, res) => {
const store = createStore({
initialState: await loadDataForRequest(req),
reducer: appReducer,
dependencies: {}
});
const html = renderToHTML(App, { store });
res.send(html);
});
Why: Each request needs its own store to prevent state leaking between users.
Key Pattern: For non-SvelteKit servers (Fastify, Express, etc.), manually initialize i18n state and dependencies.
import { createInitialI18nState, BundledTranslationLoader, createStaticLocaleDetector, serverDOM, browserDOM } from '@composable-svelte/core/i18n';
// Server: Detect locale from request
function detectLocale(request: any): string {
// 1. Check query param (?lang=fr)
const queryLang = request.query?.lang;
if (queryLang && ['en', 'fr', 'es'].includes(queryLang)) {
return queryLang;
}
// 2. Check Accept-Language header
const acceptLanguage = request.headers?.['accept-language'];
if (acceptLanguage && typeof acceptLanguage === 'string') {
const languages = acceptLanguage.split(',')
.map(lang => lang.trim().split(';')[0].split('-')[0]);
for (const lang of languages) {
if (['en', 'fr', 'es'].includes(lang)) {
return lang;
}
}
}
// 3. Default to English
return 'en';
}
// Server: Initialize i18n for SSR
async function renderApp(request, reply) {
const locale = detectLocale(request);
const i18nState = createInitialI18nState(locale, ['en', 'fr', 'es'], 'en');
// Create translation loader
const translationLoader = new BundledTranslationLoader({
bundles: {
en: { common: enTranslations },
fr: { common: frTranslations },
es: { common: esTranslations }
}
});
// Preload translations for current locale
const translations = await translationLoader.load('common', locale);
const updatedI18nState = {
...i18nState,
translations: { [`${locale}:common`]: translations }
};
// Create mock storage for server (no-op)
const mockStorage = {
getItem: (key: string) => null,
setItem: (key: string, value: unknown) => {},
removeItem: (key: string) => {},
keys: () => [],
has: (key: string) => false,
clear: () => {}
};
// Create i18n dependencies for server
const i18nDependencies = {
translationLoader,
localeDetector: createStaticLocaleDetector(locale, ['en', 'fr', 'es']),
storage: mockStorage,
dom: serverDOM
};
const store = createStore({
initialState: {
...initialState,
i18n: updatedI18nState
},
reducer: appReducer,
dependencies: {
...otherDependencies,
...i18nDependencies
}
});
const html = renderToHTML(App, { store });
reply.type('text/html').send(html);
}
import { BundledTranslationLoader, createStaticLocaleDetector, browserDOM } from '@composable-svelte/core/i18n';
// Client: Hydrate with localStorage-backed storage
const clientStorage = {
getItem: (key: string) => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
setItem: (key: string, value: unknown) => {
try {
localStorage.setItem(key, String(value));
} catch {}
},
removeItem: (key: string) => {
try {
localStorage.removeItem(key);
} catch {}
},
keys: () => {
try {
return Object.keys(localStorage);
} catch {
return [];
}
},
has: (key: string) => {
try {
return localStorage.getItem(key) !== null;
} catch {
return false;
}
},
clear: () => {
try {
localStorage.clear();
} catch {}
}
};
// Hydrate i18n on client
async function hydrate() {
const stateElement = document.getElementById('__COMPOSABLE_SVELTE_STATE__');
const parsedState = JSON.parse(stateElement.textContent);
const locale = parsedState.i18n.currentLocale;
const translationLoader = new BundledTranslationLoader({
bundles: {
en: { common: enTranslations },
fr: { common: frTranslations },
es: { common: esTranslations }
}
});
const i18nDependencies = {
translationLoader,
localeDetector: createStaticLocaleDetector(locale, ['en', 'fr', 'es']),
storage: clientStorage, // Real localStorage
dom: browserDOM
};
const store = hydrateStore(stateElement.textContent, {
reducer: appReducer,
dependencies: {
...otherDependencies,
...i18nDependencies
}
});
hydrateComponent(App, { target: document.body, props: { store } });
}
Critical: The i18n system expects a Storage interface, not a Map.
// ❌ WRONG - Map doesn't have setItem/getItem
const i18nDependencies = {
storage: new Map() // TypeError: storage.setItem is not a function
};
// ✅ CORRECT - Implement Storage interface
const mockStorage: Storage = {
getItem: (key: string) => null,
setItem: (key: string, value: unknown) => {},
removeItem: (key: string) => {},
keys: () => [],
has: (key: string) => false,
clear: () => {}
};
Combine i18n with URL routing to support language selection via URL:
// Server: Support ?lang=fr query parameter
app.get('/', async (req, res) => {
const locale = detectLocale(req); // Checks ?lang= first, then Accept-Language
const destination = parseDestinationFromURL(req.url);
const store = createStore({
initialState: {
destination,
i18n: createInitialI18nState(locale, ['en', 'fr', 'es'])
// ...
}
});
// ...
});
URL Pattern: http://localhost:3000/?lang=fr or http://localhost:3000/posts/1?lang=es
Automatic: Effects are automatically deferred on server (via ssr.deferEffects: true default).
// No need to set this explicitly - it's the default
const store = createStore({
initialState: data,
reducer: appReducer,
dependencies: {},
// ssr: { deferEffects: true } // Default
});
app.get('/posts/:id', async (req, res) => {
// Load data once
const posts = await loadPosts();
const postId = parseInt(req.params.id, 10);
const selectedPost = posts.find(p => p.id === postId);
// Initialize state with loaded data
const store = createStore({
initialState: {
posts, // Data already loaded
selectedPostId: postId,
meta: computeMeta(selectedPost)
},
reducer: appReducer,
dependencies: {}
});
const html = renderToHTML(App, { store });
res.send(html);
});
Why: Server pre-loads data, client hydrates with it. No need to fetch again on client.
❌ WRONG:
// reducer.ts
case 'init':
const theme = localStorage.getItem('theme'); // ❌ localStorage not available on server!
return [{ ...state, theme }, Effect.none()];
✅ CORRECT:
// Use environment detection
import { isServer } from '@composable-svelte/core/ssr';
case 'init':
const theme = isServer ? 'light' : localStorage.getItem('theme') || 'light';
return [{ ...state, theme }, Effect.none()];
❌ WRONG:
// client.ts
const store = hydrateStore(stateJSON, { reducer, dependencies });
mount(App, { target: document.body, props: { store } });
// If hydration fails, user sees blank screen!
✅ CORRECT:
try {
const store = hydrateStore(stateJSON, { reducer, dependencies });
mount(App, { target: document.body, props: { store } });
console.log('✅ Hydrated successfully');
} catch (error) {
console.error('❌ Hydration failed:', error);
// Show error UI
document.body.innerHTML = `<div class="error">Hydration failed: ${error.message}</div>`;
}
❌ WRONG:
// No meta tags in state, no <svelte:head> in component
// Search engines see generic meta tags
✅ CORRECT:
// State includes meta tags
interface AppState {
meta: { title: string; description: string; ogImage?: string };
}
// Component renders meta tags
<svelte:head>
<title>{$store.meta.title}</title>
<meta name="description" content={$store.meta.description} />
</svelte:head>
<svelte:head>import { generateStaticSite } from '@composable-svelte/core/ssr';
import App from './App.svelte';
import { appReducer } from './reducer';
const posts = await loadPosts();
const result = await generateStaticSite(App, {
routes: [
{ path: '/' },
{ path: '/about' },
{
path: '/posts/:id',
paths: posts.map(p => `/posts/${p.id}`),
getServerProps: async (path) => ({
post: await loadPost(path)
})
}
],
outDir: './dist',
baseURL: 'https://example.com',
onPageGenerated: (path, outPath) => {
console.log(`Generated ${path} → ${outPath}`);
}
}, {
reducer: appReducer,
dependencies: {},
getInitialState: (path) => ({ /* compute state for path */ })
});
console.log(`Generated ${result.pagesGenerated} pages in ${result.duration}ms`);
Full-Site Generation:
// Generate all routes at build time
await generateStaticSite(App, {
routes: [
{ path: '/' }, // Static route
{ path: '/about' }, // Static route
{
path: '/posts/:id', // Dynamic route
paths: ['/posts/1', '/posts/2'] // Pre-rendered paths
}
],
outDir: './static'
}, { reducer, dependencies: {} });
Selective Generation:
// Generate only specific pages
await generateStaticSite(App, {
routes: [
{ path: '/' }, // Only homepage
{ path: '/posts/1' } // Only one post
],
outDir: './static'
}, { reducer, dependencies: {} });
Dynamic Path Generation:
// Fetch paths dynamically at build time
await generateStaticSite(App, {
routes: [
{
path: '/posts/:id',
paths: async () => {
const posts = await loadAllPosts();
return posts.map(p => `/posts/${p.id}`);
}
}
],
outDir: './static'
}, { reducer, dependencies: {} });
Multi-Locale Static Generation:
const supportedLocales = ['en', 'fr', 'es'];
const posts = await loadPosts();
// Generate routes for each locale
const routes = [];
for (const locale of supportedLocales) {
const localePrefix = locale === 'en' ? '' : `/${locale}`;
// Home page
routes.push({
path: `${localePrefix}/`,
getServerProps: async (path) => {
const i18nState = await initI18n(locale);
return { ...initialState, i18n: i18nState };
}
});
// Post pages
for (const post of posts) {
routes.push({
path: `${localePrefix}/posts/${post.id}`,
getServerProps: async (path) => {
const i18nState = await initI18n(locale);
const post = await loadPost(post.id);
return { ...initialState, post, i18n: i18nState };
}
});
}
}
await generateStaticSite(App, { routes, outDir: './static' }, { reducer });
Create build script (src/build/ssg.ts):
import { generateStaticSite } from '@composable-svelte/core/ssr';
import App from '../shared/App.svelte';
import { appReducer } from '../shared/reducer';
import { loadPosts } from '../server/data';
async function build() {
console.log('Starting SSG build...');
const posts = await loadPosts();
const result = await generateStaticSite(App, {
routes: [
{ path: '/' },
{
path: '/posts/:id',
paths: posts.map(p => `/posts/${p.id}`),
getServerProps: async (path) => {
const id = parseInt(path.split('/').pop()!);
const post = await loadPost(id);
return { posts: [post] };
}
}
],
outDir: './static',
baseURL: 'https://example.com'
}, {
reducer: appReducer,
dependencies: {}
});
console.log(`✅ Generated ${result.pagesGenerated} pages in ${result.duration}ms`);
}
build().catch(console.error);
Add script to package.json:
{
"scripts": {
"build:ssg": "vite build && tsx src/build/ssg.ts"
}
}
Run build:
pnpm build:ssg
| Use Case | Recommendation | Reason |
|---|---|---|
| Blog posts | SSG | Content rarely changes, many reads |
| User dashboards | SSR | Personalized, private data |
| Product catalog | SSG | Public, static content |
| Search results | SSR | Dynamic, user-specific |
| Marketing pages | SSG | Static, performance-critical |
| Admin panels | SSR | Dynamic, authenticated |
Use SSG for static pages, SSR for dynamic:
Build-time (SSG): Generate static pages
pnpm build:ssg # Generates /static/index.html, /static/posts/*/index.html
Runtime (SSR): Serve dynamic pages
// Server fallback for non-static routes
app.get('*', async (req, res) => {
// Try to serve static file first
const staticPath = join(__dirname, '../static', req.url, 'index.html');
if (existsSync(staticPath)) {
return res.sendFile(staticPath);
}
// Fall back to SSR for dynamic routes
const store = createStore({ /* ... */ });
const html = renderToHTML(App, { store });
res.send(html);
});
This skill covers SSR and SSG patterns for Composable Svelte:
SSR Key Points:
SSG Key Points:
i18n Key Points:
BundledTranslationLoader with bundles wrappercreateStaticLocaleDetector, serverDOMcreateStaticLocaleDetector, browserDOMgetItem/setItem, not Map's get/setFor core architecture, see composable-svelte-core skill. For URL routing, see composable-svelte-navigation skill. For testing SSR/SSG, see composable-svelte-testing skill.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience