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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ tags:
- "api"
websiteUrl: "https://coolapi.dev"
githubUrl: "https://github.com/cooldev/coolapi"
# Optional: only include when explicitly supplied for announcement tagging.
announcementXHandle: "CoolAPI_Team"
logoUrl: "https://www.google.com/s2/favicons?sz=64&domain_url=https://coolapi.dev"
ogImageUrl: "https://coolapi.dev/og-image.png"
logoWidth: 64
Expand Down Expand Up @@ -160,8 +162,9 @@ Required tool fields:

Optional tool fields:

- `announcementXHandle` — an explicitly supplied X account to tag in a listing announcement, not GitHub submitter attribution. Accept a bare handle or one leading `@`, with outer whitespace trimmed. The canonical published value is bare and preserves case; it must match ASCII `[A-Za-z0-9_]{1,15}`. Omit it when no tag is requested; empty or whitespace-only strings also mean no tag. URLs (including X/Twitter profile URLs), `null`, numbers, booleans, arrays, objects, and other invalid values are rejected. Never infer it from `submittedBy`, `tool-submitters.json`, `githubUrl`, website social metadata, or a product name; do not backfill existing listings.
- `seoTitle`, `seoDescription`, and `agentSummary` are optional, non-empty plain-text strings. Contributors may suggest them; the editorial reviewer owns the final wording. Existing listings need no changes.
- Surrounding whitespace is trimmed before validation and publishing; whitespace-only values are invalid.
- For these three SEO fields, surrounding whitespace is trimmed before validation and publishing; whitespace-only values are invalid.
- `seoTitle` overrides only the tool page title; `seoDescription` overrides only its meta and social description. Each falls back independently to the current generated text when omitted. `agentSummary` adds a paragraph before the existing visible description and does not replace it.
- Draft a specific, factual title around the tool and its agent use (usually about 50–60 characters). Write a description that helps a reader decide whether to open the profile (usually about 140–160 characters). Summarize the practical agent-facing outcome in one short paragraph (roughly 40–70 words). These are editorial drafting targets, not hard validation limits or automatic truncation rules. Avoid unsupported claims, keyword repetition, promotional language, and markdown or HTML.
- `githubUrl`
Expand All @@ -181,6 +184,8 @@ Optional tool fields:

The [tool review guide](docs/tool-review-guide.md) gives the reviewer the acceptance checklist and rollout behavior for SEO suggestions.

The announcement handle is retained by the source reader and published to D1 as nullable `announcement_x_handle`. Omission or blank input writes `NULL`, and removal clears any previous value. Existing attribution and asset generators preserve the authored field. This repository does not post announcements: a downstream announcement consumer must explicitly read this field, add `@` only when formatting a tag, and omit tagging when it is absent. Supplying it does not itself trigger or guarantee a post.

Do not add `contentModifiedAt` to a tool file. D1 sets it when the record is first published and advances it only when authored or visible data changes. `synced_at` is a separate operational timestamp that advances on every sync.

Valid `pricing` values:
Expand Down Expand Up @@ -261,6 +266,7 @@ What you need to provide:
- useful, specific tags
- `githubUrl` when the tool is open source
- `logoUrl` and `ogImageUrl` when you already have the canonical values, but these are optional
- optionally, `announcementXHandle` if you explicitly want that X account tagged in a listing announcement; a handle is never required to submit a tool

What you do not need to worry about:

Expand Down
7 changes: 7 additions & 0 deletions docs/d1-publish-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Schema migrations are staged before the full content sync. `0003_add_tool_classi

`0005_add_tool_seo_metadata.sql` adds nullable `seo_title`, `seo_description`, and `agent_summary` columns. Tool authors may propose these fields in frontmatter, but the editorial reviewer accepts or revises the public wording. Omission remains valid for existing tools. The full sync writes supplied values, clears removed values to `NULL`, and advances `content_modified_at` when any of the three changes. The site resolves title and description independently; `agent_summary` appears before the existing visible description.

`0007_add_announcement_x_handle.sql` adds nullable `announcement_x_handle TEXT` with no default or backfill. Its filename and SQL match the companion website migration (the content repository has no `0006` migration); keep this shared migration identity aligned. Apply it before deploying the updated full-sync publisher. The publish workflow already applies migrations before sync. Do not apply the `ALTER TABLE` twice manually if the website has already applied this migration to the same D1 database; inspect migration history during rollout.

Tool frontmatter may optionally supply `announcementXHandle`. The parser trims outer whitespace and removes one optional leading `@`, preserving case. The canonical nonblank value must match ASCII `[A-Za-z0-9_]{1,15}`; URLs and non-string values, including `null`, are invalid. Omission or blank strings publish `NULL`, and removing a value clears it. Insert, update, and change detection all include this column. GitHub `submittedBy`/`submitted_by_github` remains entirely separate: do not infer, backfill, or substitute it as an announcement handle. Source readers expose the canonical camelCase field to downstream announcement consumers, which must add `@` only for display and omit tagging when absent. Neither repository currently implements announcement posting; this persistence contract alone does not create or trigger a post.

See [tool review guide](tool-review-guide.md) for wording and acceptance criteria.

## CI And Merge Behavior
Expand Down Expand Up @@ -90,6 +94,7 @@ Validation must fail on any of the following:
- malformed evidence items or evidence without an access date
- invalid entity, verification, image-dimension, date, array, or indexability metadata
- an empty or non-string tool `seoTitle`, `seoDescription`, or `agentSummary` when supplied
- an invalid tool `announcementXHandle` (only omitted/blank or a 1–15-character ASCII handle, optionally prefixed with `@`, is accepted; URLs and `null` are rejected)
- an author-supplied `contentModifiedAt` field
- a missing or unsupported redirect-manifest version
- invalid redirect status/path combinations, duplicate sources, chains, or cycles
Expand All @@ -114,6 +119,7 @@ The D1 sync step must:
- set `is_active = 0` for redirect rows missing from the current manifest
- publish authored category scope, evidence, tool entity, comparison, and provenance fields
- publish optional tool SEO title, SEO description, and agent summary, including removal of previously approved values
- publish explicitly supplied `announcementXHandle` as nullable `announcement_x_handle`, clearing omitted/blank values and tracking edits independently of GitHub attribution
- initialize `content_modified_at` for a new or migrated record and advance it only when substantive fields change
- advance `synced_at` on every sync without using it as a public content modification date

Expand Down Expand Up @@ -176,6 +182,7 @@ tools(
pricing,
classification,
submitted_by_github,
announcement_x_handle,
logo_url,
og_image_url,
logo_width,
Expand Down
8 changes: 8 additions & 0 deletions docs/tool-review-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ Review each field against the tool's cited first-party evidence and the existing
`seoTitle` replaces the generated page title only. `seoDescription` replaces the generated meta, Open Graph, Twitter, and WebPage description only. These two fields fall back independently, so reviewers can approve one without the other. `agentSummary` appears as a paragraph before the existing short description; it does not replace the description used in tool entities, exports, feeds, or LLM summaries.

During rollout, approve these fields one tool at a time when the wording is ready. An omitted or removed field is published as `NULL`, and the site retains its established fallback or layout. A change to any field counts as a substantive tool edit for the content modification timestamp.

## Optional announcement tag

`announcementXHandle` is a separate, opt-in announcement-tagging value. Do not require it for acceptance or substitute a GitHub submitter, repository owner, product name, or scraped social account. Confirm it was explicitly supplied for tagging; existing tools need no backfill.

Accept outer whitespace and one optional leading `@`; publish the bare, case-preserved handle matching ASCII `[A-Za-z0-9_]{1,15}`. Omission and empty/whitespace-only strings mean no tag. Reject URLs (even X/Twitter profile URLs), `null`, other non-string types, extra `@` signs, punctuation, embedded whitespace, non-ASCII characters, and overlength handles. A lone `@` is invalid, not blank. Quote numeric-looking handles in YAML to keep them strings.

The D1 value is nullable `announcement_x_handle`; removing or blanking the source clears it to `NULL`. Changes, including case-only edits or removal, advance `content_modified_at`; equivalent `@`/bare inputs do not. Metadata generation must preserve the field independently of `submittedBy`. No announcement-posting implementation exists in this repository: downstream posting must consume this explicit field and omit a tag when it is absent, never fall back to GitHub attribution. This field does not itself authorize or trigger a post.
3 changes: 3 additions & 0 deletions migrations/0007_add_announcement_x_handle.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Optional, explicitly supplied announcement tag; independent of GitHub attribution.
-- Existing rows remain NULL (no announcement tag). Apply before the content publisher writes it.
ALTER TABLE tools ADD COLUMN announcement_x_handle TEXT;
13 changes: 13 additions & 0 deletions scripts/lib/content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ export function parseToolFile(raw, sourcePath) {
data[fieldName] = data[fieldName].trim();
}
}
if (typeof data.announcementXHandle === "string") {
const trimmed = data.announcementXHandle.trim();
// Only an actually blank input is absent; a lone @ must fail validation.
data.announcementXHandle = trimmed === "" ? undefined : trimmed.replace(/^@/, "");
}
return {
...data,
body: parsed.content.trim(),
Expand Down Expand Up @@ -457,6 +462,14 @@ export async function validateContent(rootDir = process.cwd()) {
errors.push(`${tool.sourcePath}: sortOrder must be an integer when present`);
}

if (tool.announcementXHandle !== undefined
&& (typeof tool.announcementXHandle !== "string"
|| !/^[A-Za-z0-9_]{1,15}$/.test(tool.announcementXHandle))) {
errors.push(
`${tool.sourcePath}: announcementXHandle must be 1–15 ASCII letters, digits, or underscores, optionally prefixed with @; URLs are not supported`,
);
}

for (const fieldName of [
"seoTitle",
"seoDescription",
Expand Down
6 changes: 4 additions & 2 deletions scripts/lib/sync-sql.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const TOOL_CONTENT_COLUMNS = [
"pricing",
"classification",
"submitted_by_github",
"announcement_x_handle",
"logo_url",
"og_image_url",
"logo_width",
Expand Down Expand Up @@ -193,7 +194,7 @@ ON CONFLICT(slug) DO UPDATE SET
INSERT INTO tools (
slug, name, description, seo_title, seo_description, agent_summary,
body_md, category_slug, tags_json, website_url,
github_url, pricing, classification, submitted_by_github, logo_url, og_image_url,
github_url, pricing, classification, submitted_by_github, announcement_x_handle, logo_url, og_image_url,
logo_width, logo_height, og_image_width, og_image_height, entity_type,
developer_name, docs_url, pricing_url, license_url, interfaces_json,
deployment_modes_json, evidence_json, verification_level,
Expand All @@ -208,7 +209,7 @@ VALUES (
${sqlString(tool.body)},
${sqlString(tool.category)}, ${sqlJson(tool.tags)}, ${sqlString(tool.websiteUrl)},
${sqlString(tool.githubUrl)}, ${sqlString(tool.pricing)}, ${sqlString(tool.classification)},
${sqlString(tool.submittedBy)}, ${sqlString(tool.logoUrl)}, ${sqlString(tool.ogImageUrl)},
${sqlString(tool.submittedBy)}, ${sqlString(tool.announcementXHandle)}, ${sqlString(tool.logoUrl)}, ${sqlString(tool.ogImageUrl)},
${sqlInteger(tool.logoWidth)}, ${sqlInteger(tool.logoHeight)}, ${sqlInteger(tool.ogImageWidth)},
${sqlInteger(tool.ogImageHeight)}, ${sqlString(tool.entityType)}, ${sqlString(tool.developerName)},
${sqlString(tool.docsUrl)}, ${sqlString(tool.pricingUrl)}, ${sqlString(tool.licenseUrl)},
Expand All @@ -234,6 +235,7 @@ ON CONFLICT(slug) DO UPDATE SET
pricing = excluded.pricing,
classification = excluded.classification,
submitted_by_github = excluded.submitted_by_github,
announcement_x_handle = excluded.announcement_x_handle,
logo_url = excluded.logo_url,
og_image_url = excluded.og_image_url,
logo_width = excluded.logo_width,
Expand Down
Loading
Loading