Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@rectangular-labs/ui",
"www",
"seo",
"seo-www"
"seo-www",
"contact"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
TabsTrigger,
} from "@rectangular-labs/ui/components/ui/tabs";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { type ReactNode, useEffect, useState } from "react";
import { getApiClientRq } from "~/lib/api";
import { LoadingError } from "~/routes/_authed/-components/loading-error";
import { ManageContentMetadataDialog } from "./manage-content-metadata-dialog";
Expand Down Expand Up @@ -147,8 +147,6 @@ export function useContentDisplayController({
{ status: "idle" } | { status: "saving" } | { status: "saved"; at: string }
>({ status: "idle" });
const [lastSavedContentMarkdown, setLastSavedContentMarkdown] = useState("");
const latestSaveVersionRef = useRef(0);
const latestAppliedSaveVersionRef = useRef(0);

const [draftDetails, setDraftDetails] = useState({
contentMarkdown: "",
Expand All @@ -163,11 +161,8 @@ export function useContentDisplayController({
await Promise.all([
contentDetailsQuery.refetch(),
queryClient.invalidateQueries({
queryKey: api.content.list.queryKey({
input: {
organizationIdentifier,
projectId,
},
queryKey: api.content.list.key({
type: "query",
}),
}),
]);
Expand Down Expand Up @@ -197,41 +192,40 @@ export function useContentDisplayController({
status: "saved",
at: new Date(draft.updatedAt).toISOString(),
});
latestSaveVersionRef.current = 0;
latestAppliedSaveVersionRef.current = 0;
}, [draft]);

useEffect(() => {
if (!draft || !canEdit) return;
if (draftDetails.contentMarkdown === lastSavedContentMarkdown) return;
const markdownToSave = draftDetails.contentMarkdown;
if (markdownToSave === lastSavedContentMarkdown) return;

let cancelled = false;
setContentSaveIndicator({ status: "saving" });
const timeoutId = window.setTimeout(() => {
const saveVersion = ++latestSaveVersionRef.current;
const markdownToSave = draftDetails.contentMarkdown;

void saveContentMarkdownAsync({
organizationIdentifier,
projectId,
id: draft.id,
contentMarkdown: markdownToSave,
})
.then(() => {
if (saveVersion < latestAppliedSaveVersionRef.current) return;
latestAppliedSaveVersionRef.current = saveVersion;
if (cancelled) return;
setLastSavedContentMarkdown(markdownToSave);
setContentSaveIndicator({
status: "saved",
at: new Date().toISOString(),
});
})
.catch(() => {
if (saveVersion < latestAppliedSaveVersionRef.current) return;
if (cancelled) return;
setContentSaveIndicator({ status: "idle" });
});
}, 800);

return () => window.clearTimeout(timeoutId);
return () => {
cancelled = true;
window.clearTimeout(timeoutId);
};
}, [
canEdit,
draft,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,8 @@ export function ManageContentMetadataDialog({
onOpenChange(false);
await Promise.all([
queryClient.invalidateQueries({
queryKey: api.content.list.queryKey({
input: {
organizationIdentifier,
projectId,
},
queryKey: api.content.list.key({
type: "query",
}),
}),
draftId
Expand Down
1 change: 1 addition & 0 deletions packages/api-seo/src/routes/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const list = withOrganizationIdBase
db: context.db,
organizationId: context.organization.id,
projectId: input.projectId,
strategyId: input.strategyId,
});
if (!rowsResult.ok) {
throw new ORPCError("INTERNAL_SERVER_ERROR", {
Expand Down
78 changes: 36 additions & 42 deletions packages/api-seo/src/workflows/onboarding-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,14 @@ export class SeoOnboardingWorkflow extends WorkflowEntrypoint<
}

logInfo("homepage title", { homepageTitle });
const system = `You extract a company or product name from a homepage title.
const system = `You extract a company or product name from a homepage title/website url.

## Task
- Analyze the provided homepage title and URL to derive the entity name.

## Guidelines
- Return the homepage title exactly as provided in the input.
- Extract the brand or product name from the title; if unclear, return an empty string.

## Expectations
- Provide concise, exact values without extra commentary.`;
- use the homepage title and URL to derive the entity name.
- If unclear, return an empty string.`;

const { experimental_output } = await generateText({
model: google("gemini-3-flash-preview"),
Expand Down Expand Up @@ -211,43 +208,40 @@ Extract the name from the above context.`,
},
);

await step.do("trigger strategy suggestions workflow", async () => {
const db = createDb();
const taskResult = await createTask({
db,
userId: undefined,
input: {
type: "seo-generate-strategy-suggestions",
projectId: project.id,
instructions: ONBOARDING_STRATEGY_SUGGESTION_INSTRUCTIONS,
},
workflowInstanceId: `strategy_${event.instanceId}_${crypto.randomUUID().slice(0, 5)}`,
});
if (!taskResult.ok) {
logError("failed to trigger strategy suggestions workflow", {
projectId: project.id,
error: taskResult.error,
const [, brandVoiceResult] = await Promise.all([
step.do("trigger strategy suggestions workflow", async () => {
const db = createDb();
const taskResult = await createTask({
db,
userId: undefined,
input: {
type: "seo-generate-strategy-suggestions",
projectId: project.id,
instructions: ONBOARDING_STRATEGY_SUGGESTION_INSTRUCTIONS,
},
workflowInstanceId: `strategy_${event.instanceId}_${crypto.randomUUID().slice(0, 5)}`,
});
return;
}
if (!taskResult.ok) {
logError("failed to trigger strategy suggestions workflow", {
projectId: project.id,
error: taskResult.error,
});
return;
}

const updateResult = await updateSeoProject(db, {
id: project.id,
organizationId: project.organizationId,
strategySuggestionsWorkflowId: taskResult.value.id,
});
if (!updateResult.ok) {
logError("failed to save strategy suggestions workflow id", {
projectId: project.id,
error: updateResult.error,
const updateResult = await updateSeoProject(db, {
id: project.id,
organizationId: project.organizationId,
strategySuggestionsWorkflowId: taskResult.value.id,
});
}
});

const brandVoiceResult = await step.do(
"generate brand voice",
{ timeout: "10 minutes" },
async () => {
if (!updateResult.ok) {
logError("failed to save strategy suggestions workflow id", {
projectId: project.id,
error: updateResult.error,
});
}
}),
step.do("generate brand voice", { timeout: "10 minutes" }, async () => {
const { tools } = createWebToolsWithMetadata(project, this.env.CACHE);
const system = `You are an SEO research expert extracting a brand's writing tone.

Expand Down Expand Up @@ -289,8 +283,8 @@ Extract the name from the above context.`,
});

return outputResult.experimental_output;
},
);
}),
]);

const { brandVoice } = await step.do(
"update project brand voice",
Expand Down
3 changes: 2 additions & 1 deletion packages/api-seo/src/workflows/planner-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export class SeoPlannerWorkflow extends WorkflowEntrypoint<
organizationId: input.organizationId,
projectId: input.projectId,
id: input.draftId,
withContent: true,
});
if (!contentResult.ok) {
throw new NonRetryableError(contentResult.error.message);
Expand Down Expand Up @@ -447,7 +448,7 @@ export class SeoPlannerWorkflow extends WorkflowEntrypoint<
locationName,
languageCode,
project,
notes: "(none)",
notes: content.notes ?? undefined,
};
});

Expand Down
Loading