Getting Started with ScribeEngine
This guide will walk you through creating your first custom route and understanding how ScribeEngine works.
Your Project Structure
After running scribe new myapp, you have:
myapp/
├── app.stpl # Your routes (this is where you'll spend most time)
├── base.stpl # HTML layout template
├── scribe.json # Configuration (database, secret key)
├── lib/ # Helper functions (auto-loaded)
│ └── auth_helpers.py # Password hashing functions
├── migrations/ # Database schema changes
│ └── 001_users.sql # Initial user table
├── static/ # CSS, JavaScript, images
│ ├── css/
│ │ └── style.css # Your styles
│ └── js/
├── docs/ # Documentation (you are here!)
└── app.db # SQLite database (created automatically)
Running Your App
cd myapp
scribe dev
Open http://localhost:5000 in your browser. You should see the welcome page!
Understanding Routes
Routes are defined in app.stpl using the @route() decorator:
@route('/hello')
{$
message = "Hello, World!"
$}
<h1>{{ message }}</h1>
What's happening:
@route('/hello')- Defines URL path{$ ... $}- Python code block (executed server-side)message = "Hello, World!"- Creates a variable{{ message }}- Jinja2 template variable (renders in HTML)
Your First Custom Route
Let's add a simple blog post route. Open app.stpl and add at the end:
@route('/blog')
{$
page_title = "Blog"
posts = [
{"title": "First Post", "date": "2025-01-01"},
{"title": "Second Post", "date": "2025-01-15"},
]
$}
<div class="container">
<h1>Blog Posts</h1>
{% for post in posts %}
<div class="card">
<h2>{{ post.title }}</h2>
<p class="text-muted">Posted on {{ post.date }}</p>
</div>
{% endfor %}
</div>
Save the file and the server will automatically reload. Visit http://localhost:5000/blog
Working with Route Parameters
You can capture parts of the URL:
@route('/blog/<int:post_id>')
{$
page_title = f"Post #{post_id}"
# post_id is automatically available as an integer
posts = {
1: {"title": "First Post", "content": "Hello world!"},
2: {"title": "Second Post", "content": "Another post"},
}
post = posts.get(post_id, None)
if not post:
return abort(404)
$}
<div class="container">
<div class="card">
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</div>
</div>
Parameter types:
<int:id>- Integer<string:name>- String (default)<path:filepath>- Path (with slashes)
Using the Database
Your app comes with a SQLite database. Let's query it:
@route('/users')
{$
page_title = "Users"
# Query the database
users = db['default'].query("SELECT * FROM users")
$}
<div class="container">
<h1>Users</h1>
{% if users %}
{% for user in users %}
<div class="card">
<h3>{{ user.username }}</h3>
<p>Joined: {{ user.created_at }}</p>
</div>
{% endfor %}
{% else %}
<p>No users found.</p>
{% endif %}
</div>
Important: Always use parameterized queries to prevent SQL injection:
# ✅ GOOD (parameterized)
users = db['default'].query(
"SELECT * FROM users WHERE username = ?",
(username,)
)
# ❌ BAD (vulnerable to SQL injection)
users = db['default'].query(
f"SELECT * FROM users WHERE username = {username}"
)
Forms and POST Requests
Handle form submissions:
@route('/contact', methods=['GET', 'POST'])
{$
page_title = "Contact"
success = False
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Process the form (e.g., save to database)
# ... your logic here ...
success = True
$}
<div class="container-narrow">
<div class="card">
<h1>Contact Us</h1>
{% if success %}
<div class="alert alert-success">Message sent!</div>
{% endif %}
<form method="POST">
{{ csrf() }} <!-- CSRF protection -->
<div class="form-group">
<label>Name</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label>Message</label>
<textarea name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
</div>
</div>
Key points:
methods=['GET', 'POST']- Accept both methods{{ csrf() }}- Required for security (prevents CSRF attacks)request.method- Check which HTTP method was usedrequest.form.get('name')- Access form data