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
10 changes: 9 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ jobs:
bun-version: latest
- run: bun install
working-directory: docs
- run: bun run build
- name: Build docs (fail on render errors)
working-directory: docs
run: |
# vitepress prints SSR render errors but still exits 0, leaving blank pages
set -o pipefail
bun run build 2>&1 | tee build.log
if grep -qE "(TypeError|ReferenceError|SyntaxError|\[vitepress\].*error)" build.log; then
echo "::error::Docs build logged render errors (see above)."
exit 1
fi
- name: Verify build output
run: |
if [ ! -d "docs/.vitepress/dist" ] || [ -z "$(ls -A docs/.vitepress/dist)" ]; then
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Docs
- Fix blank Template Authoring page (inline `{{ }}` parsed as Vue), derive the
template count from `templates/`, WCAG-compliant brand colors in light mode,
favicon and header logo, and fail the docs CI build on render errors (#138).

### Fixed
- Findings from a Cora scan (#136): `--stdin`/`--json` temp input is now
unique, `O_EXCL`-created and removed afterwards (was a fixed `/tmp` name);
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ templates/
├── twitter-quote/
│ ├── template.svg
│ └── schema.json
└── ... # 18 templates included
└── ... # 150+ templates included
```

## Documentation
Expand Down
31 changes: 29 additions & 2 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitepress'

// Single source of truth: one directory per bundled template.
const templatesDir = fileURLToPath(new URL('../../templates', import.meta.url))
const templateCount = readdirSync(templatesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).length

export default defineConfig({
title: 'Cosy',
description: 'Template-based image generation in Rust',
Expand All @@ -9,15 +15,36 @@ export default defineConfig({
lastUpdated: true,
ignoreDeadLinks: true,

markdown: {
config: (md) => {
// Inline code like `{{ brand.brand_name }}` is minijinja syntax, not Vue.
const inline = md.renderer.rules.code_inline!
md.renderer.rules.code_inline = (tokens, idx, options, env, self) =>
inline(tokens, idx, options, env, self).replace('<code', '<code v-pre')
},
},

transformPageData(pageData) {
const features = pageData.frontmatter.features
if (pageData.relativePath === 'index.md' && Array.isArray(features)) {
for (const f of features) if (/Templates$/.test(f.title)) f.title = `${templateCount} Templates`
}
},

head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo/favicon.svg' }],
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/logo/favicon-32.png' }],
['link', { rel: 'apple-touch-icon', href: '/logo/apple-touch-icon.png' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:title', content: 'Cosy — Template-based Image Generation' }],
['meta', { property: 'og:description', content: 'Generate social media images from JSON templates. CLI + HTTP API. Rust-powered. 148 templates.' }],
['meta', { property: 'og:description', content: `Generate social media images from JSON templates. CLI + HTTP API. Rust-powered. ${templateCount} templates.` }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
],

themeConfig: {
siteTitle: 'Cosy',
logo: '/logo/logo-symbol.svg',
templateCount,
socialLinks: [
{ icon: 'github', link: 'https://github.com/codecoradev/cosy' },
],
Expand Down Expand Up @@ -58,7 +85,7 @@ export default defineConfig({
{
text: 'Overview',
items: [
{ text: 'All Templates (148)', link: '/templates/' },
{ text: `All Templates (${templateCount})`, link: '/templates/' },
],
},
{
Expand Down
20 changes: 20 additions & 0 deletions docs/.vitepress/theme/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@
--vp-c-brand-2: #b4befe;
--vp-c-brand-3: #89b4fa;
}

/* Light mode: pastel brand colors fail WCAG AA against white (2.0:1).
Use darker violet tones for text/buttons; keep pastels for dark mode. */
:root:not(.dark) {
--vp-c-brand-1: #6d28d9;
--vp-c-brand-2: #5b21b6;
--vp-c-brand-3: #7c3aed;
--vp-c-brand-soft: rgba(124, 58, 237, 0.14);
--vp-button-brand-bg: #6d28d9;
--vp-button-brand-hover-bg: #5b21b6;
--vp-button-brand-text: #ffffff;
}
.dark {
--vp-button-brand-text: #1e1e2e;
}

/* Scrollable tables/code on small screens keep a visible edge cue */
@media (max-width: 640px) {
.vp-doc table { display: block; max-width: 100%; overflow-x: auto; }
}
2 changes: 1 addition & 1 deletion docs/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@ curl -X POST http://localhost:3000/api/render \
## Next Steps

- [CLI Reference](./cli) — All commands and flags
- [Templates](/templates/) — Browse all 18 templates with examples
- [Templates](/templates/) — Browse all 150+ templates with examples
- [Template Authoring](./template-authoring) — Create your own templates
- [HTTP Server](./server) — Full API server guide
34 changes: 17 additions & 17 deletions docs/guide/template-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ The SVG file uses [minijinja](https://docs.rs/minijinja) templating syntax for d

### Accessing Data

```svg
```xml
<!-- Brand fields -->
{{ brand.brand_name }}
{{ brand.bg_color }}
Expand All @@ -156,13 +156,13 @@ The SVG file uses [minijinja](https://docs.rs/minijinja) templating syntax for d

Use the `default()` filter to provide fallback values:

```svg
```xml
<stop offset="0%" stop-color="{{ brand.bg_color | default('#1e1e2e') }}"/>
```

### Conditionals

```svg
```xml
{% if slide.source %}
<text>{{ slide.source }}</text>
{% endif %}
Expand All @@ -172,7 +172,7 @@ Use the `default()` filter to provide fallback values:

When a text field has `wrap_width` set, Cosy pre-computes wrapped lines and provides them as `{field_name}_lines`:

```svg
```xml
{% for line in stat_label_lines %}
<tspan x="540" dy="{{ loop.cycle(0, 48) }}">{{ line }}</tspan>
{% endfor %}
Expand All @@ -182,7 +182,7 @@ When a text field has `wrap_width` set, Cosy pre-computes wrapped lines and prov

Most templates use a two-stop linear gradient for the background:

```svg
```xml
<defs>
<linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="{{ brand.bg_color | default('#1e1e2e') }}"/>
Expand All @@ -194,7 +194,7 @@ Most templates use a two-stop linear gradient for the background:

### Accent Gradient (for numbers, headings)

```svg
```xml
<defs>
<linearGradient id="acc-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="{{ brand.accent_color | default('#cba6f7') }}"/>
Expand All @@ -205,7 +205,7 @@ Most templates use a two-stop linear gradient for the background:

### Radial Glow Effect

```svg
```xml
<defs>
<radialGradient id="glow" cx="75%" cy="25%" r="55%">
<stop offset="0%" stop-color="{{ brand.accent_color | default('#cba6f7') }}" stop-opacity="0.07"/>
Expand All @@ -217,7 +217,7 @@ Most templates use a two-stop linear gradient for the background:

### Dots Pattern (subtle texture)

```svg
```xml
<defs>
<pattern id="dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="1.5" fill="#ffffff" opacity="0.06"/>
Expand All @@ -228,7 +228,7 @@ Most templates use a two-stop linear gradient for the background:

### Decorative Circle (top-right accent)

```svg
```xml
<circle cx="1000" cy="80" r="180" fill="{{ brand.accent_color | default('#cba6f7') }}" opacity="0.04"/>
```

Expand Down Expand Up @@ -263,7 +263,7 @@ A background texture or photo rendered behind the gradient overlay.

**SVG pattern:**

```svg
```xml
{% if bg_image_data_uri %}
<image href="{{ bg_image_data_uri }}" width="{{ width }}" height="{{ height }}"
preserveAspectRatio="xMidYMid slice"
Expand Down Expand Up @@ -300,7 +300,7 @@ A background texture or photo rendered behind the gradient overlay.

Brand logo rendered at the bottom-right corner.

```svg
```xml
{% if logo_data_uri %}
<image href="{{ logo_data_uri }}" x="{{ width - 128 }}" y="{{ height - 128 }}"
height="48" preserveAspectRatio="xMidYMid meet"/>
Expand All @@ -323,7 +323,7 @@ Cosy bundles these fonts (embedded at compile time — no system fonts required)

**SVG usage:**

```svg
```xml
font-family="Inter,sans-serif"
font-family="Space Grotesk,sans-serif"
font-family="JetBrains Mono,monospace"
Expand Down Expand Up @@ -366,7 +366,7 @@ mkdir templates/my-template

### 3. Write template.svg

```svg
```xml
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
Expand Down Expand Up @@ -496,7 +496,7 @@ curl -o output.png -X POST http://localhost:3000/api/render \

**Avoid arithmetic in `{% set %}` within loops** — it doesn't work reliably. Instead, use `loop.index`, `loop.first`, `loop.cycle()`:

```svg
```xml
{# Good: use loop helpers #}
{% for line in title_lines %}
<tspan x="540" dy="{{ loop.cycle(0, 48) }}">{{ line }}</tspan>
Expand Down Expand Up @@ -535,7 +535,7 @@ The default Cosy color scheme:

All templates must render the brand watermark with an identical spec:

```svg
```xml
{% if brand.show_brand %}
<text x="{width/2}" y="{height-30}" text-anchor="middle" font-family="Inter,sans-serif" font-size="20" font-weight="600" fill="#7f849c">{{ brand.brand_name | default('') }}</text>
{% endif %}
Expand Down Expand Up @@ -575,7 +575,7 @@ Black, so `font-style="italic"` and `font-weight="800"` resolve to real faces.

## Hashtag Block Pattern

```svg
```xml
{% if slide.hashtag %}
<text x="{width/2}" y="{height-90}" text-anchor="middle" font-family="Inter,sans-serif"
font-size="26" font-weight="700" fill="{{ brand.accent_color | default('#cba6f7') }}"
Expand All @@ -593,7 +593,7 @@ elements (`#f-title path`, `#f-stat_number`, …) — e.g. per-element
animation in downstream pipelines. Grouping does not change painting,
so PNG/WebP output is unaffected.

```svg
```xml
<g id="bg">
<rect width="{width}" height="{height}" fill="url(#bg-grad)"/>
</g>
Expand Down
7 changes: 6 additions & 1 deletion docs/templates/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
aside: false
---

<script setup>
import { useData } from 'vitepress'
const { theme } = useData()
</script>

# Templates

Cosy ships with **148 templates** out of the box. Browse the gallery below, or use the sidebar to filter by category.
Cosy ships with **{{ theme.templateCount }} templates** out of the box. Browse the gallery below, or use the sidebar to filter by category.

<style>
.template-masonry {
Expand Down
Loading