Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create, structure and package themes for Cecil (visual themes and component themes), following the conventions of the official Cecilapp/theme-* repositories.
SkillScore100/100
Test results 100
3/3 safety checks passed
27%100%of test cases passed +73 pts
Passed 22 of 22 test cases with the skill, and 6 of 22 without it.
16 fixed6 passed either way0 still failing0 broke
One test taskAccording to the coding conventions for Cecil themes, should Twig template files terminate with a trailing newline at the end of the file?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add cecilapp-cecil-theme
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install cecilapp-cecil-themeThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/cecilapp-cecil-theme/SKILL.md https://app.decimal.ai/s/cecilapp-cecil-theme/SKILL.md.claude/skills/cecilapp-cecil-theme/SKILL.mdThis gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add cecilapp-cecil-themeimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add cecilapp-cecil-themefrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add cecilapp-cecil-themeimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add cecilapp-cecil-themeimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add cecilapp-cecil-themefrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add cecilapp-cecil-themefrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 2 other files are not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add cecilapp-cecil-themeimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add cecilapp-cecil-themefrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 2 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add cecilapp-cecil-themefrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 2 other files are not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add cecilapp-cecil-themeimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 2 other files are not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add cecilapp-cecil-themeimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 2 other files are not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add cecilapp-cecil-themefrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("cecilapp-cecil-theme")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/cecilapp-cecil-theme/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/cecilapp-cecil-theme/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 2 other files are not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add cecilapp-cecil-themeNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/cecilapp-cecil-theme/SKILL.md https://app.decimal.ai/s/cecilapp-cecil-theme/SKILL.md.agents/skills/cecilapp-cecil-theme/SKILL.mdThis gets SKILL.md only. The skill's 2 other files are not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 2 other files are not included.
Raw files: /s/cecilapp-cecil-theme/SKILL.md · /s/cecilapp-cecil-theme.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/cecilapp-cecil-theme/SKILL.mdcurl -fsSL https://app.decimal.ai/s/cecilapp-cecil-theme.jsonThis gets SKILL.md only. The skill's 2 other files are not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
You are an expert Cecil theme developer. You create reusable themes for Cecil: full visual themes (layouts, styles, assets) and small component themes (a feature plugged into any site or theme), packaged for Composer like the official Cecilapp themes.
For general Cecil usage (content, configuration, CLI), see the cecil skill.
Use this skill when:
layouts/ into a reusable themeflexsearch, pwa, darkmodetoggle) into a site or a visual theme; ask the user first (see "Ask the user before installing")cecil/theme-<name>)Read the matching reference before working on a topic it covers:
| Read | Before |
|---|---|
| references/component-themes.md | Creating a component theme, or installing, combining or pre-wiring official component themes (FlexSearch, PWA, dark mode toggle, etc.) |
| references/packaging.md | Packaging a theme for Composer: composer.json, .editorconfig, README, .gitattributes, demo site |
A theme is a website without pages, stored in themes/<name>/ and enabled in the site configuration:
theme: hyde
# or, for several themes (the first one overrides the others)
theme:
- pwa
- hyde
Resolution rules (from src/):
| Resource | Lookup order |
|---|---|
| Layouts | layouts/ → themes/<theme>/layouts/ (in theme list order) → Cecil internal resources/layouts/ |
| Assets | assets/ → themes/<theme>/assets/ → static/ → themes/<theme>/static/ (Sass import paths also include the theme's dirs) |
| Static files | Themes' static/ are copied first (reverse order), then site static/, so the site wins |
| Translations | Cecil internal → themes/<theme>/translations/ (in theme list order, so the last theme wins between themes) → site translations/ (site wins) |
| Data files | Site data/ and each themes/<theme>/data/ are loaded into site.data (theme data is loaded even if the site has no data/ directory) |
| Configuration | themes/<theme>/config.yml overrides Cecil defaults (including default pages, generators and output formats), site config always wins (first theme wins between themes) |
Consequences:
config.yml (not config.yaml); it's the only one loaded.themes/<name>/layouts/ or themes/<name>/config.yml exists; otherwise the build fails with Theme "<name>" not found. Did you forgot to install it?. A theme without layouts/ (config, data or assets only) is supported._default/home, _default/term, _default/vocabulary or _default/404 is picked before a theme's _default/list or _default/page: ship these layouts in the theme to control them. The built-ins extend ['page.html.twig', '_default/page.html.twig'] and fill the content block, so the theme base layout must define a content block.static.exclude, output.pagetypeformats.<type>) replaces Cecil's default list; only a site list is combined with the theme list.theme: [a]) and theme a declares theme: [b], b is added and its layouts, assets, static files and translations are used, but its config.yml is not loaded; with the string form (theme: a), b is ignored. Document the full list the user must declare (e.g. theme: [links, fontawesome]).themes/<name>/extensions/ (autoloaded, path matching the namespace or extensions.autoload PSR-4 mappings; use the theme name as namespace, not the reserved Cecil\), registered in its config.yml as Twig extensions (layouts.extensions), generators (pages.generators), post-processors (output.postprocessors) or build events subscribers (extensions.subscribers); prefer a single plugin (extensions.plugins, a class extending Cecil\Plugin\AbstractPlugin) registering all of them.theme-<name>/
├─ .editorconfig
├─ .gitattributes # * text=auto
├─ .gitignore # vendor/
├─ composer.json
├─ config.yml # default config + namespaced theme options
├─ theme.yml # optional metadata
├─ LICENSE
├─ README.md
├─ docs/
│ └─ screenshot.png
├─ assets/ # processed by asset(): css/, sass/, js/, images
├─ static/ # copied as-is: favicon.ico, fonts/
├─ data/ # optional data files (site.data.*)
├─ extensions/ # optional PHP classes (Twig extensions, generators)
├─ layouts/
│ ├─ _default/
│ │ ├─ page.html.twig # base layout (defines blocks, including `content`)
│ │ ├─ list.html.twig # sections
│ │ ├─ home.html.twig # otherwise the built-in one is used
│ │ ├─ term.html.twig # otherwise the built-in one is used
│ │ ├─ vocabulary.html.twig
│ │ └─ 404.html.twig
│ ├─ <section>/ # optional per-section layouts (e.g. blog/page.html.twig)
│ ├─ components/ # optional Twig components ({% x:<name> %} or <x-name>)
│ ├─ partials/ # header, footer, nav, pagination, post-item...
│ │ └─ notes/ # optional Markdown notes templates (`:::<type>` → <type>.html.twig)
│ └─ macros/ # optional reusable macros
└─ translations/
└─ messages.fr.yml
Put layouts under _default/: this way a site can still provide its own top-level page.html.twig or list.html.twig, which take priority in the lookup.
A component theme only ships what it needs, for example (structure of theme-flexsearch):
theme-<name>/
├─ .github/workflows/demo.yml # builds demo/ and deploys it to GitHub Pages
├─ .gitattributes # export-ignore for .*, /demo, /docs
├─ composer.json
├─ config.yml # new generated pages, output formats, options
├─ theme.yml
├─ README.md
├─ assets/<name>/ # namespaced assets (e.g. flexsearch/flexsearch.css)
├─ demo/ # demo website using the theme
│ ├─ config.yml
│ ├─ layouts/_default/page.html.twig
│ ├─ pages/
│ └─ link.php # links the repository as demo/themes/<name>
├─ docs/screenshot.png
├─ layouts/
│ ├─ _default/<layout>.<format>.twig
│ ├─ partials/<name>.html.twig # snippet to include in <head> or <body>
│ ├─ partials/<name>/*.html.twig # optional sub-partials (e.g. trigger + dialog)
│ └─ macros/<name>.twig # macros to import
└─ translations/messages.fr.yml
Put a component theme's assets in an assets/<name>/ subfolder so they don't collide with the site's or another theme's files.
Before scaffolding, ask the user which features the theme needs and whether official component themes should provide some of them (search, dark mode, PWA, icons…), as described in "Ask the user before installing". In the same message, confirm the use of the built-in metatags partial (see Step 4).
curl -LO https://cecil.app/cecil.phar
php cecil.phar new:site -n --demo
rm -rf layouts # make sure the theme's layouts are used
mkdir -p themes/<name>
Develop the theme directly in themes/<name>/ and enable it in the site cecil.yml (created by new:site; config.yml is only loaded when there is no cecil.yml):
theme:
- <name>
config.ymlProvide sensible Cecil defaults the theme relies on, plus the theme options under a namespace named after the theme:
# Cecil config required by the theme
taxonomies:
tags: tag
pages:
pagination:
max: 10 # only set what differs from Cecil defaults
# Theme options
<name>:
sidebar:
sticky: true
color: '' # red, orange, green, blue...
reverse: false
Rules:
?? or |default(), knowing that |default() also replaces empty values (false, '', 0, []), not only undefined ones:
?? when the user can disable or empty an option, and the fallback is truthy: site.<name>.breadcrumb ?? true, site.<name>.logo ?? 'logo.svg' (logo: '' hides it), site.<name>.toc.min ?? 2 (0 is kept). With |default(true), breadcrumb: false would be ignored.|default() is fine when the fallback is falsy (site.<name>.reverse|default(false), page.blocks|default([])), or when an empty value must be replaced (|default('Latest posts')).?? in parentheses inside a condition combined with other operators: (site.<name>.toc.enabled ?? true) and (page.toc ?? true).taxonomies if the theme displays tags or categories.static.exclude, pages.ext…): a theme list replaces the default list instead of extending it.title, baseurl, description…) out of the theme config.theme.yml (optional metadata)name: <Name>
github: https://github.com/<vendor>/theme-<name>/
author:
- name: <Author>
url: <https://author.tld>
layouts/_default/page.html.twig defines the HTML skeleton and blocks that other layouts (and users) can override:
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{- include('partials/metatags.html.twig', {page, site}, with_context = false) }}
{%- block head_css %}
{{ html(asset(['css/base.css', 'css/<name>.css']), {title: '<Name>'}, {preload: true}) }}
{%- endblock head_css %}
{{- include('partials/pwa.html.twig', {site}, with_context = false, ignore_missing = true) }}
</head>
<body class="{{ site.<name>.reverse|default(false) ? 'reverse' }}">
{%- block header %}
{{ include('partials/site-header.html.twig') }}
{%- endblock header %}
<main>
{%- block content %}
<article>
<h1>{{ page.title }}</h1>
{%- if page.date %}
<time datetime="{{ page.date|date('c') }}">{{ page.date|format_date('long') }}</time>
{%- endif %}
{{ page.content }}
</article>
{%- endblock content %}
</main>
{%- block footer %}
{{ include('partials/site-footer.html.twig') }}
{%- endblock footer %}
{%- block scripts %}{% endblock scripts %}
</body>
</html>
Guidelines:
partials/metatags.html.twig in the <head> of every HTML base layout, and never hand-write <title>, description, canonical, Open Graph or Twitter tags next to it (they would be duplicated). Since it replaces the theme's own <title>, confirm with the user first, explaining the benefit in one line, e.g.: "I'll use Cecil's built-in metatags partial: it generates the title, description, canonical, favicons, Open Graph/Twitter cards, feeds, hreflang alternates (and optional JSON-LD) from front matter and config, with no code to maintain. OK?" Only skip it if the user explicitly declines; in that case, document in the README which tags the theme outputs.favicon.svg, favicon.png resized, favicon.ico, from assets or static) and outputs JSON-LD as a linked graph (WebSite, WebPage, BreadcrumbList, BlogPosting…) when metatags.data is enabled: don't add favicon links or a separate JSON-LD/breadcrumb schema. Ship assets/favicon.png (or .svg) instead.layouts.autoescape: false): {{ page.content }} needs no |raw, but escape values printed in attributes (alt="{{ page.title|e('html_attr') }}"), and don't rely on autoescaping for user-provided text.partials/paginator.html.twig, partials/navigation.html.twig (expects a menu variable, and optionally page to mark the current entry with aria-current: {menu: site.menus.main, page: page}), partials/breadcrumb.html.twig, partials/languages.html.twig, partials/theme-selector.html.twig._default/page.html.twig, keep the code blocks styled, e.g. <style>{{ include('partials/highlight.css.twig') }}</style> when config.pages.body.highlight is enabled, or ship CSS for both engines' classes (hljs-* for highlightjs, hl-* for tempest, see pages.body.highlight.engine).<a class="heading-anchor"> link added to h2/h3 headings (see pages.body.anchors, e.g. a # on hover) and use the data-lang attribute of <pre> code blocks (e.g. a language label).:::<type> notes render as <aside class="note note-<type>">, and the built-in tabs, code-group, steps, accordion and cards types as <div class="note note-<type>"> (no JavaScript); style these classes, and the items: .note-tab (tabs, code-group), .note-steps-list, .note-accordion-item (<details>), .note-cards-list. A theme can override or add partials/notes/<type>.html.twig, which only receives a note variable (no page or site).ignore_missing = true so the theme works whether they are installed or not.with_context = false) to partials that only need site/page.Extend with a list of templates so a site-level override wins over the theme default:
{# layouts/blog/page.html.twig #}
{% extends ['page.html.twig', '_default/page.html.twig'] %}
{% block content %}
<article class="post">
<h1>{{ page.title }}</h1>
{{ page.content }}
</article>
{{- include('partials/post-meta.html.twig') }}
{%- endblock content %}
List layout idiom (set the pages variable before extends):
{# layouts/_default/list.html.twig #}
{% set pages = site.pages.showable %}
{% if page.pages is defined %}
{% set pages = page.pages %}
{% endif %}
{% if page.paginator.pages is defined %}
{% set pages = page.paginator.pages %}
{% endif %}
{% extends ['page.html.twig', '_default/page.html.twig'] %}
{% block content %}
<ul class="posts">
{%- for post in pages %}
{%- include 'partials/post-item.html.twig' with {post} only %}
{%- endfor %}
</ul>
{{- include('partials/paginator.html.twig') }}
{%- endblock content %}
Section alias (e.g. reuse the blog list for the section fallback):
{% extends 'blog/list.html.twig' %}
To map a whole section to another section's layouts, the site (or the theme config) can use layouts.sections (e.g. layouts: {sections: {news: blog}}).
Remember the lookup rules when naming layouts (see the cecil skill, "Lookup Rules"): <section>/index, <section>/page, <section>/list, section/<section>, parent sections, _default/section, index/home, taxonomy/<plural> (vocabulary), taxonomy/<plural>/<term> and taxonomy/<singular> (terms), term, vocabulary, _default/*, with optional language variants (page.html.fr.twig). For non-HTML outputs, the file name is <layout>.<format>.twig (e.g. list.rss.twig, page.webmanifest.twig).
Non-HTML built-ins a theme may override: feeds (_default/list.atom.twig, list.rss.twig, list.jsonfeed.twig, all extending extended/feed.twig), their XSL stylesheets (_default/feed.xsl.twig, _default/sitemap.xsl.twig), _default/sitemap.xml.twig, _default/robots.txt.twig, _default/home.llms.twig, the *.markdown.twig and *.json.twig outputs, _default/404.html.twig and _default/redirect.html.twig. Override them only when needed: they receive Cecil fixes and features.
Put processed files in assets/ and reference them with asset():
{# bundle several CSS files into one #}
{{ html(asset(['css/poole.css', 'css/syntax.css', 'css/hyde.css'])) }}
{# compile Sass and inline it #}
<style>{{ asset('sass/<name>.scss')|inline }}</style>
{# remote asset, downloaded at build time #}
{{ html(asset('https://cdn.jsdelivr.net/npm/lib/dist/lib.min.js', {filename: 'lib.min.js', minify: false}), {defer: ''}) }}
{# responsive image #}
{{ html(asset(post.image), {alt: post.title}, {responsive: true, formats: ['webp']}) }}
{# optional asset #}
{% set logo = asset(site.logo|default('logo.svg'), {ignore_missing: true}) %}
{% if not logo.missing %}{{ html(logo, {alt: site.title}) }}{% endif %}
{# small inline CSS/JS #}
<style>{% apply minify_css %}.hero { color: red; }{% endapply %}</style>
html() function (the |html filter is deprecated).static/ for files that must keep their name and not be processed: web fonts, downloads, etc. Link them with url() (favicons are already linked by metatags). Don't ship a static/robots.txt: robots.txt is a built-in default page (custom rules go in a pages/robots.md page of the site).assets.compile.variables in the theme config.yml (e.g. assets: {compile: {variables: {primary: "#0d6efd"}}}); Sass import paths (assets.compile.import, [sass, scss, node_modules] by default) are resolved in the assets/ and static/ directories of the site and of the themes, and next to the compiled file.assets.minify, assets.fingerprint) rather than forcing them in templates.assets/ are not copied to the output (only static/ is), and *.scss is already excluded by default: no static.exclude is needed.assets/styles.css) and provide build scripts (see "Packaging").<nav>
<ul>
{%- for entry in site.menus.main|sort_by_weight %}
<li{% if url(entry.url) == url(page) %} class="active"{% endif %}><a href="{{ url(entry.url) }}">{{ entry.name }}</a></li>
{%- endfor %}
</ul>
</nav>
Always build links with url() (pages, menu entries, paths, assets), never hardcode site.baseurl.
Use the external test to detect links outside the site (e.g. {% if entry is external %} rel="noopener" target="_blank"{% endif %}), rather than matching http.
Link to the Cecil website with the cecil global variable, never a hardcoded https://cecil.app: cecil.url is already absolute (it includes the version as anchor), so don't pass it to url(). cecil.poweredby (Cecil v<version>) and cecil.version are available for the label:
{% trans with {'%cecil%': '<a href="' ~ cecil.url ~ '">Cecil</a>'} %}Powered by %cecil%{% endtrans %}
<a href="{{ cecil.url }}">{{ cecil.poweredby }}</a>
Filter collections: site.pages.showable|filter_by('section', 'blog')|sort_by_date|slice(0, 5).
Tags links: url('tags/' ~ tag|slugify) or iterate site.taxonomies.tags.
Get a specific page: site.page('about').
Wrap every UI string so the theme is translatable:
{% trans %}Read more{% endtrans %}
{% trans with {'%title%': site.title} %}%title% on GitHub{% endtrans %}
{{ 'You are offline'|trans }}
Provide at least one translation file, keyed by the source string:
# translations/messages.fr.yml
"Read more": "Lire la suite"
"%title% on GitHub": "%title% sur GitHub"
messages.<locale>.<ext> (yml/yaml, or compiled mo; .po files are not loaded), where <locale> is the language locale (fr_FR) or its first two letters (fr).php cecil.phar util:translations:extract --locale=fr --save --theme=<name>: the file is written to the site translations/ directory (messages.fr.yaml), move it into themes/<name>/translations/.No automatic injection mechanism exists: a component theme plugs into a site through generated pages and output formats, partials to include, macros, section-specific layouts, front matter blocks or Twig components. Official component themes exist (FlexSearch, PWA, dark mode toggle, etc.): ask the user before installing one. Read references/component-themes.md for the patterns and the official component themes.
A theme is published as a Composer package (composer.json with type cecil-theme). Read references/packaging.md for the files to provide.
config.yml (not .yaml) with theme options under a <name>: namespacelayouts/_default/ (including home, term, vocabulary and 404 to override the built-ins), children extend ['page.html.twig', '_default/page.html.twig'], base layout defines a content blockpartials/metatags.html.twig included in every HTML base layout <head>, with no duplicated <title>/meta tags (unless the user declined it)paginator, navigation, breadcrumb…)url(), all processed files with asset(){{ cecil.url }} (not a hardcoded URL, not wrapped in url())?? or |default(); ?? for options that can be set to false, '' or 0 with a truthy fallback (never |default(true))trans, at least one translations/messages.<locale>.ymlcomposer.json with "type": "cecil-theme", extra.cecil.theme-name and "cecil/theme-installer": "^2.1"assets/<name>/, CSS classes and custom properties prefixed with <name>, technical generated pages with excluded: true and serviceworker.precache: falsedemo/ + link.php) with php cecil.phar build -v and php cecil.phar serve, without a site layouts/ directoryTheme "<name>" not found: the folder must be themes/<name>/ (matching the theme name resolved by the installer: extra.cecil.theme-name, extra.name or the package name) and contain layouts/ or config.yml.vendor/ instead of themes/: the project doesn't allow the plugin; add "cecil/theme-installer": true to config.allow-plugins in the project's composer.json, then reinstall.The ".../themes/<name>/layouts" directory does not exist: the theme has a config.yml but no layouts/ directory; add one, even empty.layouts/ file with the same path, or any file (site, theme or built-in) matching a higher-priority lookup name, wins (e.g. the built-in _default/home.html.twig before the theme _default/list.html.twig); check with php cecil.phar build -vv, or remove the site layout.site.data.*): the site has no data/ directory; create one.output.pagetypeformats.homepage) is added to the theme list, so the values required by the theme are kept; the site can only remove one with a map of booleans (e.g. homepage: {atom: false}).theme key; a dependency declared in a theme's config.yml never has its own config.yml loaded.asset() are relative to assets/ (or static/) of the site or theme, without the directory prefix.messages.<locale> or messages.<first two letters of the locale> (e.g. fr_FR → messages.fr_FR.yml or messages.fr.yml); the translations cache is refreshed when a file is updated or removed, but not when a new one is added: run php cecil.phar cache:clear:translations.Deprecated configuration option(s) in …: the theme's config.yml uses a renamed or removed option (see config/deprecated.php), which makes the build fail for every site using the theme; apply the snippet given in the error message.php cecil.phar doctor to check theme configuration.theme): https://cecil.app/documentation/configuration/site/#themeSkills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.