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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ site/public/api-meta/
site/public/api-catalog.json
site/public/docs-markdown/
site/public/sitemap.xml
site/public/feed.xml

###############
# OS / IDE
Expand Down
47 changes: 32 additions & 15 deletions site/app/composables/usePageSeo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type PageSeoOptions = {
title?: MaybeRefOrGetter<string | undefined>
description?: MaybeRefOrGetter<string | undefined>
image?: MaybeRefOrGetter<string | undefined>
/** Article last-updated day as YYYY-MM-DD (adds Open Graph modified_time). */
modifiedTime?: MaybeRefOrGetter<string | undefined>
}

export function usePageSeo(options: PageSeoOptions = {}) {
Expand All @@ -22,23 +24,38 @@ export function usePageSeo(options: PageSeoOptions = {}) {
const description = computed(() => toValue(options.description) ?? DEFAULT_DESCRIPTION)
const image = computed(() => `${siteUrl}${toValue(options.image) ?? DEFAULT_OG_IMAGE}`)
const url = computed(() => `${siteUrl}${route.path}`)
const modifiedTime = computed(() => {
const day = toValue(options.modifiedTime)
return day ? `${day}T00:00:00.000Z` : undefined
})

useHead({
title,
meta: computed(() => [
{ name: 'description', content: description.value, tagPriority: seoTagPriority },
{ property: 'og:site_name', content: 'AutoFixture', tagPriority: seoTagPriority },
{ property: 'og:type', content: 'website', tagPriority: seoTagPriority },
{ property: 'og:title', content: title.value, tagPriority: seoTagPriority },
{ property: 'og:description', content: description.value, tagPriority: seoTagPriority },
{ property: 'og:image', content: image.value, tagPriority: seoTagPriority },
{ property: 'og:image:width', content: OG_IMAGE_WIDTH, tagPriority: seoTagPriority },
{ property: 'og:image:height', content: OG_IMAGE_HEIGHT, tagPriority: seoTagPriority },
{ property: 'og:url', content: url.value, tagPriority: seoTagPriority },
{ name: 'twitter:card', content: 'summary_large_image', tagPriority: seoTagPriority },
{ name: 'twitter:title', content: title.value, tagPriority: seoTagPriority },
{ name: 'twitter:description', content: description.value, tagPriority: seoTagPriority },
{ name: 'twitter:image', content: image.value, tagPriority: seoTagPriority },
]),
meta: computed(() => {
const tags = [
{ name: 'description', content: description.value, tagPriority: seoTagPriority },
{ property: 'og:site_name', content: 'AutoFixture', tagPriority: seoTagPriority },
{ property: 'og:type', content: 'website', tagPriority: seoTagPriority },
{ property: 'og:title', content: title.value, tagPriority: seoTagPriority },
{ property: 'og:description', content: description.value, tagPriority: seoTagPriority },
{ property: 'og:image', content: image.value, tagPriority: seoTagPriority },
{ property: 'og:image:width', content: OG_IMAGE_WIDTH, tagPriority: seoTagPriority },
{ property: 'og:image:height', content: OG_IMAGE_HEIGHT, tagPriority: seoTagPriority },
{ property: 'og:url', content: url.value, tagPriority: seoTagPriority },
{ name: 'twitter:card', content: 'summary_large_image', tagPriority: seoTagPriority },
{ name: 'twitter:title', content: title.value, tagPriority: seoTagPriority },
{ name: 'twitter:description', content: description.value, tagPriority: seoTagPriority },
{ name: 'twitter:image', content: image.value, tagPriority: seoTagPriority },
]

if (modifiedTime.value) {
tags.push(
{ property: 'article:modified_time', content: modifiedTime.value, tagPriority: seoTagPriority },
{ property: 'og:updated_time', content: modifiedTime.value, tagPriority: seoTagPriority },
)
}

return tags
}),
})
}
15 changes: 14 additions & 1 deletion site/app/pages/docs/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { formatDocsUpdatedDate } from '~/utils/formatDocsUpdatedDate'

definePageMeta({
layout: 'docs',
})
Expand All @@ -20,9 +22,12 @@ const { data: surround } = await useAsyncData(
() => queryCollectionItemSurroundings('docs', docsPath.value),
)

const updated = computed(() => formatDocsUpdatedDate(page.value?.updated))

usePageSeo({
title: () => page.value?.title,
description: () => page.value?.description,
modifiedTime: () => updated.value?.iso,
})

const tocLinks = computed(() => page.value?.body?.toc?.links ?? [])
Expand All @@ -33,7 +38,15 @@ const tocLinks = computed(() => page.value?.body?.toc?.links ?? [])
<UPageHeader
:title="page.title"
:description="page.description"
/>
>
<p
v-if="updated"
class="mt-3 text-sm text-muted"
>
Updated
<time :datetime="updated.iso">{{ updated.label }}</time>
</p>
</UPageHeader>

<UPageBody>
<ContentRenderer
Expand Down
69 changes: 69 additions & 0 deletions site/app/utils/formatDocsUpdatedDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Normalize and format docs frontmatter `updated` (YYYY-MM-DD or Date).
*/

export type DocsUpdatedDate = {
/** Calendar date as YYYY-MM-DD (UTC). */
iso: string
/** Human-readable label, e.g. "March 15, 2026". */
label: string
}

function pad2(n: number): string {
return String(n).padStart(2, '0')
}

/** Return YYYY-MM-DD for a valid calendar date, or undefined. */
export function toDocsUpdatedIso(value: unknown): string | undefined {
if (value === undefined || value === null || value === '') {
return undefined
}

if (value instanceof Date && !Number.isNaN(value.getTime())) {
return `${value.getUTCFullYear()}-${pad2(value.getUTCMonth() + 1)}-${pad2(value.getUTCDate())}`
}

if (typeof value === 'string') {
const trimmed = value.trim()
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed)
if (!match) {
return undefined
}
const year = Number(match[1])
const month = Number(match[2])
const day = Number(match[3])
const probe = new Date(Date.UTC(year, month - 1, day))
if (
probe.getUTCFullYear() !== year
|| probe.getUTCMonth() !== month - 1
|| probe.getUTCDate() !== day
) {
return undefined
}
return trimmed
}

return undefined
}

/**
* Format an `updated` value for display and `<time datetime>`.
* Uses the en-US long date style (month day, year).
*/
export function formatDocsUpdatedDate(value: unknown): DocsUpdatedDate | undefined {
const iso = toDocsUpdatedIso(value)
if (!iso) {
return undefined
}

const [year, month, day] = iso.split('-').map(Number)
const date = new Date(Date.UTC(year!, month! - 1, day!))
const label = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
}).format(date)

return { iso, label }
}
5 changes: 5 additions & 0 deletions site/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export default defineContentConfig({
},
schema: z.object({
description: z.string().optional(),
/**
* Last meaningful content update (calendar day).
* Prefer `YYYY-MM-DD` in frontmatter; YAML dates are also accepted.
*/
updated: z.union([z.string(), z.date()]).optional(),
/** Sidebar badge — string shortcut (`New`, `Updated`, `Preview`) or Nuxt UI BadgeProps. */
badge: z.union([z.string(), z.number(), badgeObjectSchema]).optional(),
}),
Expand Down
1 change: 1 addition & 0 deletions site/content/docs/5.integrations/10.tunit.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: TUnit
description: Use AutoDataSource, AutoArguments, and parameter attributes with TUnit test projects.
updated: 2026-09-08
badge:
label: Preview
color: warning
Expand Down
7 changes: 7 additions & 0 deletions site/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export default defineNuxtConfig({
type: 'image/svg+xml',
href: '/favicon.svg?v=2',
},
{
key: 'docs-atom-feed',
rel: 'alternate',
type: 'application/atom+xml',
title: 'AutoFixture documentation',
href: '/feed.xml',
},
],
},
},
Expand Down
3 changes: 2 additions & 1 deletion site/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ Slugs and version segments are lowercase. Package ids match the catalog (autofix
- https://autofixture.com/api-meta/routes.json — all API HTML routes
- https://autofixture.com/api-meta/{packageId}/{versionSegment}/toc.json — package TOC
- https://autofixture.com/api-meta/{packageId}/{versionSegment}/search.json — searchable API snippets
- https://autofixture.com/sitemap.xml — HTML and markdown URLs
- https://autofixture.com/sitemap.xml — HTML and markdown URLs (`lastmod` when guides set `updated`)
- https://autofixture.com/feed.xml — Atom feed of recently updated guides

## Optional

Expand Down
24 changes: 21 additions & 3 deletions site/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ just site-generate # runs prepare-agent-assets, then nuxt generate
| `/api-markdown/**` | Raw API markdown (generated; not committed) |
| `/llms.txt` | Agent discovery index |
| `/robots.txt` | Crawler rules + sitemap pointer |
| `/sitemap.xml` | Generated URL list (guides, docs-markdown, API) |
| `/sitemap.xml` | Generated URL list (guides, docs-markdown, API); includes `lastmod` when set |
| `/feed.xml` | Atom feed of guides that declare `updated` |

API markdown is generated into `public/api-markdown` (not Nuxt Content). On API pages, the header shows the package and version from the API catalog.

Expand All @@ -37,13 +38,30 @@ API markdown is generated into `public/api-markdown` (not Nuxt Content). On API
`scripts/prepare-agent-assets.mjs` (also `npm run prepare-agent-assets` / `pregenerate`):

1. Mirrors `content/docs/**/*.md` → `public/docs-markdown/` with Nuxt-style paths (numeric prefixes stripped).
2. Writes `public/sitemap.xml` from those guides plus `public/api-meta/routes.json` when present.
2. Writes `public/sitemap.xml` from those guides plus `public/api-meta/routes.json` when present (`lastmod` from frontmatter `updated`).
3. Writes `public/feed.xml` (Atom) for guides that set `updated`.

Committed: `public/llms.txt`, `public/robots.txt`.
Generated (gitignored): `public/docs-markdown/`, `public/sitemap.xml`.
Generated (gitignored): `public/docs-markdown/`, `public/sitemap.xml`, `public/feed.xml`.

Keep `llms.txt` in sync when you add major guide sections.

## Article freshness (`updated`)

Set `updated` in page frontmatter to a calendar day (`YYYY-MM-DD`). The docs page shows an "Updated …" line under the header, Open Graph gets `article:modified_time`, the sitemap gets `lastmod`, and the Atom feed includes the article.

```yaml
---
title: Build DSL
description: Use Build, With, Without, and OmitAutoProperties…
updated: 2026-09-08
---
```

Bump `updated` when you make a meaningful content change. Omit it when the day is unknown — the UI simply hides the line.

Sidebar `badge: Updated` stays optional and explicit; it is not derived from `updated`.

## Sidebar badges

Set an optional `badge` in page frontmatter to show a label next to the article in the docs sidebar (desktop and mobile). Badges are explicit — nothing is inferred from dates or git.
Expand Down
Loading