docs refactor
All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 52s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 1m1s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m50s

This commit is contained in:
2025-10-09 18:21:23 -04:00
parent c9a3a21f07
commit ad81d7f3db
39 changed files with 12551 additions and 1037 deletions

465
CLAUDE.md
View File

@ -84,468 +84,3 @@ The application uses TOML configuration with sections for:
- Hidden file/folder access restrictions
- Configurable admin interface with password protection
- Input validation and sanitization for file paths
# Coding Guidelines
The following is `The Grug Brained Developer`. Use it as a sent of principles in all your development
```
The Eternal Enemy: Complexity
apex predator of grug is complexity
complexity bad
say again:
complexity very bad
you say now:
complexity very, very bad
given choice between complexity or one on one against t-rex, grug take t-rex: at least grug see t-rex
complexity is spirit demon that enter codebase through well-meaning but ultimately very clubbable non grug-brain developers and project managers who not fear complexity spirit demon or even know about sometime
one day code base understandable and grug can get work done, everything good!
next day impossible: complexity demon spirit has entered code and very dangerous situation!
grug no able see complexity demon, but grug sense presence in code base
demon complexity spirit mocking him make change here break unrelated thing there what!?! mock mock mock ha ha so funny grug love programming and not becoming shiney rock speculator like grug senior advise
club not work on demon spirit complexity and bad idea actually hit developer who let spirit in with club: sometimes grug himself!
sadly, often grug himself
so grug say again and say often: complexity very, very bad
Saying No
best weapon against complexity spirit demon is magic word: "no"
"no, grug not build that feature"
"no, grug not build that abstraction"
"no, grug not put water on body every day or drink less black think juice you stop repeat ask now"
note, this good engineering advice but bad career advice: "yes" is magic word for more shiney rock and put in charge of large tribe of developer
sad but true: learn "yes" then learn blame other grugs when fail, ideal career advice
but grug must to grug be true, and "no" is magic grug word. Hard say at first, especially if you nice grug and don't like disappoint people (many such grugs!) but easier over time even though shiney rock pile not as high as might otherwise be
is ok: how many shiney rock grug really need anyway?
Saying ok
sometimes compromise necessary or no shiney rock, mean no dinosaur meat, not good, wife firmly remind grug about young grugs at home need roof, food, and so forth, no interest in complexity demon spirit rant by grug for fiftieth time
in this situation, grug recommend "ok"
"ok, grug build that feature"
then grug spend time think of 80/20 solution to problem and build that instead.
80/20 solution say "80 want with 20 code" solution maybe not have all bell-whistle that project manager want, maybe a little ugly, but work and deliver most value, and keep demon complexity spirit at bay for most part to extent
sometimes probably best just not tell project manager and do it 80/20 way. easier forgive than permission, project managers mind like butterfly at times overworked and dealing with many grugs. often forget what even feature supposed to do or move on or quit or get fired grug see many such cases
anyway is in project managers best interest anyway so grug not to feel too bad for this approach usually
Factoring Your Code
next strategy very harder: break code base up properly (fancy word: "factor your code properly") here is hard give general advice because each system so different. however, one thing grug come to believe: not factor your application too early!
early on in project everything very abstract and like water: very little solid holds for grug's struggling brain to hang on to. take time to develop "shape" of system and learn what even doing. grug try not to factor in early part of project and then, at some point, good cut-points emerge from code base
good cut point has narrow interface with rest of system: small number of functions or abstractions that hide complexity demon internally, like trapped in crystal
grug quite satisfied when complexity demon trapped properly in crystal, is best feeling to trap mortal enemy!
grug try watch patiently as cut points emerge from code and slowly refactor, with code base taking shape over time along with experience. no hard/ fast rule for this: grug know cut point when grug see cut point, just take time to build skill in seeing, patience
sometimes grug go too early and get abstractions wrong, so grug bias towards waiting
big brain developers often not like this at all and invent many abstractions start of project
grug tempted to reach for club and yell "big brain no maintain code! big brain move on next architecture committee leave code for grug deal with!"
but grug learn control passions, major difference between grug and animal
instead grug try to limit damage of big brain developer early in project by giving them thing like UML diagram (not hurt code, probably throw away anyway) or by demanding working demo tomorrow
working demo especially good trick: force big brain make something to actually work to talk about and code to look at that do thing, will help big brain see reality on ground more quickly
remember! big brain have big brain! need only be harness for good and not in service of spirit complexity demon on accident, many times seen
(best grug brain able to herd multiple big brain in right direction and produce many complexity demon trap crystals, large shiney rock pile awaits such grug!)
also sometimes call demo approach "prototype", sound fancier to project manager
grug say prototype early in software making, especially if many big brains
Testing
grug have love/hate relationship with test: test save grug many, many uncountable time and grug love and respect test
unfortunately also many test shamans exist. some test shaman make test idol, demand things like "first test" before grug even write code or have any idea what grug doing domain!
how grug test what grug not even understand domain yet!?
"Oh, don't worry: the tests will show you what you need to do."
grug once again catch grug slowly reaching for club, but grug stay calm
grug instead prefer write most tests after prototype phase, when code has begun firm up
but, note well: grug must here be very disciplined!
easy grug to move on and not write tests because "work on grugs machine"!
this very, very bad: no guarantee work on other machine and no guarantee work on grug machine in future, many times
test shaman have good point on importance of test, even if test shaman often sometimes not complete useful feature in life and talk only about test all time, deserve of club but heart in right place
also, test shaman often talk unit test very much, but grug not find so useful. grug experience that ideal tests are not unit test or either end-to-end test, but in-between test
unit tests fine, ok, but break as implementation change (much compared api!) and make refactor hard and, frankly, many bugs anyway often due interactions other code. often throw away when code change.
grug write unit test mostly at start of project, help get things going but not get too attached or expect value long time
end to end tests good, show whole system work, but! hard to understand when break and drive grug crazy very often, sometimes grugs just end up ignoring because "oh, that break all time" very bad!
in-between tests, grug hear shaman call "integration tests" sometime often with sour look on face. but grug say integration test sweet spot according to grug: high level enough test correctness of system, low level enough, with good debugger, easy to see what break
grug prefer some unit tests especially at start but not 100% all code test and definitely not "first test". "test along the way" work pretty well for grug, especially as grug figure things out
grug focus much ferocious integration test effort as cut point emerge and system stabilize! cut point api hopefully stable compared implementation and integration test remain valuable many long time, and easy debug
also small, well curated end-to-end test suite is created to be kept working religiously on pain of clubbing. focus of important end-to-end test on most common UI features and few most important edge cases, but not too many or become impossible maintain and then ignored
this ideal set of test to grug
you may not like, but this peak grug testing
also, grug dislike mocking in test, prefer only when absolute necessary to (rare/never) and coarse grain mocking (cut points/systems) only at that
one exception "first test" dislike by grug: when bug found. grug always try first reproduce bug with regression test then fix bug, this case only for some reason work better
Agile
grug think agile not terrible, not good
end of day, not worst way to organize development, maybe better than others grug supposes is fine
danger, however, is agile shaman! many, many shiney rock lost to agile shaman!
whenever agile project fail, agile shaman say "you didn't do agile right!" grug note this awfully convenient for agile shaman, ask more shiney rock better agile train young grugs on agile, danger!
grug tempted reach for club when too much agile talk happen but always stay calm
prototyping, tools and hiring good grugs better key to success software: agile process ok and help some but sometimes hurt taken too seriously
grug say no silver club fix all software problems no matter what agile shaman say (danger!)
Refactoring
refactoring fine activity and often good idea, especially later in project when code firmed up
however, grug note that many times in career "refactors" go horribly off rails and end up causing more harm than good
grug not sure exactly why some refactors work well, some fail, but grug notice that larger refactor, more likely failure appear to be
so grug try to keep refactors relatively small and not be "too far out from shore" during refactor. ideally system work entire time and each step of finish before other begin.
end-to-end tests are life saver here, but often very hard understand why broke... such is refactor life.
also grug notice that introducing too much abstraction often lead to refactor failure and system failure. good example was J2EE introduce, many big brain sit around thinking too much abstraction, nothing good came of it many project hurt
another good example when company grug work for introduce OSGi to help manage/trap spriit complexity demon in code base. not only OSGi not help, but make complexity demon much more powerful! took multiple man year of best developers to rework as well to boot! more complex spirit and now features impossible implement! very bad!
Chesterton's Fence
wise grug shaman chesterton once say
here exists in such a case a certain institution or law; let us say, for the sake of simplicity, a fence or gate erected across a road. The more modern type of reformer goes gaily up to it and says, “I dont see the use of this; let us clear it away.” To which the more intelligent type of reformer will do well to answer: “If you dont see the use of it, I certainly wont let you clear it away. Go away and think. Then, when you can come back and tell me that you do see the use of it, I may allow you to destroy it.”
many older grug learn this lesson well not start tearing code out willy nilly, no matter how ugly look
grug understand all programmer platonists at some level wish music of spheres perfection in code. but danger is here, world is ugly and gronky many times and so also must code be
humility not often come big brained or think big brained easily or grug even, but grug often find "oh, grug no like look of this, grug fix" lead many hours pain grug and no better or system worse even
grug early on in career often charge into code base waving club wildly and smash up everything, learn not good
grug not say no improve system ever, quite foolish, but recommend take time understand system first especially bigger system is and is respect code working today even if not perfect
here tests often good hint for why fence not to be smashed!
Microservices
grug wonder why big brain take hardest problem, factoring system correctly, and introduce network call too
seem very confusing to grug
Tools
grug love tool. tool and control passion what separate grug from dinosaurs! tool allow grug brain to create code that not possible otherwise by doing thinking for grug, always good relief! grug always spend time in new place learning tools around him to maximize productivity: learn tools for two weeks make development often twice faster and often have dig around ask other developers help, no docs
code completion in IDE allow grug not have remembered all API, very important!
java programming nearly impossible without it for grug!
really make grug think some time
good debugger worth weight in shiney rocks, in fact also more: when faced with bug grug would often trade all shiney rock and perhaps few children for good debugger and anyway debugger no weigh anything far as grug can tell
grug always recommend new programmer learn available debugger very deeply, features like conditional break points, expression evaluation, stack navigation, etc teach new grug more about computer than university class often!
grug say never be not improving tooling
Type Systems
grug very like type systems make programming easier. for grug, type systems most value when grug hit dot on keyboard and list of things grug can do pop up magic. this 90% of value of type system or more to grug
big brain type system shaman often say type correctness main point type system, but grug note some big brain type system shaman not often ship code. grug suppose code never shipped is correct, in some sense, but not really what grug mean when say correct
grug say tool magic pop up of what can do and complete of code major most benefit of type system, correctness also good but not so nearly so much
also, often sometimes caution beware big brains here!
some type big brain think in type systems and talk in lemmas, potential danger!
danger abstraction too high, big brain type system code become astral projection of platonic generic turing model of computation into code base. grug confused and agree some level very elegant but also very hard do anything like record number of club inventory for Grug Inc. task at hand
generics especially dangerous here, grug try limit generics to container classes for most part where most value add
temptation generics very large is trick! spirit demon complex love this one trick! beware!
always most value type system come: hit dot see what grug can do, never forget!
Expression Complexity
grug once like to minimize lines of code much as possible. write code like this:
if(contact && !contact.isActive() && (contact.inGroup(FAMILY) || contact.inGroup(FRIENDS))) {
// ...
}
over time grug learn this hard debug, learn prefer write like so:
if(contact) {
var contactIsInactive = !contact.isActive();
var contactIsFamilyOrFriends = contact.inGroup(FAMILY) || contact.inGroup(FRIENDS);
if(contactIsInactive && contactIsFamilyOrFriends) {
// ...
}
}
grug hear screams from young grugs at horror of many line of code and pointless variable and grug prepare defend self with club
club fight start with other developers attack and grug yell: "easier debug! see result of each expression more clearly and good name! easier understand conditional expression! EASIER DEBUG!"
definitely easier debug and once club fight end calm down and young grug think a bit, they realize grug right
grug still catch grug writing code like first example and often regret, so grug not judge young grug
DRY
DRY mean Don't Repeat Self, powerful maxim over mind of most developers
grug respect DRY and good advice, however grug recommend balance in all things, as gruggest big brain aristotle recommend
grug note humourous graph by Lea Verou correspond with grug passion not repeat:
code concerns over time
over time past ten years program grug not as concerned repeat code. so long as repeat code simple enough and obvious enough, and grug begin feel repeat/copy paste code with small variation is better than many callback/closures passed arguments or elaborate object model: too hard complex for too little benefit at times
hard balance here, repeat code always still make grug stare and say "mmm" often, but experience show repeat code sometimes often better than complex DRY solution
note well! grug encourage over literal developer not take does work line too serious, is joke
Separation of Concerns (SoC)
Separation of Concern (SoC) another powerful idea over many developer mind, idea to separate different aspects of system into distinct sections code
canonical example from web development: separation of style (css file), markup (html file) and logic (javascript file)
here grug much more sour faced than DRY and in fact write big brained essay on alternative design principle locality of behavior (LoB) against SoC
grug much prefer put code on the thing that do the thing. now when grug look at the thing grug know the thing what the thing do, alwasy good relief!
when separate of concern grug must often all over tarnation many file look understand what how button do, much confuse and time waste: bad!
Closures
grug like closures for right job and that job usually abstracting operation over collection of objects
grug warn closures like salt, type systems and generics: small amount go long way, but easy spoil things too much use give heart attack
javascript developers call very special complexity demon spirit in javascript "callback hell" because too much closure used by javascript libraries very sad but also javascript developer get what deserved let grug be frank
Logging
grug huge fan of logging and encourage lots of it, especially in cloud deployed. some non-grugs say logging expensive and not important. grug used think this way no more
funny story: grug learn idol rob pike working on logging at google and decide: "if rob pike working on logging, what grug do there?!?" so not pursue. turn out logging very important to google so of course best programmer work on it, grug!
don't be such grug brain, grug, much less shiney rock now!
oh well, grug end up at good company anyway and rob pike dress habit increasingly erratic, so all work out in end, but point stand: logging very important!
grug tips on logging are:
log all major logical branches within code (if/for)
if "request" span multiple machine in cloud infrastructure, include request ID in all so logs can be grouped
if possible make log level dynamically controlled, so grug can turn on/off when need debug issue (many!)
if possible make log level per user, so can debug specific user issue
last two points are especially handy club when fighting bugs in production systems very often
unfortunately log libraries often very complex (java, why you do?) but worth investing time in getting logging infrastructure "just right" pay off big later in grug experience
logging need taught more in schools, grug think
Concurrency
grug, like all sane developer, fear concurrency
as much as possible, grug try to rely on simple concurrency models like stateless web request handlers and simple remote job worker queues where jobs no interdepend and simple api
optimistic concurrency seem work well for web stuff
occasionally grug reach for thread local variable, usually when writing framework code
some language have good concurrent data structure, like java ConcurrentHashMap but still need careful grug work to get right
grug has never used erlang, hear good things, but language look wierd to grug sorry
Optimizing
ultra biggest of brain developer once say:
premature optimization is the root of all evil
this everyone mostly know and grug in humble violent agreement with ultra biggest of big brain
grug recommend always to have concrete, real world perf profile showing specific perf issue before begin optimizing.
never know what actual issue might be, grug often surprise! very often!
beware only cpu focus: easy to see cpu and much big o notation thinking having been done in school, but often not root of all slowness, surprise to many including grug
hitting network equivalent of many, many millions cpu cycle and always to be minimized if possible, note well big brain microservice developer!
inexperienced big brain developer see nested loop and often say "O(n^2)? Not on my watch!"
complexity demon spirit smile
APIs
grug love good apis. good apis not make grug think too much
unfortunately, many apis very bad, make grug think quite a bit. this happen many reasons, here two:
API creators think in terms of implementation or domain of API, rather than in terms of use of API
API creators think too abstract and big brained
usually grug not care too deeply about detail of api: want write file or sort list or whatever, just want to call write() or sort() or whatever
but big brain api developers say:
"not so fast, grug! is that file open for write? did you define a Comparator for that sort?"
grug find self restraining hand reaching for club again
not care about that stuff right now, just want sort and write file mr big brain!
grug recognize that big brain api designer have point and that sometime these things matter, but often do not. big brain api developers better if design for simple cases with simple api, make complex cases possible with more complex api
grug call this "layering" apis: two or three different apis at different level complexity for various grug needs
also, if object oriented, put api on thing instead of elsewhere. java worst at this!
grug want filter list in java
"Did you convert it to a stream?"
fine, grug convert to stream
"OK, now you can filter."
OK, but now need return list! have stream!
"Well, did you collect your stream into a list?"
what?
"Define a Collector<? super T, A, R> to collect your stream into a list"
grug now swear on ancestor grave he club every single person in room, but count two instead and remain calm
put common thing like filter() on list and make return list, listen well big brain java api developer!
nobody care about "stream" or even hear of "stream" before, is not networking api, all java grugs use list mr big brain!
Parsing
grug love make programming language at drop of hat and say recursive descent most fun and beautiful way create parser
unfortunately many big brain school teach only parser generator tool. here grug usual love of tool is not: parser generator tool generate code of awful snakes nest: impossible understand, bottom up, what? hide recursive nature of grammar from grug and debug impossible, very bad according grug!
grug think this because while complexity demon bad for code base and understand, complexity demon very good for generation of much academic papers, sad but true
production parser almost always recursive descent, despite ignore by schools! grug furious when learn how simple parse is! parsing not big brain only magic: so can you!
grug very elated find big brain developer Bob Nystrom redeem the big brain tribe and write excellent book on recursive descent: Crafting Interpreters
book available online free, but grug highly recommend all interested grugs purchase book on general principle, provide much big brain advice and grug love book very much except visitor pattern (trap!)
The Visitor Pattern
bad
Front End Development
some non-grugs, when faced with web development say:
"I know, I'll split my front end and back end codebase up and use a hot new SPA library talking to a GraphQL JSON API back end over HTTP (which is funny because I'm not transferring hypertext)"
now you have two complexity demon spirit lairs
and, what is worse, front end complexity demon spirit even more powerful and have deep spiritual hold on entire front end industry as far as grug can tell
back end developers try keep things simple and can work ok, but front end developers make very complex very quickly and introduce lots of code, demon complex spirit
even when website just need put form into database or simple brochure site!
everyone do this now!
grug not sure why except maybe facebook and google say so, but that not seem very good reason to grug
grug not like big complex front end libraries everyone use
grug make htmx and hyperscript to avoid
keep complexity low, simple HTML, avoid lots javascript, the natural ether of spirit complexity demon
maybe they work for you, but no job post, sorry
react better for job and also some type application, but also you become alcolyte of complexity demon whether you like or no, sorry such is front end life
Fads
grug note lots of fads in development, especially front end development today
back end better more boring because all bad ideas have tried at this point maybe (still retry some!)
still trying all bad ideas in front end development so still much change and hard to know
grug recommend taking all revolutionary new approach with grain salt: big brains have working for long time on computers now, most ideas have tried at least once
grug not saying can't learn new tricks or no good new ideas, but also much of time wasted on recycled bad ideas, lots of spirit complexity demon power come from putting new idea willy nilly into code base
Fear Of Looking Dumb
note! very good if senior grug willing to say publicly: "hmmm, this too complex for grug"!
many developers Fear Of Looking Dumb (FOLD), grug also at one time FOLD, but grug learn get over: very important senior grug say "this too complicated and confuse to me"
this make it ok for junior grugs to admit too complex and not understand as well, often such case! FOLD major source of complexity demon power over developer, especially young grugs!
take FOLD power away, very good of senior grug!
note: important to make thinking face and look big brained when saying though. be prepare for big brain or, worse and much more common, thinks is big brain to make snide remark of grug
be strong! no FOLD!
club sometimes useful here, but more often sense of humor and especially last failed project by big brain very useful, so collect and be calm
Impostor Syndrome
grug note many such impostor feels in development
always grug one of two states: grug is ruler of all survey, wield code club like thor OR grug have no idea what doing
grug is mostly latter state most times, hide it pretty well though
now, grug make softwares of much work and moderate open source success , and yet grug himself often feel not any idea what doing! very often! grug still fear make mistake break everyone code and disappoint other grugs, imposter!
is maybe nature of programming for most grug to feel impostor and be ok with is best: nobody imposter if everybody imposter
any young grug read this far probably do fine in program career even if frustrations and worry is always to be there, sorry
```

View File

@ -5,8 +5,8 @@ styles_dir = "/home/dubey/projects/foldsite/docs/styles"
[server]
listen_address = "0.0.0.0"
listen_port = 8080
enable_admin_browser = false
listen_port = 8081
admin_browser = true
admin_password = "password"
max_threads = 4
debug = false

View File

@ -1,35 +0,0 @@
# Configuration
## Configuration file
Foldsite is configured using a TOML file (default: `config.toml`). This file specifies paths to content, templates, and styles, as well as server settings.
Example `config.toml`:
```toml
[paths]
content_dir = "/home/myuser/site/content"
templates_dir = "/home/myuser/site/themes/cobalt/templates"
styles_dir = "/home/myuser/site/themes/cobalt/styles"
[server]
listen_address = "127.0.0.1"
listen_port = 8080
debug = false
access_log = true
max_threads = 4
admin_browser = false
admin_password = "your_admin_password"
```
## Server Settings
- **`listen_address`**: The IP address the server listens on (default: `127.0.0.1`).
- **`listen_port`**: The port the server listens on (default: `8080`).
- **`debug`**: Enables or disables debug mode (default: `false`). In debug mode, the server will automatically reload when code changes are detected, and more detailed error messages will be shown.
- **`access_log`**: Enables or disables access logging (default: `true`). If enabled, access logs will be written to standard output.
- **`max_threads`**: The maximum number of threads to use for handling requests (default: `4`). This setting directly impacts the concurrency of the server.
- **`admin_browser`**: Enables or disables the built-in file manager (default: `false`).
- **`admin_password`**: Sets the password for the file manager. Required if `admin_browser` is `true`.
The `Configuration` class (`/foldsite/src/config/config.py`) is responsible for loading and parsing this configuration file. It also sets global variables (`CONTENT_DIR`, `TEMPLATES_DIR`, `STYLES_DIR`) for easy access to these directories throughout the application. Errors are raised if the config file is missing, invalid, or lacks required sections (like `paths` or `server`).

View File

@ -1,56 +0,0 @@
## Rendering Process
The `RouteManager` class (`/foldsite/src/routes/routes.py`) and `render_page` function (`/foldsite/src/rendering/renderer.py`) are central to the rendering process.
### How Foldsite Determines File Types
The `determine_type` function (in `renderer.py`) is crucial for figuring out how to handle a given file or directory. It examines file extensions and directory contents to classify files into broad categories (defined in `GENERIC_FILE_MAPPING` in `/foldsite/src/rendering/__init__.py`):
* **`document`**: Files with extensions like `.md`, `.txt`, and `.html`.
* **`image`**: Files with extensions like `.png`, `.jpg`, `.jpeg`, `.gif`, and `.svg`.
* **`directory`**: Directories. If a directory contains files, the most common file extension within that directory is used to infer the directory's "type".
* **`other`**: Files that don't match any of the above categories.
* **`multimedia`**: This is a combination that contains `image`.
### Template Search
When a request comes in, Foldsite searches for an appropriate template in the `templates` directory. The search logic is implemented in `render_page` and follows a specific order, prioritizing more specific templates:
1. **Exact Path Match:** If a template exists with the exact same path relative to the `templates` directory as the requested content file (but with a `.html` extension), it's used. For example, if the request is for `/about/team.md`, and a template exists at `templates/about/team.md.html`, that template will be used.
2. **Folder-Specific Template:** If the requested path is a directory, Foldsite looks for a `__folder.html` template within that directory. For example, if the request is for `/blog/`, and `templates/blog/__folder.html` exists, it will be used.
3. **Type and Extension-Specific Templates:** Foldsite searches for templates named `__{type}.{extension}.html` within the requested directory and its parent directories, moving upwards. For instance, if requesting `/blog/post1.md`, it would look for:
* `templates/blog/__file.md.html`
* `templates/__file.md.html`
4. **Type and Category-Specific Templates:** Similar to the above, but searches for `__{type}.{category}.html`. If requesting an image at `/images/logo.png`, it looks for:
* `templates/images/__file.image.html`
* `templates/__file.image.html`
5. **Base Template:** Finally, if no other template is found, `templates/base.html` is used as a fallback. This template *must* exist; otherwise, an exception is raised.
### Style Search
CSS styles are searched similarly to templates, prioritizing specificity:
1. **Exact Path Match:** A CSS file with the exact same path as the requested content file (relative to the `styles` directory) is used. For example, `/about/team.md` would look for `styles/about/team.md.css`.
2. **Type and Extension-Specific Styles:** Searches for `__{type}.{extension}.css` in the requested directory and its parent directories. For example, `/blog/post1.md` would look for:
* `styles/blog/__file.md.css`
* `styles/__file.md.css`
3. **Type and Category-Specific Styles:** Similar to the above, but searches for `__{type}.{category}.css`.
* `styles/images/__file.image.css`
* `styles/__file.image.css`
4. **Base Style:** `styles/base.css` is always included.
The discovered styles are added to the `styles` variable, which is passed to the Jinja2 template. The order ensures that more specific styles override general ones.
### Error Handling
The `render_error_page` function (in `renderer.py`) handles errors. If a requested resource is not found (404 error) or if an exception occurs during processing, this function is called. It looks for a template named `__error.html` in the `templates` directory. If found, it's used to render the error page; otherwise, a default error page is generated. The error code, message, and description are passed to the template.

219
docs/content/about.md Normal file
View File

@ -0,0 +1,219 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "About Foldsite"
description: "The philosophy and story behind Foldsite"
summary: "Learn why Foldsite was created and how it empowers you to reclaim your corner of the internet with simple, file-based content management."
quick_tips:
- "Foldsite is part of the DWS mission to help people own their web presence"
- "Your content stays as simple files and folders - no database lock-in"
- "Built for people who want to focus on content, not configuration"
---
# About Foldsite
## The Vision
**Foldsite** exists to make creating and hosting your own website as simple as organizing files on your computer. It's part of the broader [DWS (Dubey Web Services)](https://dws.rip) mission: **"It's your Internet. Take it back."**
In an era where social media platforms control your content and complex CMSs require constant maintenance, Foldsite offers a refreshing alternative: your website is just folders and files on your filesystem.
## Why Foldsite Was Created
### The Problem
Modern web development has become unnecessarily complex:
- **Content Management Systems** require databases, constant updates, and security patches
- **Static Site Generators** force you to learn specific frameworks and build processes
- **Social Media Platforms** own your content and can remove it at any time
- **Blog Platforms** lock you into their ecosystem with proprietary formats
### The Solution
Foldsite strips away the complexity:
1. **Your content is just files** - Markdown, images, PDFs - organize them however makes sense
2. **Your structure is your site** - Folders become URLs automatically
3. **Templates are optional** - Start with defaults, customize when you need
4. **No build step required** - Dynamic server or export to static files
## Core Principles
### 1. Content Ownership
Your content lives in simple text files using standard markdown format. No proprietary databases, no vendor lock-in. Take your files anywhere - they'll work with any markdown tool.
### 2. Convention Over Configuration
Foldsite uses sensible defaults based on file types and folder structure:
- Markdown files become pages
- Image folders become galleries
- Folder names become navigation
- No routing configuration needed
### 3. Progressive Enhancement
Start with the simplest possible setup:
```
content/
└── index.md
```
Add complexity only when you need it:
```
content/
├── index.md
├── blog/
│ └── post.md
templates/
├── base.html
└── __file.md.html
styles/
└── base.css
```
### 4. Developer Friendly
When things don't work as expected:
- Clear error messages explain what went wrong
- Debug mode shows template discovery process
- File-based structure makes troubleshooting intuitive
## Who Foldsite Is For
### Content Creators
Writers, photographers, artists who want to share their work without fighting with technology. Focus on creating, not configuring.
### Personal Website Owners
People who want a simple blog, portfolio, or personal site without the overhead of WordPress or the limitations of site builders.
### Documentation Writers
Technical writers and project maintainers who need clean, navigable documentation that mirrors their mental model.
### Privacy-Conscious Individuals
Anyone who wants to control their web presence without relying on platforms that monetize user data.
### Hobbyist Developers
Developers who appreciate simple, understandable tools and want to hack on their personal sites without complex build pipelines.
## What Foldsite Is NOT
To set clear expectations:
- **Not a CMS replacement** - No admin UI for non-technical users (though an optional file manager exists)
- **Not optimized for huge sites** - Works best with hundreds to thousands of pages, not millions
- **Not a full application framework** - It renders content, doesn't handle complex application logic
- **Not trying to replace everything** - It's a focused tool for a specific use case
## The Technology
Foldsite is built with:
- **Python & Flask** - Proven, stable web framework
- **Jinja2** - Powerful templating with familiar syntax
- **Markdown** - Universal content format
- **No JavaScript required** - Works with JS disabled (progressive enhancement)
It can run as:
- **Dynamic server** - Live rendering with Python
- **Static site** - Export to HTML files
- **Docker container** - Easy deployment anywhere
## The DWS Philosophy
DWS (Dubey Web Services) believes the internet should be:
### Decentralized
Not controlled by a handful of mega-platforms. Everyone should be able to host their own corner of the web.
### Simple
Technology should serve users, not the other way around. Complexity is often unnecessary.
### Yours
You should own your content, your presentation, and your digital presence.
### Open
Open source tools, open standards, open community. No lock-in, no secrets.
## Use Cases
### Personal Blog
Write in markdown, organize by topic or date, let Foldsite handle the rest. Built-in helpers for recent posts, tags, and related content.
### Photography Portfolio
Upload images to folders, automatic EXIF extraction, thumbnail generation, and gallery views. Organize by project, date, or theme.
### Documentation Site
Mirror your project structure in folders, automatic navigation and breadcrumbs, searchable content hierarchy.
### Digital Garden
Non-linear, interconnected notes and thoughts. Flexible organization, easy linking between pages.
### Project Showcase
Portfolio of work with custom templates per project. Mix markdown descriptions with galleries and downloads.
## The Name
**Foldsite** = **Fold**er + Web**site**
Your folders become your site. Simple as that.
## Community & Contribution
Foldsite is open source and welcomes contributions:
- **Use it** - Build your site and share what you create
- **Improve it** - Submit bug fixes and features
- **Extend it** - Create themes and templates
- **Share it** - Tell others about the project
See the [Develop Section](develop/) for contribution guidelines.
## Getting Help
- **Documentation** - You're reading it! Check other sections for specific topics
- **Examples** - See [Explore Foldsites](explore.md) for real-world sites
- **Support** - Visit [Support](support.md) for help channels
- **Source Code** - [GitHub Repository](https://github.com/DWSresearch/foldsite)
## Future Vision
Foldsite aims to remain focused and simple while adding:
- More template helpers for common use cases
- Better performance optimization
- Enhanced developer tooling
- Growing library of themes and recipes
- Strong community of Foldsite users
The goal is never to become a monolithic CMS, but to remain a focused tool that does one thing well: turns folders into websites.
## Start Building
Ready to create your own site?
- [Quick Start Guide](index.md#quick-start)
- [Directory Structure](directory-structure.md)
- [Deployment Options](deployment/)
- [Template Recipes](recipes/)
**It's your internet. Take it back.**

View File

@ -0,0 +1,735 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Docker Deployment"
description: "Running Foldsite in Docker containers"
summary: "Complete guide to deploying Foldsite with Docker for consistent, isolated, and portable deployments."
quick_tips:
- "Docker ensures consistent environments across development and production"
- "Use docker-compose for easy multi-container setup"
- "Mount content as volumes for live updates without rebuilding"
---
# Docker Deployment
Docker provides isolated, reproducible deployments of Foldsite. Perfect for testing, staging, and production environments.
## Why Docker?
**Benefits:**
- **Consistency** - Same environment everywhere
- **Isolation** - Dependencies don't conflict with system
- **Portability** - Run anywhere Docker runs
- **Easy deployment** - Single command to start
- **Version control** - Docker images are versioned
**Use cases:**
- Team development (everyone has same environment)
- Staging environments before production
- Production deployments
- CI/CD pipelines
- Cloud platform deployments
## Prerequisites
### Install Docker
**macOS:**
```bash
# Download Docker Desktop from docker.com
# Or use Homebrew
brew install --cask docker
```
**Linux (Ubuntu/Debian):**
```bash
# Update package index
sudo apt update
# Install Docker
sudo apt install docker.io docker-compose
# Add your user to docker group
sudo usermod -aG docker $USER
# Log out and back in for group changes
```
**Windows:**
```bash
# Download Docker Desktop from docker.com
# Requires WSL2
```
**Verify installation:**
```bash
docker --version
docker-compose --version
```
## Quick Start with Docker Compose
### Step 1: Create docker-compose.yml
In your Foldsite directory:
```yaml
version: '3.8'
services:
foldsite:
image: python:3.11-slim
container_name: foldsite
working_dir: /app
command: >
sh -c "pip install -r requirements.txt &&
python main.py --config config.toml"
ports:
- "8081:8081"
volumes:
- .:/app
- ./my-site/content:/app/content
- ./my-site/templates:/app/templates
- ./my-site/styles:/app/styles
environment:
- PYTHONUNBUFFERED=1
restart: unless-stopped
```
### Step 2: Create config.toml for Docker
```toml
[paths]
content_dir = "/app/content"
templates_dir = "/app/templates"
styles_dir = "/app/styles"
[server]
listen_address = "0.0.0.0" # Important: bind to all interfaces
listen_port = 8081
admin_browser = false
max_threads = 4
debug = false
access_log = true
```
### Step 3: Start Container
```bash
# Start in background
docker-compose up -d
# View logs
docker-compose logs -f
# Stop
docker-compose down
```
Visit `http://localhost:8081` to see your site!
## Building a Custom Docker Image
For production, build a dedicated Foldsite image:
### Create Dockerfile
```dockerfile
# Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first (for caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create directories for content
RUN mkdir -p /content /templates /styles
# Expose port
EXPOSE 8081
# Run application
CMD ["python", "main.py", "--config", "/app/config.toml"]
```
### Build Image
```bash
# Build image
docker build -t foldsite:latest .
# Tag for versioning
docker tag foldsite:latest foldsite:1.0.0
```
### Run Container
```bash
docker run -d \
--name foldsite \
-p 8081:8081 \
-v $(pwd)/my-site/content:/content \
-v $(pwd)/my-site/templates:/templates \
-v $(pwd)/my-site/styles:/styles \
foldsite:latest
```
## Development with Docker
### Hot Reload Setup
Mount your code as volumes for live updates:
```yaml
# docker-compose.dev.yml
version: '3.8'
services:
foldsite:
build: .
container_name: foldsite-dev
ports:
- "8081:8081"
volumes:
# Mount everything for development
- .:/app
- ./my-site/content:/app/content
- ./my-site/templates:/app/templates
- ./my-site/styles:/app/styles
environment:
- PYTHONUNBUFFERED=1
- FLASK_ENV=development
command: >
sh -c "pip install -r requirements.txt &&
python main.py --config config.toml"
```
**Usage:**
```bash
# Start development environment
docker-compose -f docker-compose.dev.yml up
# Changes to content, templates, and styles appear immediately
# Code changes require restart
```
### Interactive Development
Run commands inside container:
```bash
# Start bash session in running container
docker exec -it foldsite bash
# Inside container, you can:
python main.py --config config.toml
pip install new-package
ls /app/content
```
## Production Docker Setup
### Multi-Stage Build
Optimize image size with multi-stage build:
```dockerfile
# Dockerfile.production
# Stage 1: Build dependencies
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Copy Python dependencies from builder
COPY --from=builder /root/.local /root/.local
# Copy application
COPY . .
# Create non-root user
RUN useradd -m -u 1000 foldsite && \
chown -R foldsite:foldsite /app
# Switch to non-root user
USER foldsite
# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8081
# Use Gunicorn for production
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8081", "main:app"]
```
### Production docker-compose.yml
```yaml
version: '3.8'
services:
foldsite:
build:
context: .
dockerfile: Dockerfile.production
container_name: foldsite-prod
ports:
- "8081:8081"
volumes:
- ./content:/app/content:ro # Read-only
- ./templates:/app/templates:ro
- ./styles:/app/styles:ro
environment:
- PYTHONUNBUFFERED=1
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/"]
interval: 30s
timeout: 10s
retries: 3
```
## Docker Compose Examples
### With Nginx Reverse Proxy
```yaml
version: '3.8'
services:
foldsite:
build: .
container_name: foldsite
expose:
- "8081"
volumes:
- ./content:/app/content:ro
- ./templates:/app/templates:ro
- ./styles:/app/styles:ro
restart: always
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- foldsite
restart: always
```
**nginx.conf:**
```nginx
events {
worker_connections 1024;
}
http {
upstream foldsite {
server foldsite:8081;
}
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://foldsite;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
proxy_pass http://foldsite;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
}
```
### Multiple Sites
Run multiple Foldsite instances:
```yaml
version: '3.8'
services:
blog:
build: .
ports:
- "8081:8081"
volumes:
- ./blog/content:/app/content
- ./blog/templates:/app/templates
- ./blog/styles:/app/styles
restart: always
portfolio:
build: .
ports:
- "8082:8081"
volumes:
- ./portfolio/content:/app/content
- ./portfolio/templates:/app/templates
- ./portfolio/styles:/app/styles
restart: always
```
## Volume Management
### Content Volumes
**Development** - Mount host directories:
```yaml
volumes:
- ./my-site/content:/app/content
- ./my-site/templates:/app/templates
- ./my-site/styles:/app/styles
```
**Production** - Read-only mounts:
```yaml
volumes:
- ./content:/app/content:ro
- ./templates:/app/templates:ro
- ./styles:/app/styles:ro
```
### Named Volumes
For persistent data:
```yaml
services:
foldsite:
volumes:
- content-data:/app/content
- templates-data:/app/templates
volumes:
content-data:
templates-data:
```
**Backup named volumes:**
```bash
# Backup
docker run --rm \
-v foldsite_content-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/content-backup.tar.gz -C /data .
# Restore
docker run --rm \
-v foldsite_content-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/content-backup.tar.gz -C /data
```
## Environment Variables
Pass configuration via environment:
```yaml
services:
foldsite:
environment:
- FOLDSITE_DEBUG=false
- FOLDSITE_PORT=8081
- FOLDSITE_MAX_THREADS=4
```
**Use in config:**
```python
# In a config loader
import os
debug = os.getenv('FOLDSITE_DEBUG', 'false').lower() == 'true'
port = int(os.getenv('FOLDSITE_PORT', '8081'))
```
## Common Docker Commands
### Container Management
```bash
# Start containers
docker-compose up -d
# Stop containers
docker-compose down
# Restart
docker-compose restart
# View logs
docker-compose logs -f
# View logs for specific service
docker-compose logs -f foldsite
# Exec into running container
docker exec -it foldsite bash
# View running containers
docker ps
# View all containers (including stopped)
docker ps -a
```
### Image Management
```bash
# Build image
docker-compose build
# Pull images
docker-compose pull
# List images
docker images
# Remove unused images
docker image prune
# Remove all unused data
docker system prune -a
```
### Debugging
```bash
# Check container logs
docker logs foldsite
# Follow logs
docker logs -f foldsite
# Inspect container
docker inspect foldsite
# View container stats
docker stats foldsite
# Check container health
docker ps --filter health=healthy
```
## Troubleshooting
### Port Already in Use
```
Error: port is already allocated
```
**Solution:** Change port mapping:
```yaml
ports:
- "8082:8081" # Map to different host port
```
### Permission Errors
```
PermissionError: [Errno 13] Permission denied
```
**Solution:** Fix volume permissions:
```bash
# Fix ownership
sudo chown -R $USER:$USER ./my-site
# Or run container as your user
docker run --user $(id -u):$(id -g) ...
```
### Container Won't Start
```bash
# Check logs
docker-compose logs
# Common issues:
# 1. Missing requirements.txt
# 2. Wrong working directory
# 3. Port conflicts
# 4. Volume mount errors
```
### Changes Not Appearing
**Content changes:**
- Should appear immediately (volumes mounted)
- Try hard refresh in browser
**Code changes:**
- Require container restart: `docker-compose restart`
**Template changes:**
- Should appear immediately
- Check volume mounts are correct
### Container Crashes
```bash
# View exit reason
docker ps -a
# Check logs
docker logs foldsite
# Try running interactively
docker run -it foldsite bash
```
## Performance Optimization
### Resource Limits
Limit container resources:
```yaml
services:
foldsite:
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
```
### Build Cache
Speed up builds:
```bash
# Use BuildKit
DOCKER_BUILDKIT=1 docker build .
# Cache from registry
docker build --cache-from myregistry/foldsite:latest .
```
### Layer Optimization
Order Dockerfile for better caching:
```dockerfile
# Dependencies first (change rarely)
COPY requirements.txt .
RUN pip install -r requirements.txt
# Code last (changes often)
COPY . .
```
## CI/CD Integration
### GitHub Actions Example
```yaml
# .github/workflows/docker.yml
name: Build Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build -t foldsite:${{ github.sha }} .
- name: Test
run: |
docker run -d --name test foldsite:${{ github.sha }}
docker logs test
docker stop test
```
## Cloud Platform Deployment
### Deploy to AWS ECS
```bash
# Build and push to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
docker tag foldsite:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/foldsite:latest
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/foldsite:latest
```
### Deploy to Google Cloud Run
```bash
# Build and push to GCR
gcloud builds submit --tag gcr.io/PROJECT_ID/foldsite
gcloud run deploy --image gcr.io/PROJECT_ID/foldsite --platform managed
```
### Deploy to Azure Container Instances
```bash
# Create container instance
az container create \
--resource-group myResourceGroup \
--name foldsite \
--image myregistry.azurecr.io/foldsite:latest \
--ports 8081
```
## Next Steps
- **[Production Deployment](production.md)** - Production-grade setup
- **[Local Development](local-development.md)** - Development workflow
- **[Support](../support.md)** - Get help
Docker provides a solid foundation for deploying Foldsite anywhere. From development to production, containers ensure consistency and reliability.

View File

@ -0,0 +1,380 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Deployment Overview"
description: "Getting Foldsite running - from local development to production"
summary: "Learn how to deploy Foldsite in various environments: local development, Docker containers, or production servers."
quick_tips:
- "Start with local development for fastest iteration"
- "Docker provides consistent environments across machines"
- "Production deployment supports both dynamic and static modes"
---
# Deployment Overview
Foldsite is flexible in how you run it. Choose the deployment method that fits your needs:
## Deployment Options
### [Local Development](local-development.md)
**Best for:** Content creation, theme development, testing
Run Foldsite directly with Python for the fastest development cycle. Changes to content and templates appear immediately (no rebuild needed).
```bash
python main.py --config config.toml
```
**Pros:**
- Fastest iteration - see changes instantly
- Easy debugging with Python stack traces
- Full access to logs and error messages
**Cons:**
- Requires Python environment
- Manual dependency management
- Not suitable for production
**When to use:** Always use this during development. It's the fastest way to see your changes.
### [Docker Deployment](docker.md)
**Best for:** Consistent environments, easy deployment, testing production builds
Run Foldsite in a Docker container for isolated, reproducible deployments.
```bash
docker-compose up
```
**Pros:**
- Consistent across development and production
- Isolated dependencies
- Easy to share with team members
- Simplifies deployment to cloud platforms
**Cons:**
- Slight overhead from containerization
- Requires Docker knowledge
- Extra layer to debug
**When to use:** Use Docker when you need consistency across environments, or when deploying to platforms that support containers.
### [Production Deployment](production.md)
**Best for:** Public-facing websites, high-traffic sites, static hosting
Deploy Foldsite as either a dynamic Python server or export to static files.
**Dynamic Mode:**
```bash
gunicorn -w 4 -b 0.0.0.0:8081 main:app
```
**Static Export:**
```bash
# Generate static HTML files
python export.py --output ./dist
```
**Pros:**
- Optimized for production workloads
- Can use static hosting (cheap/free)
- Supports CDN caching
- Professional-grade performance
**Cons:**
- More complex setup
- Requires understanding of web servers
- Static mode requires rebuilds for updates
**When to use:** Use this for your live website once development is complete.
## Quick Comparison
| Method | Speed | Complexity | Best For |
|--------|-------|------------|----------|
| **Local Development** | ⚡⚡⚡ | ⭐ | Creating content and themes |
| **Docker** | ⚡⚡ | ⭐⭐ | Team collaboration, staging |
| **Production (Dynamic)** | ⚡⚡ | ⭐⭐⭐ | Live sites with frequent updates |
| **Production (Static)** | ⚡⚡⚡ | ⭐⭐⭐ | Live sites, maximum performance |
## Prerequisites
### For All Deployment Methods
1. **Git** (to clone the repository)
```bash
git --version
```
2. **A text editor** (VS Code, Sublime, vim, etc.)
### For Local Development
1. **Python 3.10+**
```bash
python3 --version
```
2. **pip** (Python package manager)
```bash
pip --version
```
### For Docker Deployment
1. **Docker** and **Docker Compose**
```bash
docker --version
docker-compose --version
```
### For Production Deployment
Choose based on your hosting strategy:
- **Dynamic mode:** Python 3.10+, web server (nginx/apache)
- **Static mode:** Any web server or static host (GitHub Pages, Netlify, etc.)
## Getting the Source Code
All deployment methods start by getting Foldsite:
```bash
# Clone the repository
git clone https://github.com/DWSresearch/foldsite.git
cd foldsite
# Or download and extract the latest release
wget https://github.com/DWSresearch/foldsite/archive/main.zip
unzip main.zip
cd foldsite-main
```
## Configuration
Every deployment uses a `config.toml` file to specify paths and settings:
```toml
[paths]
content_dir = "/path/to/your/content"
templates_dir = "/path/to/your/templates"
styles_dir = "/path/to/your/styles"
[server]
listen_address = "0.0.0.0"
listen_port = 8081
admin_browser = false
admin_password = "change-me"
max_threads = 4
debug = false
access_log = true
```
**Important:** Adjust these paths before running Foldsite!
### Development Config Example
```toml
[paths]
content_dir = "./my-site/content"
templates_dir = "./my-site/templates"
styles_dir = "./my-site/styles"
[server]
listen_address = "127.0.0.1" # Local only
listen_port = 8081
debug = true # Enable debug mode
access_log = true
```
### Production Config Example
```toml
[paths]
content_dir = "/var/www/site/content"
templates_dir = "/var/www/site/templates"
styles_dir = "/var/www/site/styles"
[server]
listen_address = "0.0.0.0" # All interfaces
listen_port = 8081
debug = false # Disable debug mode
access_log = true
max_threads = 8 # More workers for production
```
## Typical Deployment Workflow
### 1. Development Phase
Use **local development** for content creation and theme building:
```bash
# Edit content
vim content/my-post.md
# Run server
python main.py --config config.toml
# Visit http://localhost:8081
# See changes immediately
```
### 2. Testing Phase
Use **Docker** to test in a production-like environment:
```bash
# Build and run
docker-compose up
# Test on http://localhost:8081
# Verify everything works in container
```
### 3. Deployment Phase
Deploy to **production** using your preferred method:
```bash
# Option A: Dynamic server
gunicorn -w 4 main:app
# Option B: Static export
python export.py --output ./dist
rsync -avz ./dist/ user@server:/var/www/site/
```
## Common Scenarios
### Scenario: Personal Blog
**Best deployment:** Local development + static export to GitHub Pages
```bash
# Develop locally
python main.py --config config.toml
# Export to static files
python export.py --output ./docs
# Push to GitHub (Pages serves from /docs)
git add docs/
git commit -m "Update site"
git push
```
### Scenario: Team Documentation
**Best deployment:** Docker for everyone, dynamic server in production
```bash
# Everyone on the team uses Docker
docker-compose up
# Production uses dynamic Python server
# for real-time updates when docs change
```
### Scenario: Photography Portfolio
**Best deployment:** Local development, Docker for staging, static export for production
High-performance static site with CDN for fast image delivery.
## Troubleshooting
### Port Already in Use
```
OSError: [Errno 48] Address already in use
```
**Solution:** Change `listen_port` in config.toml or stop the other service using that port.
### Module Not Found
```
ModuleNotFoundError: No module named 'flask'
```
**Solution:** Install dependencies:
```bash
pip install -r requirements.txt
```
### Permission Denied
```
PermissionError: [Errno 13] Permission denied
```
**Solution:** Check directory permissions:
```bash
chmod -R 755 content/ templates/ styles/
```
### Template Not Found
```
Exception: Base template not found
```
**Solution:** Ensure `base.html` exists in templates directory:
```bash
ls templates/base.html
```
## Next Steps
Choose your deployment path:
- **[Local Development Guide](deployment/local-development.md)** - Start here for development
- **[Docker Deployment Guide](deployment/docker.md)** - Containerized deployment
- **[Production Deployment Guide](deployment/production.md)** - Go live with your site
## Security Considerations
### Development
- **Only bind to localhost** (`listen_address = "127.0.0.1"`)
- **Never commit config.toml** with sensitive data to version control
### Production
- **Use HTTPS** - Always use TLS/SSL in production
- **Strong passwords** - If using admin interface, use strong passwords
- **Firewall rules** - Only expose necessary ports
- **Regular updates** - Keep Foldsite and dependencies updated
- **Content validation** - Be careful with user-uploaded content
See [Production Deployment](production.md) for detailed security guidance.
## Performance Tips
### For All Deployments
- **Use hidden files** for drafts (`___draft.md`) to avoid processing
- **Optimize images** before uploading (Foldsite generates thumbnails, but smaller source = faster)
- **Minimize template complexity** - Simple templates render faster
### For Production
- **Enable caching** at the web server level
- **Use a CDN** for static assets
- **Compress responses** with gzip/brotli
- **Monitor resource usage** and scale as needed
## Getting Help
Need assistance with deployment?
- **[Support](../support.md)** - Community help and resources
- **GitHub Issues** - Report bugs or ask questions
- **Example Configs** - See `/examples` directory in repository
Happy deploying!

View File

@ -0,0 +1,629 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Local Development"
description: "Running Foldsite on your local machine for development"
summary: "Complete guide to setting up and running Foldsite locally for the fastest development workflow."
quick_tips:
- "Local development gives instant feedback - no build step needed"
- "Use debug mode to see template discovery and errors clearly"
- "Changes to content and templates appear immediately on refresh"
---
# Local Development
Running Foldsite locally is the fastest way to develop your site. Changes to content and templates appear instantly without any build process.
## Prerequisites
### Required Software
**Python 3.10 or higher**
```bash
# Check your Python version
python3 --version
# Should output: Python 3.10.x or higher
```
If you don't have Python 3.10+:
- **macOS:** `brew install python3`
- **Ubuntu/Debian:** `sudo apt install python3.10`
- **Windows:** Download from [python.org](https://www.python.org/downloads/)
**pip (Python package manager)**
```bash
# Check pip version
pip --version
# If missing, install
python3 -m ensurepip --upgrade
```
**Git (recommended)**
```bash
git --version
```
### Optional but Recommended
**Virtual environment support**
```bash
python3 -m venv --help
```
**Text editor with syntax highlighting**
- VS Code (recommended)
- Sublime Text
- Vim/Neovim
- Any editor you prefer
## Installation
### Step 1: Get Foldsite
**Option A: Clone with Git (recommended)**
```bash
# Clone the repository
git clone https://github.com/DWSresearch/foldsite.git
cd foldsite
```
**Option B: Download ZIP**
```bash
# Download latest release
wget https://github.com/DWSresearch/foldsite/archive/main.zip
unzip main.zip
cd foldsite-main
```
### Step 2: Create Virtual Environment
Using a virtual environment keeps dependencies isolated:
```bash
# Create virtual environment
python3 -m venv venv
# Activate it
# On macOS/Linux:
source venv/bin/activate
# On Windows:
venv\Scripts\activate
# Your prompt should now show (venv)
```
**Why virtual environment?**
- Isolates project dependencies
- Prevents conflicts with system Python
- Easy to recreate or remove
- Professional Python practice
### Step 3: Install Dependencies
```bash
# Ensure pip is up to date
pip install --upgrade pip
# Install Foldsite dependencies
pip install -r requirements.txt
```
**Dependencies installed:**
- Flask - Web framework
- Jinja2 - Template engine
- mistune - Markdown parser
- python-frontmatter - YAML frontmatter parsing
- Pillow - Image processing
- Gunicorn - WSGI server
**Troubleshooting:**
If you see compilation errors:
```bash
# Install build tools
# macOS:
xcode-select --install
# Ubuntu/Debian:
sudo apt install python3-dev build-essential
# Then retry:
pip install -r requirements.txt
```
### Step 4: Set Up Your Content
Create your site directory structure:
```bash
# Create your site directories
mkdir -p my-site/content
mkdir -p my-site/templates
mkdir -p my-site/styles
# Create a basic index page
echo "# Welcome to My Site" > my-site/content/index.md
# Copy example templates to start
cp -r example_site/template/* my-site/templates/
cp -r example_site/style/* my-site/styles/
```
### Step 5: Configure Foldsite
Create a configuration file:
```bash
# Copy example config
cp config.toml my-site-config.toml
```
Edit `my-site-config.toml`:
```toml
[paths]
content_dir = "/absolute/path/to/my-site/content"
templates_dir = "/absolute/path/to/my-site/templates"
styles_dir = "/absolute/path/to/my-site/styles"
[server]
listen_address = "127.0.0.1" # Only accessible from your machine
listen_port = 8081
admin_browser = false # Disable admin interface for now
max_threads = 4
debug = true # Enable debug mode for development
access_log = true
```
**Important:** Use absolute paths in development to avoid confusion.
**Find absolute path:**
```bash
# macOS/Linux:
cd my-site && pwd
# Windows:
cd my-site && cd
```
## Running Foldsite
### Start the Server
```bash
# Make sure virtual environment is activated
source venv/bin/activate # or venv\Scripts\activate on Windows
# Run Foldsite
python main.py --config my-site-config.toml
```
**Expected output:**
```
[2025-01-15 10:30:45] [INFO] Starting Foldsite server
[2025-01-15 10:30:45] [INFO] Content directory: /path/to/my-site/content
[2025-01-15 10:30:45] [INFO] Templates directory: /path/to/my-site/templates
[2025-01-15 10:30:45] [INFO] Listening on http://127.0.0.1:8081
```
### Visit Your Site
Open your browser to:
```
http://localhost:8081
```
You should see your site!
### Stop the Server
Press `Ctrl+C` in the terminal where Foldsite is running.
## Development Workflow
### The Edit-Refresh Cycle
Foldsite has **no build step**. Changes appear immediately:
1. **Edit content** - Modify markdown files
2. **Save file** - Ctrl+S / Cmd+S
3. **Refresh browser** - F5 or Cmd+R
4. **See changes instantly**
**What updates live:**
- Content (markdown files)
- Templates (HTML files)
- Styles (CSS files)
- Configuration (requires restart)
### Example Workflow
**Scenario:** Adding a blog post
```bash
# 1. Create new post
vim my-site/content/blog/my-new-post.md
```
```markdown
---
title: "My New Blog Post"
date: "2025-01-15"
tags: ["tutorial", "foldsite"]
---
# My New Blog Post
This is my latest post!
```
```bash
# 2. Save file and switch to browser
# 3. Visit http://localhost:8081/blog/my-new-post.md
# 4. See your post rendered immediately
```
**No restart needed!**
### Working with Templates
```bash
# 1. Edit template
vim my-site/templates/__file.md.html
```
```html
<!-- Add a new section -->
<article>
<header>
<h1>{{ metadata.title }}</h1>
<time>{{ metadata.date }}</time>
</header>
{{ content|safe }}
<!-- NEW: Add related posts -->
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<aside class="related">
<h3>Related Posts</h3>
{% for post in related %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
</aside>
{% endif %}
</article>
```
```bash
# 2. Save and refresh browser
# 3. See related posts section appear
```
### Working with Styles
```bash
# 1. Edit CSS
vim my-site/styles/base.css
```
```css
/* Add new styles */
.related {
margin-top: 2rem;
padding: 1rem;
background: #f5f5f5;
border-radius: 4px;
}
.related h3 {
margin-bottom: 0.5rem;
}
```
```bash
# 2. Save and hard refresh (Cmd+Shift+R / Ctrl+Shift+R)
# 3. See styled related posts
```
## Debug Mode
Enable debug mode for helpful development information:
```toml
[server]
debug = true
```
**What debug mode shows:**
- Template discovery process
- Which templates were considered
- Which template was chosen
- Detailed error messages with stack traces
- Template variables and context
**Example debug output:**
When visiting a page, console shows:
```
[DEBUG] Template search for: blog/my-post.md
[DEBUG] Checking: templates/blog/my-post.html - Not found
[DEBUG] Checking: templates/blog/__file.md.html - Found!
[DEBUG] Using template: templates/blog/__file.md.html
[DEBUG] Available variables: content, metadata, currentPath, styles
```
**View in browser:**
With debug mode, error pages show:
- Full Python stack trace
- Template rendering context
- What went wrong and where
- Suggestions for fixes
## Common Development Tasks
### Creating New Pages
```bash
# Simple page
echo "# About Me\n\nI'm a web developer." > my-site/content/about.md
# With frontmatter
cat > my-site/content/projects.md << 'EOF'
---
title: "My Projects"
description: "Things I've built"
---
# My Projects
Here are some things I've worked on...
EOF
```
### Organizing Content
```bash
# Create a blog section
mkdir -p my-site/content/blog
# Add posts
for i in {1..5}; do
cat > my-site/content/blog/post-$i.md << EOF
---
title: "Blog Post $i"
date: "2024-01-$i"
tags: ["example"]
---
# Post $i
Content here...
EOF
done
```
### Testing Template Helpers
Create a test page to experiment:
```bash
cat > my-site/content/test.md << 'EOF'
---
title: "Template Helper Test"
---
# Testing Helpers
## Recent Posts
{% for post in get_recent_posts(limit=5) %}
- [{{ post.title }}]({{ post.url }}) - {{ post.date }}
{% endfor %}
## All Tags
{% for tag in get_all_tags() %}
- {{ tag.name }} ({{ tag.count }})
{% endfor %}
## Current Path Info
- Path: {{ currentPath }}
- Metadata: {{ metadata }}
EOF
```
Visit `/test.md` to see helper output.
### Hiding Work in Progress
Use the `___` prefix:
```bash
# Hidden draft
vim my-site/content/___draft-post.md
# Hidden development folder
mkdir my-site/content/___testing
```
These won't appear in navigation or listings.
## Editor Setup
### VS Code
Recommended extensions:
- **Python** (Microsoft)
- **Jinja** (wholroyd.jinja)
- **Markdown All in One** (yzhang.markdown-all-in-one)
**Settings:**
```json
{
"files.associations": {
"*.html": "jinja-html"
},
"editor.formatOnSave": true
}
```
### Vim/Neovim
Add to `.vimrc` or `init.vim`:
```vim
" Jinja syntax for .html files
autocmd BufNewFile,BufRead */templates/*.html set filetype=jinja
" Markdown settings
autocmd FileType markdown setlocal spell spelllang=en_us
autocmd FileType markdown setlocal textwidth=80
```
## Troubleshooting
### Port Already in Use
```
Error: [Errno 48] Address already in use
```
**Solution 1:** Change port in config:
```toml
[server]
listen_port = 8082
```
**Solution 2:** Find and kill process:
```bash
# Find process using port 8081
lsof -i :8081
# Kill it
kill -9 <PID>
```
### Module Not Found
```
ModuleNotFoundError: No module named 'flask'
```
**Solution:**
```bash
# Ensure virtual environment is activated
source venv/bin/activate
# Reinstall dependencies
pip install -r requirements.txt
```
### Template Not Found
```
Exception: Base template not found
```
**Solution:**
```bash
# Check templates directory exists and has base.html
ls my-site/templates/base.html
# If missing, copy from example
cp example_site/template/base.html my-site/templates/
```
### Changes Not Appearing
**Possible causes:**
1. **Browser cache** - Hard refresh (Cmd/Ctrl + Shift + R)
2. **Wrong file** - Check you're editing the file Foldsite is using
3. **Configuration issue** - Verify config.toml paths
4. **Syntax error** - Check console for error messages
**Debug:**
```bash
# Enable debug mode
# Edit config.toml
[server]
debug = true
# Restart Foldsite
# Check console output
```
### Permission Denied
```
PermissionError: [Errno 13] Permission denied
```
**Solution:**
```bash
# Fix permissions
chmod -R 755 my-site/
# Or run with correct user
sudo chown -R $USER:$USER my-site/
```
## Performance Tips
### Development Speed
**Fast iteration:**
- Keep browser DevTools open
- Use multiple browser windows for comparison
- Enable auto-reload browser extensions
- Use terminal multiplexer (tmux/screen)
**Organize workspace:**
```
Terminal 1: Foldsite server
Terminal 2: Content editing
Terminal 3: Git operations
Browser: Live preview
Editor: Code/templates
```
### Working with Large Sites
If your site has many files:
```toml
[server]
max_threads = 8 # Increase workers
```
**Cache considerations:**
- Template helpers are cached automatically
- Folder contents cached for 5 minutes
- Recent posts cached for 10 minutes
- Restart server to clear caches
## Next Steps
Now that you have local development running:
- **[Templates Guide](../templates/)** - Learn the template system
- **[Template Helpers](../templates/template-helpers.md)** - Explore helper functions
- **[Recipes](../recipes/)** - Copy working examples
- **[Docker Deployment](docker.md)** - Test in container
- **[Production Deployment](production.md)** - Go live
## Tips from Developers
> "Keep the server running in a dedicated terminal. Switch to it to see errors immediately." — Foldsite contributor
> "Use `___testing/` folder for experimenting. It's ignored so you can mess around without affecting the site." — Content creator
> "Debug mode is your friend. Always enable it in development." — Theme developer
> "Create test pages to try helper functions. Much faster than reading docs." — Documentation writer
Happy developing! Local development is where the magic happens. 🚀

View File

@ -0,0 +1,810 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Production Deployment"
description: "Deploying Foldsite for production use"
summary: "Complete guide to deploying Foldsite in production - from VPS servers to static hosting, with security, performance, and reliability best practices."
quick_tips:
- "Use Gunicorn with multiple workers for dynamic deployments"
- "Static export is fastest and cheapest for sites that don't change frequently"
- "Always use HTTPS in production with proper SSL certificates"
---
# Production Deployment
Deploy Foldsite to serve real traffic with reliability, security, and performance.
## Deployment Options
### Option 1: Dynamic Server (Python)
**Best for:**
- Frequently updated content
- Sites needing template helpers in real-time
- Admin file browser interface
- Dynamic content generation
**Characteristics:**
- Runs Python/Gunicorn server
- Content updates appear immediately
- Template helpers work dynamically
- Requires server with Python
**Hosting:** VPS, dedicated server, PaaS platforms
### Option 2: Static Export
**Best for:**
- Infrequently updated sites
- Maximum performance
- Minimal cost
- CDN delivery
**Characteristics:**
- Pre-rendered HTML files
- Blazing fast delivery
- Can host anywhere (GitHub Pages, Netlify, S3)
- Rebuild required for updates
**Hosting:** Static hosts, CDN, object storage
## Dynamic Server Deployment
### Prerequisites
- Linux server (Ubuntu 20.04+ recommended)
- Python 3.10+
- Domain name
- SSH access
### Step 1: Server Setup
**Update system:**
```bash
sudo apt update
sudo apt upgrade -y
```
**Install dependencies:**
```bash
# Python and pip
sudo apt install -y python3.10 python3-pip python3-venv
# Build tools
sudo apt install -y build-essential python3-dev
# Nginx
sudo apt install -y nginx
# Certbot for SSL
sudo apt install -y certbot python3-certbot-nginx
```
### Step 2: Deploy Foldsite
**Create user:**
```bash
sudo useradd -m -s /bin/bash foldsite
sudo su - foldsite
```
**Clone and setup:**
```bash
# Clone repository
git clone https://github.com/DWSresearch/foldsite.git
cd foldsite
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
pip install gunicorn # Production WSGI server
```
**Setup your content:**
```bash
# Create site structure
mkdir -p ~/site/content
mkdir -p ~/site/templates
mkdir -p ~/site/styles
# Copy your content
# (upload via SCP, rsync, or git)
```
**Create production config:**
```bash
vim ~/foldsite/production-config.toml
```
```toml
[paths]
content_dir = "/home/foldsite/site/content"
templates_dir = "/home/foldsite/site/templates"
styles_dir = "/home/foldsite/site/styles"
[server]
listen_address = "127.0.0.1" # Only accept local connections
listen_port = 8081
admin_browser = false # Disable for security
max_threads = 8 # Adjust based on server
debug = false # Never enable in production
access_log = true
```
### Step 3: Systemd Service
Create service file:
```bash
sudo vim /etc/systemd/system/foldsite.service
```
```ini
[Unit]
Description=Foldsite Web Server
After=network.target
[Service]
Type=simple
User=foldsite
Group=foldsite
WorkingDirectory=/home/foldsite/foldsite
Environment="PATH=/home/foldsite/foldsite/venv/bin"
ExecStart=/home/foldsite/foldsite/venv/bin/gunicorn \
--workers 4 \
--bind 127.0.0.1:8081 \
--access-logfile /var/log/foldsite/access.log \
--error-logfile /var/log/foldsite/error.log \
--config /home/foldsite/foldsite/production-config.toml \
main:app
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
**Create log directory:**
```bash
sudo mkdir -p /var/log/foldsite
sudo chown foldsite:foldsite /var/log/foldsite
```
**Enable and start service:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable foldsite
sudo systemctl start foldsite
# Check status
sudo systemctl status foldsite
```
### Step 4: Nginx Reverse Proxy
**Create Nginx config:**
```bash
sudo vim /etc/nginx/sites-available/foldsite
```
```nginx
upstream foldsite {
server 127.0.0.1:8081;
keepalive 32;
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Logs
access_log /var/log/nginx/foldsite-access.log;
error_log /var/log/nginx/foldsite-error.log;
# Max upload size
client_max_body_size 10M;
# Proxy to Foldsite
location / {
proxy_pass http://foldsite;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://foldsite;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# Security: deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
```
**Enable site:**
```bash
sudo ln -s /etc/nginx/sites-available/foldsite /etc/nginx/sites-enabled/
sudo nginx -t # Test configuration
sudo systemctl reload nginx
```
### Step 5: SSL Certificate
**Get certificate with Certbot:**
```bash
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
```
Follow prompts. Certbot will:
- Obtain certificate from Let's Encrypt
- Modify Nginx config for HTTPS
- Setup auto-renewal
**Verify auto-renewal:**
```bash
sudo certbot renew --dry-run
```
**Final Nginx config (with SSL):**
Certbot updates your config to include:
```nginx
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# ... rest of config
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com www.your-domain.com;
return 301 https://$server_name$request_uri;
}
```
### Step 6: Firewall
**Configure UFW:**
```bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
```
### Verification
Visit your domain:
```
https://your-domain.com
```
Should see your Foldsite with valid SSL!
## Static Export Deployment
### Generate Static Files
*Note: Static export functionality may need to be implemented or use a static site generator mode*
**Conceptual approach:**
```python
# export.py
import os
from pathlib import Path
from src.rendering.renderer import render_page
from src.config.config import Configuration
def export_static(config_path, output_dir):
"""Export all pages to static HTML"""
config = Configuration(config_path)
config.load_config()
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
# Walk content directory
for content_file in config.content_dir.rglob('*'):
if content_file.is_file() and not content_file.name.startswith('___'):
# Generate output path
rel_path = content_file.relative_to(config.content_dir)
out_path = output / rel_path.with_suffix('.html')
out_path.parent.mkdir(parents=True, exist_ok=True)
# Render and save
html = render_page(
content_file,
base_path=config.content_dir,
template_path=config.templates_dir,
style_path=config.styles_dir
)
with open(out_path, 'w') as f:
f.write(html)
# Copy styles
import shutil
shutil.copytree(config.styles_dir, output / 'styles')
if __name__ == '__main__':
import sys
export_static(sys.argv[1], sys.argv[2])
```
**Usage:**
```bash
python export.py config.toml ./dist
```
### Deploy Static Files
#### GitHub Pages
```bash
# Export to docs/
python export.py config.toml ./docs
# Commit and push
git add docs/
git commit -m "Update site"
git push
# Enable Pages in GitHub repo settings
# Source: docs/ folder
```
#### Netlify
```bash
# Export
python export.py config.toml ./dist
# Install Netlify CLI
npm install -g netlify-cli
# Deploy
netlify deploy --prod --dir=dist
```
**Or use continuous deployment:**
```yaml
# netlify.toml
[build]
command = "pip install -r requirements.txt && python export.py config.toml dist"
publish = "dist"
```
#### AWS S3 + CloudFront
```bash
# Export
python export.py config.toml ./dist
# Sync to S3
aws s3 sync ./dist s3://your-bucket-name --delete
# Invalidate CloudFront cache
aws cloudfront create-invalidation --distribution-id YOUR_DIST_ID --paths "/*"
```
#### Vercel
```bash
# Export
python export.py config.toml ./dist
# Deploy
vercel --prod ./dist
```
## Performance Optimization
### Nginx Caching
Add to Nginx config:
```nginx
# Define cache path
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=foldsite_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
# ...
location / {
proxy_cache foldsite_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://foldsite;
}
}
```
### Gzip Compression
```nginx
# In /etc/nginx/nginx.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;
```
### Image Optimization
Foldsite automatically generates thumbnails, but optimize source images:
```bash
# Install optimization tools
sudo apt install jpegoptim optipng
# Optimize JPEGs
find content/ -name "*.jpg" -exec jpegoptim --strip-all {} \;
# Optimize PNGs
find content/ -name "*.png" -exec optipng -o2 {} \;
```
### CDN Integration
Use CDN for static assets:
```nginx
# Separate static assets
location /styles/ {
alias /home/foldsite/site/styles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /download/ {
# Proxy to Foldsite for thumbnails
proxy_pass http://foldsite;
expires 30d;
}
```
Then point DNS for `static.yourdomain.com` to CDN origin.
## Monitoring & Logging
### Application Logs
**View logs:**
```bash
# Systemd logs
sudo journalctl -u foldsite -f
# Application logs
tail -f /var/log/foldsite/error.log
tail -f /var/log/foldsite/access.log
```
### Nginx Logs
```bash
tail -f /var/log/nginx/foldsite-access.log
tail -f /var/log/nginx/foldsite-error.log
```
### Log Rotation
Create `/etc/logrotate.d/foldsite`:
```
/var/log/foldsite/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 foldsite foldsite
sharedscripts
postrotate
systemctl reload foldsite
endscript
}
```
### Monitoring Tools
**Install basic monitoring:**
```bash
# Netdata (system monitoring)
bash <(curl -Ss https://my-netdata.io/kickstart.sh)
# Access at http://your-server:19999
```
**Check application health:**
```bash
#!/bin/bash
# health-check.sh
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/)
if [ $response -eq 200 ]; then
echo "Foldsite is healthy"
exit 0
else
echo "Foldsite is down (HTTP $response)"
systemctl restart foldsite
exit 1
fi
```
Run via cron:
```bash
*/5 * * * * /usr/local/bin/health-check.sh >> /var/log/foldsite/health.log 2>&1
```
## Backup Strategy
### Content Backup
```bash
#!/bin/bash
# backup.sh
BACKUP_DIR="/backups/foldsite"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Backup content, templates, styles
tar czf $BACKUP_DIR/site-$DATE.tar.gz \
/home/foldsite/site/
# Keep only last 30 days
find $BACKUP_DIR -name "site-*.tar.gz" -mtime +30 -delete
echo "Backup completed: site-$DATE.tar.gz"
```
**Run daily via cron:**
```bash
0 2 * * * /usr/local/bin/backup.sh
```
### Remote Backup
```bash
# Sync to remote server
rsync -avz /home/foldsite/site/ user@backup-server:/backups/foldsite/
# Or sync to S3
aws s3 sync /home/foldsite/site/ s3://your-backup-bucket/foldsite/
```
## Updating Foldsite
### Update Process
```bash
# As foldsite user
cd ~/foldsite
# Pull latest code
git pull
# Activate venv
source venv/bin/activate
# Update dependencies
pip install -r requirements.txt
# Restart service
sudo systemctl restart foldsite
# Check logs
sudo journalctl -u foldsite -n 50
```
### Zero-Downtime Updates
Use multiple Gunicorn workers and graceful reloading:
```bash
# Graceful reload (workers restart one by one)
sudo systemctl reload foldsite
# Or send HUP signal to Gunicorn
sudo pkill -HUP gunicorn
```
## Security Hardening
### Disable Directory Listing
Nginx automatically prevents this, but verify:
```nginx
autoindex off;
```
### Rate Limiting
Add to Nginx config:
```nginx
limit_req_zone $binary_remote_addr zone=foldsite_limit:10m rate=10r/s;
server {
location / {
limit_req zone=foldsite_limit burst=20 nodelay;
# ... rest of config
}
}
```
### Fail2ban
Protect against brute force:
```bash
sudo apt install fail2ban
# Create /etc/fail2ban/jail.local
[nginx-foldsite]
enabled = true
port = http,https
filter = nginx-foldsite
logpath = /var/log/nginx/foldsite-access.log
maxretry = 5
bantime = 3600
```
### Security Headers
Already in Nginx config, but verify:
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:;" always;
```
### File Permissions
Ensure proper permissions:
```bash
# Content should be readable by foldsite user
chmod -R 755 ~/site/content
chmod -R 755 ~/site/templates
chmod -R 755 ~/site/styles
# Application should not be writable
chmod -R 555 ~/foldsite/src
```
## Troubleshooting
### Service Won't Start
```bash
# Check logs
sudo journalctl -u foldsite -xe
# Common issues:
# - Wrong Python path
# - Missing dependencies
# - Port already in use
# - Permission errors
```
### 502 Bad Gateway
Nginx can't reach Foldsite:
```bash
# Check if Foldsite is running
sudo systemctl status foldsite
# Check if port is listening
sudo netstat -tulpn | grep 8081
# Check Nginx error log
sudo tail /var/log/nginx/error.log
```
### High Memory Usage
```bash
# Check process memory
ps aux | grep gunicorn
# Reduce workers or add swap
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
```
### Slow Response Times
```bash
# Check Nginx access logs for slow requests
sudo tail -f /var/log/nginx/foldsite-access.log
# Enable query logging in Foldsite
# Check for expensive template helpers
# Consider caching with Redis/Memcached
```
## Platform-Specific Guides
### DigitalOcean
```bash
# Create droplet (Ubuntu 22.04)
# Follow server setup steps above
# Use DigitalOcean firewall for security
# Enable backups in control panel
```
### AWS EC2
```bash
# Launch Ubuntu instance
# Setup security groups (ports 22, 80, 443)
# Use Elastic IP for static IP
# Consider RDS for database if needed
```
### Hetzner Cloud
```bash
# Create CX11 or larger instance
# Follow server setup
# Use Hetzner firewall
# Consider Hetzner volumes for storage
```
## Next Steps
- **[Local Development](local-development.md)** - Development workflow
- **[Docker Deployment](docker.md)** - Container deployment
- **[Support](../support.md)** - Get help
Your Foldsite is now production-ready! Monitor it regularly, keep it updated, and enjoy your self-hosted corner of the internet.

View File

@ -0,0 +1,594 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Develop Foldsite"
description: "Contributing to Foldsite development"
summary: "Guidelines for contributing code, documentation, themes, and ideas to the Foldsite project."
quick_tips:
- "Start with documentation or small bug fixes"
- "Discuss major changes in issues before implementing"
- "Follow existing code style and patterns"
---
# Develop Foldsite
Want to contribute to Foldsite? We welcome contributions of all kinds!
## Ways to Contribute
### 1. Documentation
**Impact:** High | **Difficulty:** Low
Improving documentation helps everyone:
- Fix typos and unclear explanations
- Add missing examples
- Create tutorials
- Translate to other languages
**How to start:**
1. Find documentation that confused you
2. Fork the repository
3. Edit markdown files in `docs/content/`
4. Submit pull request
No coding required!
### 2. Bug Fixes
**Impact:** High | **Difficulty:** Low to Medium
Fix issues others are experiencing:
- Browse [GitHub Issues](https://github.com/DWSresearch/foldsite/issues)
- Look for "good first issue" label
- Fix the bug
- Add test if possible
- Submit pull request
### 3. New Features
**Impact:** High | **Difficulty:** Medium to High
Add new capabilities:
- Discuss in issue first
- Get feedback on approach
- Implement feature
- Add documentation
- Add tests
- Submit pull request
### 4. Templates & Themes
**Impact:** Medium | **Difficulty:** Medium
Create reusable designs:
- Build complete theme
- Document installation
- Submit to theme gallery
- Help others customize
### 5. Testing
**Impact:** Medium | **Difficulty:** Low
Help ensure quality:
- Test new releases
- Report bugs with details
- Verify fixes work
- Test on different platforms
## Development Setup
### Prerequisites
- Python 3.10 or higher
- Git
- Text editor or IDE
### Getting Started
```bash
# Clone repository
git clone https://github.com/DWSresearch/foldsite.git
cd foldsite
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Install development dependencies
pip install -r requirements-dev.txt # If exists
# Run tests
python -m pytest # If tests exist
# Run Foldsite
python main.py --config config.toml
```
### Development Workflow
1. **Create branch** for your changes:
```bash
git checkout -b feature/my-feature
```
2. **Make changes** following code style
3. **Test changes**:
```bash
# Manual testing
python main.py --config config.toml
# Run tests if available
python -m pytest
```
4. **Commit changes**:
```bash
git add .
git commit -m "Add feature: description"
```
5. **Push and create PR**:
```bash
git push origin feature/my-feature
```
## Code Style
### Python
Follow [PEP 8](https://pep8.org/) with these specifics:
**Formatting:**
- 4 spaces for indentation
- Max line length: 100 characters
- Use type hints where helpful
- Docstrings for public functions
**Example:**
```python
def render_page(
path: Path,
base_path: Path = Path("./"),
template_path: Path = Path("./"),
) -> str:
"""
Renders a web page based on the provided path.
Args:
path: The path to the target file or directory
base_path: The base path for relative paths
template_path: Path to directory containing templates
Returns:
Rendered HTML content of the page
"""
# Implementation...
```
**Naming:**
- `snake_case` for functions and variables
- `PascalCase` for classes
- `UPPER_CASE` for constants
### HTML Templates
- 2 spaces for indentation
- Close all tags
- Use semantic HTML
- Comment complex logic
```html
<article class="post">
{% if metadata %}
<h1>{{ metadata.title }}</h1>
{# Loop through tags #}
{% if metadata.tags %}
<div class="tags">
{% for tag in metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
{% endif %}
</article>
```
### CSS
- 2 spaces for indentation
- Alphabetical property order (within reason)
- Mobile-first responsive design
- BEM naming for classes (optional but encouraged)
```css
.post {
margin: 2rem 0;
padding: 1rem;
}
.post__title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1rem;
}
@media (min-width: 768px) {
.post {
margin: 3rem 0;
padding: 2rem;
}
}
```
## Architecture Overview
### Key Components
**main.py**
- Entry point
- Initializes configuration
- Registers template helpers
- Starts server
**src/config/**
- Configuration loading
- Command-line argument parsing
**src/server/**
- Flask application setup
- Route registration
- File manager (admin interface)
**src/routes/**
- URL routing logic
- Path validation and security
- Content serving
**src/rendering/**
- Template discovery
- Markdown rendering
- Page rendering pipeline
- Helper functions
### Request Flow
```
1. HTTP Request
2. Flask routes (/src/routes/routes.py)
3. Path validation & security checks
4. Template discovery (/src/rendering/template_discovery.py)
5. Content rendering (/src/rendering/renderer.py)
6. Template rendering with Jinja2
7. HTTP Response
```
### Adding a New Template Helper
Template helpers are functions available in all templates.
**1. Add to TemplateHelpers class** (`src/rendering/helpers.py`):
```python
class TemplateHelpers:
def __init__(self, config: Configuration):
self.config = config
def get_my_helper(self, param: str) -> list:
"""
Description of what this helper does.
Args:
param: Description of parameter
Returns:
Description of return value
"""
# Implementation
result = []
# ... logic ...
return result
```
**2. Register in main.py**:
```python
server.register_template_function("get_my_helper", t.get_my_helper)
```
**3. Use in templates**:
```jinja
{% for item in get_my_helper('value') %}
{{ item }}
{% endfor %}
```
**4. Document** in `docs/content/templates/template-helpers.md`
## Testing
### Manual Testing
Create test content:
```
test-site/
├── content/
│ ├── index.md
│ ├── test-post.md
│ └── photos/
│ └── test.jpg
├── templates/
│ ├── base.html
│ └── __file.md.html
└── styles/
└── base.css
```
Run and verify:
```bash
python main.py --config test-config.toml
```
### Writing Tests
*Note: Test infrastructure may need to be set up*
Example test structure:
```python
# tests/test_renderer.py
import pytest
from pathlib import Path
from src.rendering.renderer import render_page
def test_render_simple_markdown():
"""Test rendering a basic markdown file"""
# Setup
content = Path("test-content/simple.md")
templates = Path("test-templates")
# Execute
result = render_page(content, template_path=templates)
# Assert
assert "<h1>" in result
assert "<!DOCTYPE html>" in result
```
## Pull Request Guidelines
### Before Submitting
- [ ] Code follows style guidelines
- [ ] Changes are focused (one feature/fix per PR)
- [ ] Commit messages are clear
- [ ] Documentation updated if needed
- [ ] Manual testing completed
- [ ] No merge conflicts
### PR Description Template
```markdown
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactoring
- [ ] Other (describe)
## Testing
How you tested the changes
## Screenshots
If UI changes, add screenshots
## Checklist
- [ ] Code follows project style
- [ ] Self-reviewed code
- [ ] Commented complex code
- [ ] Updated documentation
- [ ] No breaking changes (or documented)
```
### Review Process
1. **Maintainer reviews** code and design
2. **Feedback provided** if changes needed
3. **Discussion** on approach if necessary
4. **Approval** when ready
5. **Merge** by maintainer
Be patient - maintainers are volunteers.
## Issue Guidelines
### Bug Reports
Use this template:
```markdown
**Foldsite Version:** X.X.X
**Python Version:** X.X.X
**OS:** macOS/Linux/Windows
**Description:**
Clear description of the bug
**Steps to Reproduce:**
1. Step one
2. Step two
3. See error
**Expected Behavior:**
What should happen
**Actual Behavior:**
What actually happens
**Error Messages:**
```
Paste full error/traceback
```
**Additional Context:**
Any other relevant information
```
### Feature Requests
Use this template:
```markdown
**Feature Description:**
Clear description of the feature
**Use Case:**
Why you need this feature
**Proposed Implementation:**
Ideas for how it could work (optional)
**Alternatives Considered:**
Other solutions you've thought about
**Additional Context:**
Examples, mockups, etc.
```
## Communication
### GitHub Issues
- **Bugs** - Report problems
- **Features** - Request improvements
- **Questions** - Ask for help (or use Discussions)
### GitHub Discussions
- **Q&A** - Get help using Foldsite
- **Ideas** - Discuss potential features
- **Show and Tell** - Share what you built
- **General** - Everything else
### Response Times
- **Critical bugs** - Few days
- **Other issues** - 1-2 weeks
- **Feature requests** - Varies
- **PRs** - 1-2 weeks
## Security
### Reporting Vulnerabilities
**Do not** open public issues for security problems.
Email: security@dws.rip
Include:
- Description of vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if you have one)
### Security Considerations
When contributing:
- Validate all file paths
- Sanitize user inputs
- Be careful with path traversal
- Don't expose sensitive info in errors
- Follow principle of least privilege
## Project Philosophy
Keep these in mind when contributing:
### 1. Simplicity
Prefer simple solutions over complex ones. Add complexity only when necessary.
### 2. Convention Over Configuration
Sensible defaults that work for most cases. Configuration for edge cases.
### 3. User Focus
Optimize for content creators, not developers. Make common tasks easy.
### 4. Clear Over Clever
Code should be understandable. Clever tricks make maintenance harder.
### 5. Backward Compatibility
Don't break existing sites without very good reason and clear migration path.
## Resources
### Learning
- **Python** - [docs.python.org](https://docs.python.org/)
- **Flask** - [flask.palletsprojects.com](https://flask.palletsprojects.com/)
- **Jinja2** - [jinja.palletsprojects.com](https://jinja.palletsprojects.com/)
### Tools
- **VS Code** - Great Python support with extensions
- **PyCharm** - Full-featured Python IDE
- **Git** - Version control
- **GitHub CLI** - Command-line GitHub interface
## Recognition
Contributors are recognized:
- **README** - Listed in contributors section
- **Release notes** - Mentioned in changelogs
- **Commits** - Your name in git history
- **Gratitude** - Appreciation from the community!
## Getting Help
Stuck on contribution?
- **Read existing code** - Learn from examples
- **Ask in Discussions** - Community can help
- **Open draft PR** - Get early feedback
- **Reach out** - DWS team is friendly!
## Next Steps
- **Browse issues** - Find something to work on
- **Read the code** - Understand how it works
- **Make a change** - Start small
- **Submit PR** - Share your contribution!
Thank you for helping make Foldsite better!
**Remember:** Every contribution matters, from fixing a typo to adding major features. We appreciate all help!

View File

@ -0,0 +1,441 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Directory Structure Guide"
description: "Understanding how to organize your Foldsite project"
summary: "Learn how Foldsite's directory structure works - where to put content, templates, and styles for maximum flexibility and power."
quick_tips:
- "content/ is where all your pages and media live"
- "templates/ cascade down through subdirectories for hierarchical theming"
- "Hidden files (starting with ___) are ignored by Foldsite"
---
# Directory Structure Guide
Understanding Foldsite's directory structure is essential to using it effectively. The beauty of Foldsite is that **your folder structure IS your site structure**.
## The Three Core Directories
Every Foldsite project has three main directories:
```
my-site/
├── content/ # Your pages, posts, images, and files
├── templates/ # HTML templates using Jinja2
└── styles/ # CSS stylesheets
```
### content/ - Your Website Content
This is where everything your visitors see lives. Every file and folder in here becomes part of your website.
**Example structure:**
```
content/
├── index.md # Homepage (/)
├── about.md # About page (/about.md)
├── contact.md # Contact page (/contact.md)
├── blog/ # Blog section (/blog)
│ ├── 2024-01-15-first-post.md
│ ├── 2024-02-20-second-post.md
│ └── archives/
│ └── 2023-recap.md
├── photos/ # Photo galleries
│ ├── vacation/
│ │ ├── IMG_001.jpg
│ │ ├── IMG_002.jpg
│ │ └── IMG_003.jpg
│ └── family/
│ ├── portrait.jpg
│ └── gathering.jpg
└── downloads/ # Files for download
├── resume.pdf
└── presentation.pptx
```
**How URLs work:**
- `content/index.md``http://yoursite.com/`
- `content/about.md``http://yoursite.com/about.md`
- `content/blog/first-post.md``http://yoursite.com/blog/first-post.md`
- `content/photos/vacation/``http://yoursite.com/photos/vacation`
### templates/ - HTML Templates
Templates define how your content is displayed. They mirror your content structure and cascade down through subdirectories.
**Example structure:**
```
templates/
├── base.html # Required: Main page wrapper
├── __error.html # Optional: Custom error page
├── __file.md.html # Template for all markdown files
├── __folder.md.html # Template for folders with markdown
├── __folder.image.html # Template for image galleries
├── blog/
│ └── __file.md.html # Custom template for blog posts
└── photos/
└── __folder.image.html # Custom gallery template for photos
```
**Key template files:**
- `base.html` - **REQUIRED** - Wraps all pages (header, navigation, footer)
- `__file.{category}.html` - Templates for individual files
- `__folder.{category}.html` - Templates for folder views
- `__error.html` - Custom error pages (404, etc.)
### styles/ - CSS Stylesheets
Stylesheets follow the same cascading logic as templates.
**Example structure:**
```
styles/
├── base.css # Required: Base styles
├── __file.md.css # Styles for markdown files
├── __folder.image.css # Styles for image galleries
├── blog/
│ └── __file.md.css # Blog-specific styles
└── layouts/
├── document.css # Layout styles
└── gallery.css # Gallery layout styles
```
## File Naming Conventions
### Content Files
- **Regular names**: `about.md`, `contact.md`, `my-post.md`
- **Hidden files**: Prefix with `___` to hide from navigation
- `___draft-post.md` - Won't appear in listings
- `___notes.md` - Private notes not rendered
### Template Files
Templates use a special naming pattern:
- `__file.{extension}.html` - For individual files
- `__file.md.html` - All markdown files
- `__file.jpg.html` - Individual images (rare)
- `__folder.{category}.html` - For folders
- `__folder.md.html` - Folders containing mostly markdown
- `__folder.image.html` - Photo gallery folders
- `{specific-name}.html` - For specific pages
- `index.html` - Only for index.md
- `about.html` - Only for about.md
### Style Files
Styles follow the same pattern as templates:
- `base.css` - Always loaded
- `__file.md.css` - Loaded for markdown files
- `__folder.image.css` - Loaded for image galleries
- `{specific-path}.css` - Loaded for specific pages
## How File Categories Work
Foldsite automatically categorizes files by extension:
| Category | Extensions | Template Pattern | Use Case |
|----------|-----------|------------------|----------|
| **document** | `.md`, `.txt`, `.html` | `__file.document.html` | Articles, pages |
| **image** | `.jpg`, `.png`, `.gif`, `.svg` | `__file.image.html` | Photos |
| **multimedia** | `.mp4`, `.mp3`, `.webm` | `__file.multimedia.html` | Videos, audio |
| **other** | Everything else | `__file.other.html` | PDFs, downloads |
Folders are categorized by their **most common file type**:
```
photos/ # Mostly images → "image" category
IMG_001.jpg
IMG_002.jpg
notes.txt # Ignored for categorization
blog/ # Mostly markdown → "document" category
post-1.md
post-2.md
header.jpg # Ignored for categorization
```
## Template and Style Discovery
Foldsite uses a **hierarchical discovery system**. When rendering a page, it searches for templates from most specific to most general.
### Example: Rendering `content/blog/my-post.md`
**Template search order:**
1. `templates/blog/my-post.html` - Specific file template
2. `templates/blog/__file.md.html` - Blog folder markdown template
3. `templates/blog/__file.document.html` - Blog folder document template
4. `templates/__file.md.html` - Root markdown template
5. `templates/__file.document.html` - Root document template
6. `templates/__file.html` - Generic file template
**First match wins.**
### Example: Rendering `content/photos/vacation/` folder
**Template search order:**
1. `templates/photos/vacation/__folder.html` - Specific folder template
2. `templates/photos/__folder.image.html` - Photo folder image template
3. `templates/__folder.image.html` - Root image folder template
4. `templates/__folder.html` - Generic folder template
**Style discovery works the same way**, building a list of all matching styles from most specific to most general.
## Configuration File
The `config.toml` file tells Foldsite where your directories are:
```toml
[paths]
content_dir = "/path/to/content"
templates_dir = "/path/to/templates"
styles_dir = "/path/to/styles"
[server]
listen_address = "0.0.0.0"
listen_port = 8081
admin_browser = false
admin_password = "change-me"
max_threads = 4
debug = false
access_log = true
```
### Key Configuration Options
**paths:**
- `content_dir` - Absolute path to content folder
- `templates_dir` - Absolute path to templates folder
- `styles_dir` - Absolute path to styles folder
**server:**
- `listen_address` - IP to bind to (`0.0.0.0` for all interfaces)
- `listen_port` - Port number (default 8081)
- `admin_browser` - Enable file manager UI (true/false)
- `admin_password` - Password for file manager (if enabled)
- `max_threads` - Number of worker threads
- `debug` - Enable debug mode (shows template discovery)
- `access_log` - Log HTTP requests
## Special Files and Folders
### Hidden Content (`___` prefix)
Files and folders starting with three underscores are ignored:
```
content/
├── public-post.md # ✓ Visible
├── ___draft-post.md # ✗ Hidden
├── blog/ # ✓ Visible
│ └── article.md
└── ___notes/ # ✗ Hidden (entire folder)
└── ideas.md
```
Use this for:
- Draft posts
- Private notes
- Work-in-progress content
- Template testing
### Index Files
`index.md` files serve as folder landing pages:
```
content/
├── index.md # Homepage: /
└── blog/
├── index.md # Blog index: /blog or /blog/
├── post-1.md # Post: /blog/post-1.md
└── post-2.md # Post: /blog/post-2.md
```
Without an `index.md`, folders display using the `__folder.*` template.
## Practical Examples
### Minimal Setup
Simplest possible Foldsite:
```
my-site/
├── content/
│ └── index.md
├── templates/
│ ├── base.html
│ └── __file.md.html
└── styles/
└── base.css
```
### Blog Site
Typical blog structure:
```
blog-site/
├── content/
│ ├── index.md # Homepage
│ ├── about.md # About page
│ └── posts/
│ ├── 2024-01-post.md
│ ├── 2024-02-post.md
│ └── ___draft.md # Hidden draft
├── templates/
│ ├── base.html
│ ├── __file.md.html # Default post template
│ ├── __folder.md.html # Post listing
│ └── index.html # Custom homepage
└── styles/
├── base.css
├── __file.md.css
└── __folder.md.css
```
### Photo Portfolio
Photography site structure:
```
portfolio/
├── content/
│ ├── index.md
│ └── galleries/
│ ├── landscape/
│ │ ├── IMG_001.jpg
│ │ └── IMG_002.jpg
│ ├── portrait/
│ │ └── photos...
│ └── urban/
│ └── photos...
├── templates/
│ ├── base.html
│ ├── __folder.image.html # Gallery template
│ └── galleries/
│ └── __folder.image.html # Custom for galleries
└── styles/
├── base.css
└── __folder.image.css
```
### Documentation Site
Hierarchical documentation:
```
docs-site/
├── content/
│ ├── index.md
│ ├── getting-started/
│ │ ├── installation.md
│ │ ├── configuration.md
│ │ └── first-steps.md
│ ├── guides/
│ │ ├── basics.md
│ │ └── advanced.md
│ └── api/
│ ├── overview.md
│ └── reference.md
├── templates/
│ ├── base.html
│ ├── __file.md.html
│ └── __folder.md.html
└── styles/
├── base.css
└── layouts/
└── document.css
```
## Best Practices
### 1. Organize Content Logically
Your folder structure should make sense to humans:
```
✓ Good:
content/
├── blog/
├── projects/
└── about.md
✗ Confusing:
content/
├── stuff/
├── things/
└── misc/
```
### 2. Use Descriptive Names
File and folder names become URLs and navigation labels:
```
✓ Good:
getting-started.md
python-tutorial.md
2024-year-review.md
✗ Poor:
page1.md
doc.md
tmp.md
```
### 3. Keep Templates DRY
Don't duplicate templates. Use inheritance and the cascade:
```
✓ Good:
templates/
├── base.html # Shared structure
├── __file.md.html # All markdown files
└── blog/
└── __file.md.html # Only blog-specific changes
✗ Redundant:
templates/
├── base.html
├── about.html # Unnecessary if using __file.md.html
├── contact.html # Unnecessary if using __file.md.html
└── ...
```
### 4. Use Hidden Files for Work in Progress
Keep drafts and private notes out of production:
```
content/
├── published-post.md
├── ___draft-post.md # Hidden
└── ___notes/ # Hidden folder
└── ideas.md
```
## Next Steps
Now that you understand the directory structure:
- [Learn about Templates](templates/) - Master the template system
- [Explore Styles](styles/) - Understand CSS cascading
- [See Recipes](recipes/) - Ready-to-use examples
- [Deploy Your Site](deployment/) - Get it online
The directory structure is the foundation of Foldsite. Master it, and everything else becomes intuitive.

356
docs/content/explore.md Normal file
View File

@ -0,0 +1,356 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Explore Foldsites"
description: "Real-world Foldsite examples and inspiration"
summary: "Discover websites built with Foldsite - from personal blogs to photo portfolios and documentation sites."
quick_tips:
- "Real sites provide the best learning examples"
- "View source to see how they're built"
- "Steal ideas (with attribution) and make them your own"
---
# Explore Foldsites
See what people are building with Foldsite. These real-world examples demonstrate different approaches, designs, and use cases.
## Featured Sites
### Personal Sites & Blogs
#### Tanishq Dubey's Site
**URL:** [https://tanishq.page](https://tanishq.page)
**Type:** Personal blog + photo gallery
**Highlights:**
- Clean, minimal design
- Mix of blog posts and photo galleries
- Breadcrumb navigation
- Responsive layout
**What you can learn:**
- How to combine blog and gallery in one site
- Sidebar navigation pattern
- Photo organization by folders
---
### Documentation Sites
#### Foldsite Documentation
**URL:** (This site!)
**Type:** Product documentation
**Highlights:**
- Hierarchical content organization
- Sibling page navigation
- Code examples with syntax highlighting
- Comprehensive frontmatter usage
**What you can learn:**
- Documentation site structure
- Template organization
- Content hierarchy best practices
---
## Community Showcase
*This section is community-curated. Add your site below!*
### How to Add Your Site
Built something with Foldsite? Share it with the community!
**To add your site:**
1. Fork the Foldsite repository
2. Edit `docs/content/explore.md`
3. Add your site using this template:
```markdown
#### Your Site Name
**URL:** https://your-site.com
**Type:** Blog / Portfolio / Gallery / Docs / Other
**Highlights:**
- Key feature 1
- Key feature 2
- Key feature 3
**What makes it special:**
Brief description of unique aspects or interesting implementation details.
```
4. Submit a pull request
**Guidelines:**
- Your site must be publicly accessible
- Must be built with Foldsite
- Keep description concise
- No commercial promotion (personal/hobby sites only)
- Family-friendly content
---
## Inspiration Gallery
### Blog Designs
**Minimalist Blog**
- Clean typography
- Lots of whitespace
- Focus on content
- Tag-based navigation
**Magazine Style**
- Grid layout
- Featured images
- Multi-column
- Category sections
**Tech Blog**
- Syntax highlighting
- Code-focused
- Dark mode
- Technical diagrams
### Photo Galleries
**Travel Photography**
- Location-based organization
- EXIF data display
- Lightbox viewing
- Map integration (possible)
**Portfolio Site**
- Project-based galleries
- About/contact pages
- Custom landing page
- Client testimonials
**Photo Blog**
- Mix of photos and text
- Chronological posts
- Photo series
- Behind-the-scenes content
### Documentation Styles
**API Reference**
- Code examples
- Parameter tables
- Return value documentation
- Search functionality
**User Guide**
- Step-by-step tutorials
- Screenshots/diagrams
- Prerequisites sections
- Troubleshooting guides
**Knowledge Base**
- FAQ style
- Search by category
- Related articles
- Quick answers
## Design Patterns
Common patterns seen across successful Foldsites:
### Navigation
**Sidebar Navigation** (Like example_site)
```
┌─────────────┬──────────────────────────┐
│ │ │
│ Sidebar │ Main Content │
│ Nav │ │
│ │ │
│ - Home │ Page content here... │
│ - About │ │
│ - Blog │ │
│ - Photos │ │
│ │ │
└─────────────┴──────────────────────────┘
```
**Top Navigation**
```
┌────────────────────────────────────────┐
│ Logo Home About Blog Contact │
├────────────────────────────────────────┤
│ │
│ Main Content │
│ │
└────────────────────────────────────────┘
```
**Breadcrumb Navigation**
```
Home / Blog / 2024 / My Post
────────────────────────────────────────
Post Content
````
### Layouts
**Single Column**
- Best for reading
- Focus on content
- Mobile-friendly by default
**Two Column**
- Content + sidebar
- Related posts/navigation
- Additional information
**Grid**
- Photo galleries
- Post previews
- Portfolio items
## Learning from Examples
### View Source
All Foldsites are just HTML, CSS, and markdown. View source to learn:
1. **Right-click → View Page Source** - See rendered HTML
2. **Check `/styles/` URLs** - View CSS files
3. **Look at URL structure** - Understand content organization
### Clone and Experiment
For open source Foldsites:
```bash
# Clone repository
git clone https://github.com/user/their-site.git
# Copy their templates
cp -r their-site/templates my-site/templates
# Customize and make it your own
```
### Adaptation Guidelines
**Do:**
- Study patterns and techniques
- Adapt ideas to your needs
- Give credit for inspiration
- Make it your own
**Don't:**
- Copy entire sites wholesale
- Use someone else's content
- Steal unique designs exactly
- Claim others' work as yours
## Themes & Templates
### Official Examples
**example_site/** (in repository)
- Typical blog/gallery site
- Shows common patterns
- Breadcrumb navigation
- Sidebar layout
### Community Themes
*Coming soon! Community-contributed themes will be listed here.*
Want to contribute a theme? See [Theme Gallery](theme-gallery.md).
## Case Studies
### From Idea to Site
**Case Study 1: Personal Blog**
**Goal:** Simple blog to share thoughts
**Time:** 2 hours
**Approach:**
1. Started with `example_site` templates
2. Customized colors and fonts
3. Added personal branding
4. Deployed to GitHub Pages
**Lessons learned:**
- Start simple, iterate
- Templates cascade saves time
- Custom homepage makes it special
---
**Case Study 2: Photography Portfolio**
**Goal:** Showcase travel photography
**Time:** 4 hours
**Approach:**
1. Organized photos by location/trip
2. Used `__folder.image.html` template
3. Added EXIF data display
4. Integrated lightbox library
**Lessons learned:**
- Folder structure = site structure
- EXIF data adds context
- Thumbnail generation is automatic
---
**Case Study 3: Project Documentation**
**Goal:** Document open source project
**Time:** 6 hours
**Approach:**
1. Mirrored code structure in content
2. Created hierarchical docs
3. Added code examples
4. Set up sibling navigation
**Lessons learned:**
- Mirror mental model in folders
- Breadcrumbs essential for deep hierarchies
- Code blocks need good syntax highlighting
## Tips from the Community
> "Start with one template and iterate. Don't try to build everything at once."
> — Early Foldsite user
> "The folder structure IS the site structure. Once I understood that, everything clicked."
> — Documentation writer
> "Use hidden files (`___`) for drafts. Game changer for my workflow."
> — Blogger
> "Template helpers like `get_recent_posts()` saved me so much time. No database needed!"
> — Former WordPress user
## Your Site Here
Built something with Foldsite? We'd love to feature it!
**Submission criteria:**
- Publicly accessible
- Built with Foldsite
- Demonstrates interesting pattern or design
- Well-executed (doesn't have to be perfect!)
**How to submit:**
1. Open issue on GitHub with "Showcase" label
2. Include URL, description, and what makes it special
3. We'll review and add to this page
## Explore More
- **[Recipes](recipes/)** - Template code you can copy
- **[Theme Gallery](theme-gallery.md)** - Downloadable themes
- **[Templates Guide](templates/)** - Learn the system
- **[Support](support.md)** - Get help building your site
**Ready to build?** [Get Started](index.md#quick-start)
*Remember: Every great Foldsite started as an empty folder. Your site could be featured here next!*

View File

@ -1,3 +1,165 @@
# Foldsite Documentation
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Foldsite Documentation"
description: "Turn your folders into beautiful websites with zero configuration"
summary: "Welcome to Foldsite - a modern static/dynamic site generator that transforms your file structure into a navigable website. Focus on content, not configuration."
quick_tips:
- "Your folder structure IS your site structure - no complex routing needed"
- "Templates cascade automatically - create them where you need them"
- "Start with just markdown files - add templates and styles later"
---
Foldsite acts as a dynamic site generator. It takes content primarily from Markdown files, combines it with HTML templates, applies CSS styles, and serves the resulting pages. It supports features like image thumbnails, Markdown rendering with frontmatter, and a built-in file manager for administrative tasks. Foldsite is dynamic in the sense that content is rendered on demand, rather than generating static HTML up-front.
# Welcome to Foldsite
**Foldsite** is a static/dynamic site generator that lets you focus on what matters: **your content**. Drop markdown files into folders, add templates for customization, and Foldsite handles the rest.
> *"It's your Internet. Take it back."*
> — [DWS (Dubey Web Services)](https://dws.rip)
## What Makes Foldsite Different?
### Folders → Site Structure
Your directory layout becomes your website structure automatically. No routing configuration, no complex build steps. Create a folder, drop in a markdown file, and it's live.
```
content/
├── about.md → /about
├── blog/
│ ├── post-1.md → /blog/post-1.md
│ └── post-2.md → /blog/post-2.md
└── photos/
└── vacation/ → /photos/vacation
```
### Template System That Makes Sense
Templates cascade through your directory tree. Create specific templates for individual files, or general templates that apply to entire sections:
- `__file.md.html` - Template for all markdown files
- `__folder.md.html` - Template for folders containing markdown
- `__folder.image.html` - Template for photo galleries
- Custom templates for specific pages
### Powerful Helper Functions
Access content dynamically from your templates using built-in Jinja2 helpers:
```jinja
{# List recent blog posts #}
{% for post in get_recent_posts(limit=5, folder='blog') %}
<a href="/{{ post.path }}">{{ post.title }}</a>
{% endfor %}
{# Show navigation breadcrumbs #}
{% for crumb in generate_breadcrumbs(currentPath) %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% endfor %}
```
## Quick Start
### 1. Install and Run
```bash
# Clone the repository
git clone https://github.com/DWSresearch/foldsite
cd foldsite
# Install dependencies
pip install -r requirements.txt
# Run the development server
python main.py --config config.toml
```
Visit `http://localhost:8081` to see your site!
### 2. Create Your First Page
```bash
# Create a content directory
mkdir -p my-site/content
# Write your first page
echo "# Hello World" > my-site/content/index.md
```
### 3. Customize with Templates
```bash
# Create a basic template structure
mkdir -p my-site/templates my-site/styles
# Add a base template (see Templates section for examples)
```
## Common Use Cases
### Personal Blog
Perfect for sharing your thoughts with automatic post discovery, tagging, and chronological sorting.
### Photo Gallery
Built-in image handling with EXIF data extraction, thumbnail generation, and gallery views.
### Documentation Site
Hierarchical content organization with automatic navigation and sibling page discovery.
### Portfolio Site
Showcase projects with flexible templates that adapt to your content type.
## Documentation Sections
### [About Foldsite](about.md)
Learn about the philosophy behind Foldsite and why it was created.
### [Directory Structure](directory-structure.md)
Understanding how to organize your content, templates, and styles.
### [Deployment](deployment/)
Get your Foldsite running locally, in Docker, or production environments.
### [Templates](templates/)
Master the template system - from basics to advanced hierarchical templates.
### [Styles](styles/)
Learn how CSS cascades through your site structure.
### [Template Recipes](recipes/)
Ready-to-use examples for blogs, galleries, documentation sites, and more.
### [Theme Gallery](theme-gallery.md)
Explore community-created themes and templates.
### [Explore Foldsites](explore.md)
See real-world examples of Foldsite in action.
### [Develop Foldsite](develop/)
Contributing to Foldsite development.
### [Support](support.md)
Get help and connect with the community.
## Why Foldsite Exists
Foldsite is part of the **DWS (Dubey Web Services)** mission to help people reclaim their corner of the internet. In an era of complex CMSs and heavy frameworks, Foldsite brings simplicity back:
- **Own your content** - Just files and folders on your filesystem
- **Control your presentation** - Templates and styles that make sense
- **Host anywhere** - Static files or dynamic Python server
- **Zero lock-in** - Your markdown works everywhere
## Philosophy
1. **Content is king** - Your folders and files are the source of truth
2. **Convention over configuration** - Sensible defaults, customize when needed
3. **Progressive enhancement** - Start simple, add complexity only where needed
4. **Developer friendly** - Clear APIs, helpful error messages, debug tools
## Next Steps
- **New users**: Start with [Directory Structure](directory-structure.md) to understand the basics
- **Building a blog**: Jump to [Blog Site Recipe](recipes/blog-site.md)
- **Creating themes**: Read the [Templates Guide](templates/)
- **Deploying**: Check [Deployment Options](deployment/)
Ready to turn your folders into a website? Let's get started!

View File

@ -0,0 +1,682 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Template Recipes"
description: "Ready-to-use examples for common Foldsite patterns"
summary: "Complete, working examples for building blogs, photo galleries, documentation sites, and more with Foldsite."
quick_tips:
- "All recipes are complete and ready to use - just copy and customize"
- "Recipes demonstrate real-world patterns used in production Foldsites"
- "Mix and match components from different recipes"
---
# Template Recipes
Ready-to-use templates and patterns for common Foldsite use cases. Copy these examples and customize them for your needs.
## Recipe Collection
### Blog Site
Complete blog setup with recent posts, tag filtering, and related content.
### Photo Gallery
Beautiful image galleries with EXIF data, breadcrumbs, and responsive layout.
### Documentation Site
Hierarchical documentation with navigation, breadcrumbs, and sibling pages.
### Portfolio Site
Project showcase with custom layouts per project.
---
## Recipe 1: Personal Blog
A complete blog with post listings, tags, and related posts.
### Directory Structure
```
my-blog/
├── content/
│ ├── index.md
│ ├── about.md
│ └── posts/
│ ├── 2024-01-15-first-post.md
│ ├── 2024-02-20-python-tips.md
│ └── 2024-03-10-web-development.md
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── __file.md.html
│ └── posts/
│ ├── __file.md.html
│ └── __folder.md.html
└── styles/
├── base.css
└── posts/
└── __file.md.css
```
### Post Frontmatter Format
```markdown
---
title: "My First Blog Post"
date: "2024-01-15"
author: "Your Name"
tags: ["python", "tutorial", "beginners"]
description: "A brief introduction to Python for beginners"
---
# Your content here...
```
### Template: `base.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if metadata and metadata.title %}{{ metadata.title }} - {% endif %}My Blog</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
<header>
<nav class="main-nav">
<a href="/" class="logo">My Blog</a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/posts">Posts</a>
<a href="/about.md">About</a>
</div>
</nav>
</header>
<main class="container">
{{ content|safe }}
</main>
<footer>
<p>&copy; 2025 My Blog. Built with <a href="https://github.com/DWSresearch/foldsite">Foldsite</a>.</p>
</footer>
</body>
</html>
```
### Template: `index.html` (Homepage)
```html
<div class="homepage">
<section class="hero">
<h1>Welcome to My Blog</h1>
<p>Thoughts on coding, life, and everything in between.</p>
</section>
<section class="recent-posts">
<h2>Recent Posts</h2>
<div class="post-grid">
{% for post in get_recent_posts(limit=6, folder='posts') %}
<article class="post-card">
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<time datetime="{{ post.date }}">{{ post.date }}</time>
{% if post.metadata.description %}
<p class="excerpt">{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags[:3] %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
<a href="/posts" class="view-all">View All Posts →</a>
</section>
<aside class="sidebar">
<section class="popular-tags">
<h3>Popular Tags</h3>
<div class="tag-cloud">
{% for tag in get_all_tags()|sort(attribute='count', reverse=True)[:15] %}
<a href="/tags/{{ tag.name|lower }}.md"
class="tag"
style="font-size: {{ 0.9 + (tag.count * 0.15) }}rem;">
{{ tag.name }}
</a>
{% endfor %}
</div>
</section>
</aside>
</div>
```
### Template: `posts/__file.md.html` (Blog Post)
```html
<article class="blog-post">
<header class="post-header">
{% if metadata %}
<h1>{{ metadata.title }}</h1>
<div class="post-meta">
<time datetime="{{ metadata.date }}">{{ metadata.date }}</time>
{% if metadata.author %}
<span class="author">by {{ metadata.author }}</span>
{% endif %}
</div>
{% if metadata.tags %}
<div class="post-tags">
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
{% endif %}
</header>
<div class="post-content">
{{ content|safe }}
</div>
<footer class="post-footer">
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<section class="related-posts">
<h3>Related Posts</h3>
<div class="related-grid">
{% for post in related %}
<a href="{{ post.url }}" class="related-card">
<h4>{{ post.title }}</h4>
<time>{{ post.date }}</time>
</a>
{% endfor %}
</div>
</section>
{% endif %}
<nav class="post-navigation">
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
{% set current_index = namespace(value=-1) %}
{% for idx, (name, path) in enumerate(siblings) %}
{% if path == currentPath %}
{% set current_index.value = idx %}
{% endif %}
{% endfor %}
{% if current_index.value > 0 %}
{% set prev = siblings[current_index.value - 1] %}
<a href="/{{ prev[1] }}" class="nav-prev">← {{ prev[0].replace('.md', '') }}</a>
{% endif %}
{% if current_index.value < siblings|length - 1 %}
{% set next = siblings[current_index.value + 1] %}
<a href="/{{ next[1] }}" class="nav-next">{{ next[0].replace('.md', '') }} →</a>
{% endif %}
{% endif %}
</nav>
</footer>
</article>
```
### Template: `posts/__folder.md.html` (Post Index)
```html
<div class="post-index">
<header class="index-header">
<h1>All Posts</h1>
<p>{{ get_recent_posts(limit=1000, folder='posts')|length }} posts and counting</p>
</header>
<div class="post-list">
{% for post in get_recent_posts(limit=100, folder='posts') %}
<article class="post-item">
<time datetime="{{ post.date }}">{{ post.date }}</time>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
{% if post.metadata.description %}
<p class="description">{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags %}
<a href="/tags/{{ tag|lower }}.md" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
</div>
```
---
## Recipe 2: Photo Gallery
Beautiful, responsive photo galleries with EXIF data extraction.
### Directory Structure
```
photo-site/
├── content/
│ ├── index.md
│ └── galleries/
│ ├── vacation-2024/
│ │ ├── IMG_001.jpg
│ │ ├── IMG_002.jpg
│ │ └── IMG_003.jpg
│ └── family/
│ └── photos...
├── templates/
│ ├── base.html
│ └── galleries/
│ └── __folder.image.html
└── styles/
├── base.css
└── galleries/
└── __folder.image.css
```
### Template: `galleries/__folder.image.html`
```html
<div class="photo-gallery">
<header class="gallery-header">
<nav class="breadcrumbs">
{% for crumb in generate_breadcrumbs(currentPath) %}
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% else %}
<span>{{ crumb.title }}</span>
{% endif %}
{% if not loop.last %} / {% endif %}
{% endfor %}
</nav>
<h1>{{ currentPath.split('/')[-1]|replace('-', ' ')|title }}</h1>
{% set photos = get_folder_contents(currentPath)|selectattr('categories', 'contains', 'image') %}
<p class="photo-count">{{ photos|list|length }} photos</p>
{# Show sibling galleries #}
{% set sibling_folders = get_sibling_content_folders(currentPath) %}
{% if sibling_folders %}
<nav class="gallery-nav">
<h3>Other Galleries:</h3>
{% for name, path in sibling_folders %}
<a href="/{{ path }}">{{ name|replace('-', ' ')|title }}</a>
{% endfor %}
</nav>
{% endif %}
</header>
<div class="photo-grid">
{% for photo in photos|list|sort(attribute='date_created', reverse=True) %}
<figure class="photo-item">
<a href="/download/{{ photo.path }}" class="photo-link">
<img src="/download/{{ photo.path }}?max_width=800"
alt="{{ photo.name }}"
loading="lazy"
width="{{ photo.metadata.width if photo.metadata else 800 }}"
height="{{ photo.metadata.height if photo.metadata else 600 }}">
</a>
{% if photo.metadata and photo.metadata.exif %}
<figcaption class="photo-caption">
{% if photo.metadata.exif.DateTimeOriginal %}
<time>{{ photo.metadata.exif.DateTimeOriginal }}</time>
{% endif %}
{% if photo.metadata.exif.Model %}
<span class="camera">{{ photo.metadata.exif.Model }}</span>
{% endif %}
{% if photo.metadata.exif.LensModel %}
<span class="lens">{{ photo.metadata.exif.LensModel }}</span>
{% endif %}
</figcaption>
{% endif %}
</figure>
{% endfor %}
</div>
</div>
{# Optional: Include a lightbox library #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.js"></script>
<script>
const gallery = new Viewer(document.querySelector('.photo-grid'), {
toolbar: true,
title: true,
navbar: true,
});
</script>
```
### Styles: `galleries/__folder.image.css`
```css
.photo-gallery {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.gallery-header {
margin-bottom: 3rem;
text-align: center;
}
.breadcrumbs {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.breadcrumbs a {
color: #0066cc;
text-decoration: none;
}
.photo-count {
color: #888;
font-size: 0.95rem;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.photo-item {
position: relative;
margin: 0;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.photo-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.photo-link {
display: block;
line-height: 0;
}
.photo-item img {
width: 100%;
height: auto;
display: block;
}
.photo-caption {
padding: 0.75rem;
background: white;
font-size: 0.85rem;
color: #666;
}
.photo-caption time {
display: block;
margin-bottom: 0.25rem;
}
.gallery-nav {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #eee;
}
.gallery-nav a {
display: inline-block;
margin: 0.5rem;
padding: 0.5rem 1rem;
background: #f5f5f5;
border-radius: 4px;
text-decoration: none;
color: #333;
}
.gallery-nav a:hover {
background: #e5e5e5;
}
@media (max-width: 768px) {
.photo-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
}
```
---
## Recipe 3: Documentation Site
Hierarchical documentation with navigation and search.
### Directory Structure
```
docs-site/
├── content/
│ ├── index.md
│ ├── getting-started/
│ │ ├── installation.md
│ │ ├── configuration.md
│ │ └── quickstart.md
│ ├── guides/
│ │ ├── basics.md
│ │ └── advanced.md
│ └── api/
│ ├── overview.md
│ └── reference.md
├── templates/
│ ├── base.html
│ ├── index.html
│ └── __file.md.html
└── styles/
└── base.css
```
### Template: `__file.md.html` (Documentation Page)
```html
<div class="docs-layout">
<aside class="docs-sidebar">
<nav class="sidebar-nav">
<h3>Documentation</h3>
<section class="nav-section">
<h4>Getting Started</h4>
<ul>
<li><a href="/getting-started/installation.md">Installation</a></li>
<li><a href="/getting-started/configuration.md">Configuration</a></li>
<li><a href="/getting-started/quickstart.md">Quick Start</a></li>
</ul>
</section>
<section class="nav-section">
<h4>Guides</h4>
<ul>
<li><a href="/guides/basics.md">Basics</a></li>
<li><a href="/guides/advanced.md">Advanced</a></li>
</ul>
</section>
<section class="nav-section">
<h4>API Reference</h4>
<ul>
<li><a href="/api/overview.md">Overview</a></li>
<li><a href="/api/reference.md">Reference</a></li>
</ul>
</section>
</nav>
{# Show sibling pages automatically #}
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
<nav class="page-nav">
<h4>On This Page:</h4>
<ul>
{% for name, path in siblings %}
<li>
<a href="/{{ path }}"
{% if path == currentPath %}class="active"{% endif %}>
{{ name.replace('.md', '')|replace('-', ' ')|title }}
</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</aside>
<main class="docs-content">
<nav class="breadcrumbs">
{% for crumb in generate_breadcrumbs(currentPath) %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% if not loop.last %} {% endif %}
{% endfor %}
</nav>
<article class="documentation">
{% if metadata and metadata.title %}
<h1>{{ metadata.title }}</h1>
{% endif %}
{{ content|safe }}
</article>
<footer class="docs-footer">
{# Previous/Next navigation #}
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings|length > 1 %}
<nav class="pagination">
{% set current_index = namespace(value=-1) %}
{% for idx in range(siblings|length) %}
{% if siblings[idx][1] == currentPath %}
{% set current_index.value = idx %}
{% endif %}
{% endfor %}
{% if current_index.value > 0 %}
{% set prev = siblings[current_index.value - 1] %}
<a href="/{{ prev[1] }}" class="prev">
← {{ prev[0].replace('.md', '')|replace('-', ' ')|title }}
</a>
{% endif %}
{% if current_index.value >= 0 and current_index.value < siblings|length - 1 %}
{% set next = siblings[current_index.value + 1] %}
<a href="/{{ next[1] }}" class="next">
{{ next[0].replace('.md', '')|replace('-', ' ')|title }} →
</a>
{% endif %}
</nav>
{% endif %}
{# Related pages based on tags #}
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<section class="related-docs">
<h4>Related Documentation:</h4>
<ul>
{% for doc in related %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
</section>
{% endif %}
</footer>
</main>
</div>
```
---
## Recipe 4: Portfolio Site
Showcase projects with custom layouts.
### Template: Portfolio Homepage
```html
<div class="portfolio">
<section class="hero">
<h1>{{ metadata.title if metadata else "My Portfolio" }}</h1>
<p class="tagline">{{ metadata.description if metadata else "Designer & Developer" }}</p>
</section>
<section class="projects">
<h2>Featured Projects</h2>
<div class="project-grid">
{% for folder_name, folder_path in get_sibling_content_folders('projects') %}
{% set project_files = get_folder_contents(folder_path) %}
{% set cover_image = project_files|selectattr('categories', 'contains', 'image')|first %}
<a href="/{{ folder_path }}" class="project-card">
{% if cover_image %}
<img src="/download/{{ cover_image.path }}?max_width=600"
alt="{{ folder_name }}">
{% endif %}
<h3>{{ folder_name|replace('-', ' ')|title }}</h3>
</a>
{% endfor %}
</div>
</section>
</div>
```
---
## Mixing Recipes
You can combine elements from different recipes:
### Blog + Gallery
Add photo galleries to a blog site by including the gallery template in your blog's template directory.
### Documentation + Blog
Add a `/blog` section to documentation by including blog templates alongside docs templates.
### Portfolio + Blog
Showcase projects and share thoughts by combining portfolio and blog patterns.
## Customization Tips
1. **Start with one recipe** - Get it working before adding complexity
2. **Modify styles first** - Change colors, fonts, spacing to match your brand
3. **Adjust layouts gradually** - Start with structure, refine as you go
4. **Add features incrementally** - Don't try to implement everything at once
## Next Steps
- **[Template System](../templates/)** - Understanding how templates work
- **[Template Helpers](../templates/template-helpers.md)** - Complete API reference
- **[Styles Guide](../styles/)** - Styling your site
- **[Explore Foldsites](../explore.md)** - See real examples
Copy these recipes, make them your own, and build something amazing!

View File

@ -0,0 +1,770 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Styles Guide"
description: "Understanding Foldsite's CSS cascade system"
summary: "Learn how CSS styles cascade through your Foldsite project - from base styles to page-specific customizations."
quick_tips:
- "base.css is required and loaded on every page"
- "All matching styles are loaded (unlike templates where first match wins)"
- "Styles cascade down through directory structure like templates"
---
# Styles Guide
Foldsite's style system follows the same hierarchical logic as templates, allowing you to organize CSS in a maintainable, scalable way.
## The Style System
### Key Differences from Templates
| Templates | Styles |
|-----------|--------|
| **First match wins** | **All matches load** |
| One template per page | Multiple stylesheets per page |
| Must have at least one | base.css required |
**Why the difference?**
CSS is designed to cascade and layer. Loading multiple stylesheets allows you to:
- Share base styles
- Add section-specific styles
- Override with page-specific styles
## Required Style: base.css
Every Foldsite project **must have** `styles/base.css`.
This file is **loaded on every page**, providing:
- Typography
- Layout basics
- Color scheme
- Resets/normalizes
### Minimal base.css
```css
/* styles/base.css */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
line-height: 1.2;
}
a {
color: #0066cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
height: auto;
}
```
## Style Discovery
Styles follow a **hierarchical discovery pattern** similar to templates.
### For File: `blog/my-post.md`
**All matching styles load (in order):**
1. **Base style** (always)
```
/styles/base.css
```
2. **Type + extension** styles (from root to specific)
```
/styles/__file.md.css
/styles/blog/__file.md.css
```
3. **Type + category** styles
```
/styles/__file.document.css
/styles/blog/__file.document.css
```
4. **Specific file** style
```
/styles/blog/my-post.md.css
```
**All found styles are included!**
### Rendered HTML
```html
<link rel="stylesheet" href="/styles/base.css">
<link rel="stylesheet" href="/styles/__file.md.css">
<link rel="stylesheet" href="/styles/blog/__file.md.css">
```
## Style Naming Patterns
### File Styles
Pattern: `__file.{extension}.css` or `__file.{category}.css`
```
styles/
├── __file.md.css # All markdown files
├── __file.document.css # All document files
├── __file.image.css # Individual images (rare)
└── __file.other.css # Other file types
```
### Folder Styles
Pattern: `__folder.{category}.css`
```
styles/
├── __folder.md.css # Folders with markdown
├── __folder.image.css # Photo galleries
└── __folder.html # Any folder view
```
### Specific Page Styles
Pattern: `{path/to/file}.css`
```
styles/
├── index.md.css # Only homepage
├── about.md.css # Only about page
└── blog/
└── special-post.md.css # One specific post
```
## Directory Structure
### Basic Structure
```
styles/
├── base.css # Required: Base styles
├── __file.md.css # Markdown file styles
└── __folder.image.css # Gallery styles
```
### Advanced Structure
```
styles/
├── base.css # Base
├── __file.md.css # General markdown
├── __folder.image.css # General galleries
├── blog/
│ ├── __file.md.css # Blog posts
│ └── __folder.md.css # Blog index
├── docs/
│ └── __file.md.css # Documentation
├── layouts/
│ ├── document.css # Document layout
│ ├── gallery.css # Gallery layout
│ └── landing.css # Landing pages
└── components/
├── navigation.css # Navigation
├── footer.css # Footer
└── breadcrumbs.css # Breadcrumbs
```
## Cascade & Specificity
### CSS Cascade Order
**Load order matters:**
1. `base.css` - Loaded first
2. General styles (`__file.md.css`)
3. Section styles (`blog/__file.md.css`)
4. Specific styles (`blog/my-post.md.css`)
**Later styles override earlier ones** (standard CSS behavior).
### Example Cascade
**Given page:** `blog/tutorial.md`
**Loaded styles:**
```html
<link rel="stylesheet" href="/styles/base.css">
<link rel="stylesheet" href="/styles/__file.md.css">
<link rel="stylesheet" href="/styles/blog/__file.md.css">
```
**base.css:**
```css
h1 {
color: black; /* Default */
}
```
**__file.md.css:**
```css
h1 {
color: #333; /* Slightly lighter */
}
```
**blog/__file.md.css:**
```css
h1 {
color: #0066cc; /* Blue - wins! */
}
```
**Result:** Blog headings are blue.
## Common Patterns
### Pattern 1: Base + Overrides
Start with comprehensive base, override as needed:
**base.css** - Everything
```css
/* Typography */
body { font-family: sans-serif; }
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
/* Layout */
.container { max-width: 1200px; }
/* Components */
nav { /* navigation styles */ }
footer { /* footer styles */ }
```
**blog/__file.md.css** - Blog-specific
```css
/* Override heading colors for blog */
h1 { color: #0066cc; }
/* Add blog-specific components */
.post-meta { /* metadata styles */ }
```
### Pattern 2: Modular Components
Split styles into reusable modules:
**base.css** - Minimal
```css
@import url('components/typography.css');
@import url('components/layout.css');
@import url('components/navigation.css');
```
**components/typography.css**
```css
body {
font-family: Georgia, serif;
line-height: 1.6;
}
h1 { font-size: 2.5rem; }
/* ... */
```
**components/navigation.css**
```css
nav {
display: flex;
justify-content: space-between;
}
/* ... */
```
### Pattern 3: Layout Variants
Different layouts for different sections:
**layouts/document.css**
```css
.document-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
}
.docs-sidebar { /* sidebar styles */ }
.docs-content { /* content styles */ }
```
**layouts/gallery.css**
```css
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.photo-item { /* photo card styles */ }
```
**Include in templates:**
```html
<!-- __file.md.html for docs -->
<link rel="stylesheet" href="/styles/layouts/document.css">
<div class="document-layout">
<!-- content -->
</div>
```
### Pattern 4: Responsive Design
Mobile-first approach:
```css
/* base.css - Mobile first */
body {
padding: 1rem;
}
.container {
display: block;
}
/* Tablet */
@media (min-width: 768px) {
body {
padding: 2rem;
}
.container {
display: grid;
grid-template-columns: 1fr 300px;
}
}
/* Desktop */
@media (min-width: 1200px) {
body {
padding: 3rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
}
```
## Practical Examples
### Example 1: Blog Site
```
styles/
├── base.css # Site-wide styles
├── __file.md.css # General markdown
├── __folder.md.css # Folder listings
└── blog/
├── __file.md.css # Blog posts
└── __folder.md.css # Blog index
```
**base.css:**
```css
/* Basic layout and typography */
body {
font-family: Georgia, serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
a { color: #0066cc; }
```
**__file.md.css:**
```css
/* Default markdown styles */
article {
margin: 2rem 0;
}
code {
background: #f4f4f4;
padding: 0.2em 0.4em;
border-radius: 3px;
}
pre {
background: #f4f4f4;
padding: 1rem;
overflow-x: auto;
}
```
**blog/__file.md.css:**
```css
/* Blog post specific */
.post-header {
border-bottom: 2px solid #eee;
padding-bottom: 1rem;
margin-bottom: 2rem;
}
.post-meta {
color: #666;
font-size: 0.9rem;
}
.post-tags {
margin-top: 2rem;
}
.tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #f0f0f0;
border-radius: 3px;
margin-right: 0.5rem;
font-size: 0.85rem;
}
```
**blog/__folder.md.css:**
```css
/* Blog index */
.post-list {
list-style: none;
padding: 0;
}
.post-item {
margin: 2rem 0;
padding: 1.5rem;
border: 1px solid #eee;
border-radius: 4px;
}
.post-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
```
### Example 2: Photo Gallery
```
styles/
├── base.css
└── galleries/
└── __folder.image.css
```
**galleries/__folder.image.css:**
```css
.photo-gallery {
padding: 2rem;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.photo-item {
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.photo-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.photo-item img {
width: 100%;
height: auto;
display: block;
}
.photo-caption {
padding: 0.75rem;
background: white;
font-size: 0.85rem;
color: #666;
}
/* Responsive */
@media (max-width: 768px) {
.photo-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
}
```
### Example 3: Documentation Site
```
styles/
├── base.css
├── layouts/
│ └── document.css
└── docs/
└── __file.md.css
```
**layouts/document.css:**
```css
.docs-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 3rem;
max-width: 1400px;
margin: 0 auto;
}
.docs-sidebar {
position: sticky;
top: 2rem;
height: calc(100vh - 4rem);
overflow-y: auto;
}
.docs-content {
min-width: 0; /* Prevent grid blowout */
}
@media (max-width: 1024px) {
.docs-layout {
grid-template-columns: 1fr;
}
.docs-sidebar {
position: static;
height: auto;
}
}
```
**docs/__file.md.css:**
```css
.documentation {
max-width: 800px;
}
/* Table of contents */
.doc-toc {
background: #f8f8f8;
padding: 1rem;
border-radius: 4px;
margin: 2rem 0;
}
/* Code blocks */
.documentation pre {
background: #282c34;
color: #abb2bf;
padding: 1.5rem;
border-radius: 6px;
overflow-x: auto;
}
.documentation code {
font-family: 'Monaco', 'Courier New', monospace;
}
/* Callouts */
.note,
.warning,
.tip {
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-left: 4px solid;
border-radius: 4px;
}
.note {
background: #e3f2fd;
border-color: #2196f3;
}
.warning {
background: #fff3e0;
border-color: #ff9800;
}
.tip {
background: #e8f5e9;
border-color: #4caf50;
}
```
## CSS Variables
Use CSS custom properties for theming:
### base.css with Variables
```css
:root {
/* Colors */
--color-primary: #0066cc;
--color-secondary: #6c757d;
--color-text: #333;
--color-background: #fff;
--color-border: #dee2e6;
/* Typography */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
--font-serif: Georgia, serif;
--font-mono: 'Monaco', 'Courier New', monospace;
/* Spacing */
--space-xs: 0.5rem;
--space-sm: 1rem;
--space-md: 2rem;
--space-lg: 3rem;
/* Breakpoints (for reference) */
/* Use in @media queries */
}
body {
color: var(--color-text);
background: var(--color-background);
font-family: var(--font-sans);
}
a {
color: var(--color-primary);
}
/* ... */
```
### Dark Mode
```css
/* base.css */
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e4e4e4;
--color-background: #1a1a1a;
--color-border: #333;
}
}
/* Or toggle with class */
body.dark-mode {
--color-text: #e4e4e4;
--color-background: #1a1a1a;
--color-border: #333;
}
```
## Performance Tips
### 1. Minimize File Size
```css
/* Remove unnecessary spaces/newlines in production */
/* Use a CSS minifier */
```
### 2. Avoid @import
```css
/* Slow - additional HTTP request */
@import url('components/typography.css');
/* Better - combine files or use build tool */
/* Or let browser load multiple <link> tags in parallel */
```
### 3. Optimize Selectors
```css
/* Fast */
.class-name { }
#id-name { }
/* Slower */
div > ul > li > a { }
[data-attribute="value"] { }
/* Use classes for styling */
```
### 4. Use Will-Change Sparingly
```css
/* Only for elements that will actually animate */
.photo-item {
transition: transform 0.2s;
}
.photo-item:hover {
transform: translateY(-4px);
will-change: transform; /* Hint to browser */
}
```
## Debugging Styles
### Browser DevTools
1. **Inspect element** - Right-click → Inspect
2. **Check computed styles** - See which rules apply
3. **See cascade** - Understand override order
4. **Live edit** - Test changes instantly
### Debug Checklist
**Styles not loading:**
- [ ] File exists in `styles/` directory
- [ ] Filename matches expected pattern
- [ ] No syntax errors in CSS
- [ ] Browser cache cleared
**Styles not applying:**
- [ ] Check CSS specificity
- [ ] Check cascade order
- [ ] Look for typos in selectors
- [ ] Verify HTML classes match CSS
**Wrong styles applying:**
- [ ] Check for conflicting rules
- [ ] Verify file loading order
- [ ] Look for !important (avoid if possible)
## Next Steps
- **[Templates Guide](../templates/)** - Templates use these styles
- **[Template Discovery](../templates/template-discovery.md)** - How styles are discovered
- **[Recipes](../recipes/)** - Complete examples with CSS
Master the style system to create beautiful, maintainable Foldsites!

314
docs/content/support.md Normal file
View File

@ -0,0 +1,314 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Support & Community"
description: "Get help with Foldsite"
summary: "Find help, report issues, and connect with the Foldsite community."
quick_tips:
- "Check documentation first - most questions are answered here"
- "Search existing GitHub issues before opening new ones"
- "Share your Foldsite creations with the community"
---
# Support & Community
Need help with Foldsite? Here's how to get assistance and connect with others.
## Documentation
**Start here first!** Most questions are answered in the documentation:
- **[Home](index.md)** - Overview and quick start
- **[Directory Structure](directory-structure.md)** - Understanding the basics
- **[Templates](templates/)** - Template system guide
- **[Template Helpers](templates/template-helpers.md)** - Complete API reference
- **[Deployment](deployment/)** - Getting your site running
- **[Recipes](recipes/)** - Working examples
## Common Issues
### Template Not Found
**Error:** `Exception: Base template not found`
**Solution:** Ensure `base.html` exists in your templates directory:
```bash
ls templates/base.html
```
### Module Not Found
**Error:** `ModuleNotFoundError: No module named 'flask'`
**Solution:** Install dependencies:
```bash
pip install -r requirements.txt
```
### Port Already in Use
**Error:** `OSError: [Errno 48] Address already in use`
**Solution:** Change port in `config.toml`:
```toml
[server]
listen_port = 8082 # Use different port
```
Or stop the process using port 8081:
```bash
# Find process
lsof -i :8081
# Kill it
kill -9 <PID>
```
### Images Not Loading
**Problem:** Images show broken links
**Solution:** Use the `/download/` route for serving files:
```html
<!-- Correct -->
<img src="/download/{{ photo.path }}">
<!-- Wrong -->
<img src="/{{ photo.path }}">
```
### Template Changes Not Appearing
**Problem:** Modified templates don't show changes
**Solutions:**
1. Check you're editing the correct file
2. Clear browser cache (Cmd/Ctrl + Shift + R)
3. Restart the Foldsite server
4. Check `config.toml` points to correct templates directory
## GitHub Repository
**Repository:** [https://github.com/DWSresearch/foldsite](https://github.com/DWSresearch/foldsite)
### Reporting Bugs
Found a bug? Please report it on GitHub Issues.
**Before reporting:**
1. Search existing issues to avoid duplicates
2. Make sure you're using the latest version
3. Try to reproduce with minimal example
**Good bug report includes:**
- Foldsite version
- Python version
- Operating system
- Clear steps to reproduce
- Expected vs. actual behavior
- Error messages (full stack traces)
- Minimal example code if possible
**Example:**
```markdown
## Bug Report
**Foldsite version:** 1.0.0
**Python version:** 3.11.5
**OS:** macOS 14.0
### Steps to Reproduce
1. Create template with `{{ get_recent_posts() }}`
2. Run `python main.py --config config.toml`
3. Visit homepage
### Expected
Should show 5 recent posts
### Actual
Raises `AttributeError: 'NoneType' object has no attribute 'metadata'`
### Stack Trace
```
[paste full error here]
```
```
### Feature Requests
Have an idea for improvement? Open a feature request!
**Good feature request includes:**
- Clear description of the feature
- Use case (why you need it)
- Proposed implementation (if you have ideas)
- Examples from other tools (if applicable)
### Pull Requests
Contributions welcome! See [Develop Foldsite](develop/) for guidelines.
## Community
### Show & Tell
Share what you've built with Foldsite:
- Post in GitHub Discussions
- Tag `#foldsite` on social media
- Add your site to [Explore Foldsites](explore.md)
### Discussions
Have questions or want to chat? Use GitHub Discussions:
- **Q&A** - Ask questions
- **Show and Tell** - Share your site
- **Ideas** - Discuss potential features
- **General** - Everything else
## Getting Help
### Before Asking
1. **Read the docs** - Your answer might be here
2. **Search issues** - Someone might have asked already
3. **Check examples** - See working code in `/example_site`
4. **Enable debug mode** - See what Foldsite is doing:
```toml
[server]
debug = true
```
### How to Ask
**Good questions include:**
- What you're trying to achieve
- What you've tried
- Relevant code snippets
- Error messages
**Example:**
> I'm trying to show recent posts on my homepage, but `get_recent_posts()` returns an empty list.
>
> My content structure:
> ```
> content/
> ├── index.md
> └── blog/
> ├── post1.md
> └── post2.md
> ```
>
> My template code:
> ```jinja
> {% for post in get_recent_posts(limit=5) %}
> {{ post.title }}
> {% endfor %}
> ```
>
> Posts have frontmatter with `title` and `date` fields. What am I missing?
### Response Times
Foldsite is maintained by volunteers. Please be patient:
- **Bugs** - Usually addressed within a few days
- **Features** - May take longer depending on complexity
- **Questions** - Community often responds quickly
## Contributing
Want to help make Foldsite better?
### Ways to Contribute
- **Documentation** - Fix typos, clarify confusing sections, add examples
- **Bug fixes** - Fix issues and submit pull requests
- **Features** - Implement new functionality
- **Templates** - Create and share themes
- **Testing** - Test new releases, report issues
- **Community** - Help others in discussions
See [Develop Foldsite](develop/) for detailed contribution guidelines.
## Professional Support
Need dedicated help with Foldsite?
**[DWS (Dubey Web Services)](https://dws.rip)** may offer consulting for complex implementations. Contact through the website for availability and rates.
## Security Issues
Found a security vulnerability? **Do not open a public issue.**
Email security@dws.rip with:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if you have one)
We'll respond as quickly as possible and coordinate disclosure.
## Learning Resources
### Jinja2 (Template Engine)
Foldsite uses Jinja2 for templates:
- **[Official Docs](https://jinja.palletsprojects.com/)** - Complete Jinja2 reference
- **[Template Designer Docs](https://jinja.palletsprojects.com/en/3.1.x/templates/)** - Template syntax
### Markdown
Content is written in Markdown:
- **[CommonMark](https://commonmark.org/)** - Markdown specification
- **[Markdown Guide](https://www.markdownguide.org/)** - Beginner-friendly guide
### Python (For Development)
If you want to contribute to Foldsite:
- **[Python Tutorial](https://docs.python.org/3/tutorial/)** - Official Python tutorial
- **[Flask Docs](https://flask.palletsprojects.com/)** - Web framework used by Foldsite
## Acknowledgments
Foldsite is built with:
- **[Flask](https://flask.palletsprojects.com/)** - Web framework
- **[Jinja2](https://jinja.palletsprojects.com/)** - Template engine
- **[mistune](https://github.com/lepture/mistune)** - Markdown parser
- **[python-frontmatter](https://github.com/eyeseast/python-frontmatter)** - Frontmatter parsing
- **[Pillow](https://python-pillow.org/)** - Image processing
- **[Gunicorn](https://gunicorn.org/)** - WSGI HTTP server
Thank you to all contributors and users!
## Stay Updated
- **Watch** the GitHub repository for updates
- **Star** the repo if you find Foldsite useful
- **Fork** to experiment with your own changes
## Philosophy
Foldsite is part of the **DWS mission**: *"It's your Internet. Take it back."*
We believe in:
- **Simple tools** that solve real problems
- **User ownership** of content and presentation
- **Open source** collaboration
- **Privacy** and control
## Next Steps
- **[Explore Foldsites](explore.md)** - See what others have built
- **[Develop Foldsite](develop/)** - Contributing guidelines
- **[Recipes](recipes/)** - Working examples to learn from
**Still stuck?** Don't hesitate to ask for help. The Foldsite community is here to support you!

View File

@ -0,0 +1,578 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Template System"
description: "Master Foldsite's powerful cascading template system"
summary: "Learn how Foldsite's hierarchical template discovery works - from simple defaults to sophisticated, context-aware layouts."
quick_tips:
- "base.html is required and wraps every page on your site"
- "Templates cascade from specific to general - Foldsite uses the first match"
- "Use Jinja2 syntax for dynamic content and logic"
---
# Template System
Foldsite's template system is both powerful and intuitive. Templates are HTML files with **Jinja2** syntax that define how your content is displayed.
## The Template Hierarchy
Foldsite uses a **cascading template discovery system**. When rendering a page, it searches for templates from **most specific** to **most general**, using the first match found.
###Understanding Template Priority
Think of it like CSS specificity - more specific selectors override general ones:
```
Specific file > Type + Extension > Type + Category > Generic type
```
### Example: Rendering `content/blog/my-post.md`
Foldsite searches in this order:
1. `templates/blog/my-post.html` ← Exact file match
2. `templates/blog/__file.md.html` ← Markdown files in blog/
3. `templates/blog/__file.document.html` ← Document files in blog/
4. `templates/__file.md.html` ← All markdown files
5. `templates/__file.document.html` ← All document files
6. `templates/__file.html` ← Any file
7. **First match wins!**
## Required Template: base.html
**Every Foldsite project MUST have a `base.html`** in the templates root. This wraps every page on your site.
### Minimal base.html
```html
<!DOCTYPE html>
<html>
<head>
<title>My Site</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
{{ content|safe }}
</body>
</html>
```
### Available Variables in base.html
- `{{ content|safe }}` - Rendered page content (required)
- `{{ styles }}` - List of CSS files to load
- `{{ currentPath }}` - Current page path
- `{{ metadata }}` - Frontmatter from markdown files
- All template helper functions
### Complete base.html Example
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if metadata and metadata.title %}{{ metadata.title }} - {% endif %}My Site</title>
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
{% for item in get_navigation_items() %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
</nav>
</header>
<main>
{{ content|safe }}
</main>
<footer>
<p>&copy; 2025 My Site</p>
</footer>
</body>
</html>
```
## Template Naming Patterns
### File Templates
Templates for individual files use this pattern:
**Pattern:** `__file.{extension}.html` or `__file.{category}.html`
**Examples:**
- `__file.md.html` - All markdown files
- `__file.document.html` - All document types (md, txt, html)
- `__file.image.html` - Individual image pages (rare)
- `__file.jpg.html` - Specific to JPG files
### Folder Templates
Templates for directory views:
**Pattern:** `__folder.{category}.html`
**Examples:**
- `__folder.md.html` - Folders containing mostly markdown
- `__folder.image.html` - Photo gallery folders
- `__folder.html` - Generic folder view
### Specific Page Templates
Override for specific pages:
**Pattern:** `{filename}.html`
**Examples:**
- `index.html` - Only for index.md
- `about.html` - Only for about.md
- `contact.html` - Only for contact.md
## File Categories
Foldsite automatically categorizes files by extension:
| Category | Extensions | Template | Use Case |
|----------|-----------|----------|----------|
| **document** | `.md`, `.txt` | `__file.document.html` | Text content |
| **image** | `.jpg`, `.png`, `.gif` | `__file.image.html` | Photos |
| **multimedia** | `.mp4`, `.mp3` | `__file.multimedia.html` | Video/audio |
| **other** | Everything else | `__file.other.html` | Downloads |
Files can have **multiple categories**. For example, `.md` files are both `md` and `document`.
## Template Variables
Every template receives these variables:
### Always Available
- `content` - Rendered HTML content
- `styles` - List of CSS file paths
- `currentPath` - Path relative to content root
- `metadata` - Frontmatter dict (for markdown files)
### Markdown Files Only
For `.md` files, `metadata` contains frontmatter:
```markdown
---
title: "My Blog Post"
date: "2025-01-15"
author: "Your Name"
tags: ["python", "web"]
---
# Content here...
```
Access in templates:
```jinja
<h1>{{ metadata.title }}</h1>
<time>{{ metadata.date }}</time>
<p>By {{ metadata.author }}</p>
{% for tag in metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
```
## Template Helpers
Foldsite provides powerful helper functions accessible in all templates:
### Content Discovery
```jinja
{# Get folder contents #}
{% for file in get_folder_contents(currentPath) %}
<a href="/{{ file.path }}">{{ file.name }}</a>
{% endfor %}
{# Get sibling files #}
{% for sibling in get_sibling_content_files(currentPath) %}
<a href="/{{ sibling[1] }}">{{ sibling[0] }}</a>
{% endfor %}
{# Get sibling folders #}
{% for folder in get_sibling_content_folders(currentPath) %}
<a href="/{{ folder[1] }}">{{ folder[0] }}</a>
{% endfor %}
```
### Blog Functions
```jinja
{# Recent blog posts #}
{% for post in get_recent_posts(limit=5, folder='blog') %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.date }}</time>
</article>
{% endfor %}
{# Posts by tag #}
{% for post in get_posts_by_tag('python', limit=10) %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{# All tags #}
{% for tag in get_all_tags() %}
<a href="/tags/{{ tag.name }}">{{ tag.name }} ({{ tag.count }})</a>
{% endfor %}
```
### Navigation
```jinja
{# Breadcrumbs #}
{% for crumb in generate_breadcrumbs(currentPath) %}
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% else %}
<span>{{ crumb.title }}</span>
{% endif %}
{% if not loop.last %} / {% endif %}
{% endfor %}
{# Navigation menu #}
{% for item in get_navigation_items(max_items=10) %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
{# Related posts #}
{% for post in get_related_posts(currentPath, limit=3) %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
```
See [Template Helpers Reference](template-helpers.md) for complete documentation.
## Cascading Through Directories
Templates cascade down through your directory structure. Place templates in subdirectories to override for specific sections.
### Example Structure
```
templates/
├── base.html # Site-wide wrapper
├── __file.md.html # Default for all markdown
├── __folder.image.html # Default for galleries
├── blog/
│ ├── __file.md.html # Override for blog posts
│ └── __folder.md.html # Override for blog index
└── photos/
└── __folder.image.html # Override for photo galleries
```
### How It Works
**Rendering `content/blog/my-post.md`:**
1. Looks in `templates/blog/` first
2. Finds `blog/__file.md.html`**Uses this**
3. Never checks root `__file.md.html`
**Rendering `content/projects/project.md`:**
1. Looks in `templates/projects/` first
2. Doesn't find specific template
3. Falls back to `templates/__file.md.html`**Uses this**
## Practical Examples
### Simple Blog Post Template
`templates/__file.md.html`:
```html
<article>
{% if metadata %}
<header>
<h1>{{ metadata.title }}</h1>
<time datetime="{{ metadata.date }}">{{ metadata.date }}</time>
{% if metadata.author %}
<p class="author">By {{ metadata.author }}</p>
{% endif %}
</header>
{% endif %}
<div class="content">
{{ content|safe }}
</div>
{% if metadata and metadata.tags %}
<footer>
<p>Tags:
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}">#{{ tag }}</a>
{% endfor %}
</p>
</footer>
{% endif %}
</article>
```
### Blog Index Template
`templates/__folder.md.html`:
```html
<div class="blog-index">
<h1>Recent Posts</h1>
{% for post in get_recent_posts(limit=10, folder=currentPath) %}
<article class="post-preview">
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.date }}</time>
{% if post.metadata.description %}
<p>{{ post.metadata.description }}</p>
{% endif %}
</article>
{% endfor %}
</div>
```
### Photo Gallery Template
`templates/__folder.image.html`:
```html
<div class="gallery">
{% set breadcrumbs = currentPath.split('/') %}
<nav class="breadcrumbs">
<a href="/">Home</a>
{% for i in range(breadcrumbs|length) %}
{% if i+1 == breadcrumbs|length %}
/ <span>{{ breadcrumbs[i] }}</span>
{% else %}
/ <a href="/{{ '/'.join(breadcrumbs[:i+1]) }}">{{ breadcrumbs[i] }}</a>
{% endif %}
{% endfor %}
</nav>
<div class="photos">
{% for photo in get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
{% if 'image' in photo.categories %}
<a href="/download/{{ photo.path }}" class="photo">
<img src="/download/{{ photo.path }}?max_width=400"
alt="{{ photo.name }}"
loading="lazy">
</a>
{% endif %}
{% endfor %}
</div>
</div>
```
## Error Pages
Custom error template: `templates/__error.html`
```html
<div class="error-page">
<h1>Error {{ error_code }}</h1>
<p>{{ error_message }}</p>
<p>{{ error_description }}</p>
<a href="/">Return Home</a>
</div>
```
Variables available:
- `error_code` - HTTP status code (404, 500, etc.)
- `error_message` - Short message ("Not Found")
- `error_description` - Detailed description
## Jinja2 Syntax Quick Reference
### Variables
```jinja
{{ variable }}
{{ metadata.title }}
{{ post.url }}
```
### Filters
```jinja
{{ content|safe }} {# Don't escape HTML #}
{{ title|upper }} {# Uppercase #}
{{ date|default('Unknown') }} {# Default value #}
{{ items|length }} {# Count items #}
```
### Conditionals
```jinja
{% if metadata %}
<h1>{{ metadata.title }}</h1>
{% endif %}
{% if metadata and metadata.title %}
...
{% elif metadata %}
...
{% else %}
...
{% endif %}
```
### Loops
```jinja
{% for item in items %}
<p>{{ item }}</p>
{% endfor %}
{% for key, value in metadata.items() %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
{# Loop variables #}
{% for item in items %}
{{ loop.index }} {# 1-indexed #}
{{ loop.index0 }} {# 0-indexed #}
{{ loop.first }} {# True on first iteration #}
{{ loop.last }} {# True on last iteration #}
{% endfor %}
```
### Filters and Functions
```jinja
{% for file in get_folder_contents()|sort(attribute='date') %}
...
{% endfor %}
{% for post in get_recent_posts(limit=5)|reverse %}
...
{% endfor %}
```
## Best Practices
### 1. Start Simple, Add Complexity as Needed
Begin with basic templates:
```
templates/
├── base.html
├── __file.md.html
└── __folder.md.html
```
Add specific overrides only when you need different styling or layout.
### 2. Keep base.html Minimal
Your base template should handle:
- HTML document structure
- CSS loading
- Site-wide navigation
- Footer
Leave content-specific layouts to page templates.
### 3. Use Template Helpers
Don't manually read files or iterate directories. Use helpers:
```jinja
✓ Good:
{% for post in get_recent_posts(limit=5) %}
✗ Bad:
{# Trying to manually list files - won't work #}
```
### 4. Leverage the Cascade
Put general templates at the root, specific ones in subdirectories:
```
templates/
├── __file.md.html # Default for all markdown
└── blog/
└── __file.md.html # Special layout for blog
```
### 5. Test with Debug Mode
Enable debug mode in `config.toml` to see template discovery:
```toml
[server]
debug = true
```
This shows which templates Foldsite considered and why it chose the one it did.
## Common Patterns
### Pattern: Site Navigation
In `base.html`:
```html
<nav>
<a href="/">Home</a>
{% for item in get_navigation_items() %}
<a href="{{ item.url }}"
{% if currentPath == item.path %}class="active"{% endif %}>
{{ item.title }}
</a>
{% endfor %}
</nav>
```
### Pattern: Sidebar with Recent Posts
```html
<aside>
<h3>Recent Posts</h3>
<ul>
{% for post in get_recent_posts(limit=5) %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
<small>{{ post.date }}</small>
</li>
{% endfor %}
</ul>
</aside>
```
### Pattern: Tag Cloud
```html
<div class="tag-cloud">
{% for tag in get_all_tags() %}
<a href="/tags/{{ tag.name|lower }}"
style="font-size: {{ 0.8 + (tag.count * 0.1) }}em;">
{{ tag.name }}
</a>
{% endfor %}
</div>
```
## Next Steps
- **[Template Discovery](templates/template-discovery.md)** - Deep dive into how templates are found
- **[Template Helpers Reference](templates/template-helpers.md)** - Complete API documentation
- **[Template Recipes](../recipes/)** - Ready-to-use template examples
- **[Styles Guide](../styles/)** - Styling your templates
Master the template system, and you can build any type of site with Foldsite!

View File

@ -0,0 +1,558 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Template Discovery System"
description: "Understanding how Foldsite finds and chooses templates"
summary: "Deep dive into Foldsite's hierarchical template discovery algorithm - learn exactly how templates are matched to content."
quick_tips:
- "Templates are searched from most specific to most general"
- "First match wins - more specific templates override general ones"
- "Templates cascade down through directory hierarchies"
---
# Template Discovery System
Understanding how Foldsite discovers and chooses templates is key to building sophisticated sites. This guide explains the complete template resolution algorithm.
## The Discovery Algorithm
When Foldsite renders a page, it follows a **hierarchical search pattern** to find the best template.
### Core Principle
**Specificity wins.** More specific templates override general ones.
Think of it like CSS specificity:
```
#specific-id (most specific)
.class-name
element (least specific)
```
In Foldsite:
```
my-post.html (most specific - exact file)
__file.md.html (category + extension)
__file.document.html (category only)
__file.html (least specific - any file)
```
## File Type Detection
Before template discovery, Foldsite determines the content type:
### For Files
**Step 1:** Extract extension
```
blog/my-post.md → extension: "md"
```
**Step 2:** Map to categories
```
"md" → categories: ["document", "md"]
```
**Category mapping:**
```python
GENERIC_FILE_MAPPING = {
"document": ["md", "txt", "html"],
"image": ["png", "jpg", "jpeg", "gif", "svg"],
"multimedia": ["mp4", "mp3", "webm"],
"other": [...] # Everything else
}
```
### For Folders
**Step 1:** Count file types in folder
```
photos/vacation/
IMG_001.jpg → jpg: 1
IMG_002.jpg → jpg: 2
IMG_003.jpg → jpg: 3
notes.txt → txt: 1
```
**Step 2:** Most common type wins
```
Most common: jpg (3 files)
→ categories: ["image", "jpg"]
```
## Template Search Order
### For Files: `blog/my-post.md`
**Given:**
- Path: `blog/my-post.md`
- Type: `file`
- Categories: `["document", "md"]`
- Extension: `md`
**Search order:**
1. **Exact file match** in current directory
```
templates/blog/my-post.html
```
2. **Type + extension** in current directory
```
templates/blog/__file.md.html
```
3. **Type + category** in current directory (most specific category first)
```
templates/blog/__file.document.html
```
4. **Generic type** in current directory
```
templates/blog/__file.html
```
5. **Move up one directory**, repeat steps 2-4
```
templates/__file.md.html
templates/__file.document.html
templates/__file.html
```
6. **Default template** (if exists)
```
templates/default.html
```
**First match wins!**
### For Folders: `photos/vacation/`
**Given:**
- Path: `photos/vacation/`
- Type: `folder`
- Categories: `["image"]` (most files are images)
**Search order:**
1. **Exact folder match**
```
templates/photos/vacation/__folder.html
```
2. **Type + category** in current directory
```
templates/photos/__folder.image.html
```
3. **Generic folder** in current directory
```
templates/photos/__folder.html
```
4. **Move up one directory**, repeat steps 2-3
```
templates/__folder.image.html
templates/__folder.html
```
5. **Default template**
```
templates/default.html
```
## Practical Examples
### Example 1: Blog Post
**Content:** `content/blog/posts/2024-my-post.md`
**Template search:**
```
1. templates/blog/posts/2024-my-post.html ✗ Not found
2. templates/blog/posts/__file.md.html ✗ Not found
3. templates/blog/posts/__file.document.html ✗ Not found
4. templates/blog/posts/__file.html ✗ Not found
5. templates/blog/__file.md.html ✓ FOUND!
```
**Result:** Uses `templates/blog/__file.md.html`
**Why:** Most specific template found in the hierarchy.
### Example 2: Homepage
**Content:** `content/index.md`
**Template search:**
```
1. templates/index.html ✓ FOUND!
```
**Result:** Uses `templates/index.html`
**Why:** Exact file match (most specific possible).
### Example 3: Photo Gallery
**Content:** `content/photos/vacation/` (folder with 50 JPG files)
**Categories:** `["image"]`
**Template search:**
```
1. templates/photos/vacation/__folder.html ✗ Not found
2. templates/photos/__folder.image.html ✓ FOUND!
```
**Result:** Uses `templates/photos/__folder.image.html`
**Why:** Type + category match in parent directory.
### Example 4: Deep Nesting
**Content:** `content/docs/guides/advanced/testing.md`
**Template search:**
```
1. templates/docs/guides/advanced/testing.html ✗ Not found
2. templates/docs/guides/advanced/__file.md.html ✗ Not found
3. templates/docs/guides/__file.md.html ✗ Not found
4. templates/docs/__file.md.html ✓ FOUND!
```
**Result:** Uses `templates/docs/__file.md.html`
**Why:** Climbs directory tree until finding a match.
## Template Cascade
Templates **cascade down** through directories:
```
templates/
├── __file.md.html ← Default for ALL markdown
└── blog/
└── __file.md.html ← Override for blog/ only
```
**Rendering `content/about.md`:**
- Uses `templates/__file.md.html`
**Rendering `content/blog/post.md`:**
- Uses `templates/blog/__file.md.html` (more specific)
### Multi-Level Cascade
```
templates/
├── __file.md.html ← Level 1: Site default
├── blog/
│ └── __file.md.html ← Level 2: Blog default
└── blog/
└── tutorials/
└── __file.md.html ← Level 3: Tutorial-specific
```
**Effect:**
- `content/about.md` → Level 1 template
- `content/blog/news.md` → Level 2 template
- `content/blog/tutorials/python.md` → Level 3 template
## Category Priority
Files can belong to multiple categories. **More specific categories come first.**
### Category Hierarchy
For `my-post.md`:
```
Categories: ["md", "document"]
↑ ↑
specific general
```
**Search order:**
```
1. __file.md.html ← Most specific
2. __file.document.html ← More general
3. __file.html ← Most general
```
## Debug Mode
Enable debug mode to see template discovery in action:
```toml
# config.toml
[server]
debug = true
```
**Console output when visiting a page:**
```
[DEBUG] Template discovery for: blog/my-post.md
[DEBUG] Content type: file
[DEBUG] Categories: ['md', 'document']
[DEBUG] Extension: md
[DEBUG]
[DEBUG] Searching templates:
[DEBUG] 1. templates/blog/my-post.html - NOT FOUND
[DEBUG] 2. templates/blog/__file.md.html - FOUND!
[DEBUG]
[DEBUG] Using template: templates/blog/__file.md.html
```
### Debug Test Page
Create a test page to understand discovery:
```markdown
---
title: "Template Discovery Test"
---
# Current Template Info
**Current Path:** {{ currentPath }}
**Styles Loaded:**
{% for style in styles %}
- {{ style }}
{% endfor %}
**Metadata:**
{{ metadata }}
```
Visit this page and check the console to see which template was used.
## Edge Cases
### No Template Found
If no template matches:
**For files:**
- Foldsite serves the file directly (download)
- Useful for PDFs, images, etc.
**For folders:**
- Returns 404 error
- Need at least one `__folder.html` template
### Hidden Files
Files/folders starting with `___` are ignored:
```
content/
├── post.md ✓ Will be rendered
└── ___draft.md ✗ Ignored completely
```
No template search is performed for hidden content.
### Multiple Extensions
Only the last extension is considered:
```
my-file.tar.gz → extension: "gz"
```
Not: `tar.gz` or `tar`
## Style Discovery
Styles follow **similar logic** to templates but accumulate instead of first-match:
### Style Search for `blog/post.md`
**All matching styles are loaded (in order):**
```
1. /base.css ← Always loaded
2. /blog/__file.md.css ← If exists
3. /blog/__file.document.css ← If exists
4. /__file.md.css ← If exists (from root)
5. /blog/post.md.css ← If exists (specific file)
```
**All found styles are included** (not just first match).
**Rendering:**
```html
<link rel="stylesheet" href="/styles/base.css">
<link rel="stylesheet" href="/styles/blog/__file.md.css">
<link rel="stylesheet" href="/styles/__file.md.css">
```
## Optimization Tips
### 1. Keep Templates at Appropriate Levels
**Good:**
```
templates/
├── __file.md.html ← General template
└── blog/
└── __file.md.html ← Blog-specific customization
```
**Avoid:**
```
templates/
├── post1.html
├── post2.html
├── post3.html ← Repetitive!
└── ...
```
### 2. Use Categories Effectively
**Good:**
```
templates/
├── __file.md.html ← For markdown
├── __folder.image.html ← For galleries
└── __file.document.html ← For all documents
```
**Avoid:**
```
templates/
├── __file.html ← Too generic
└── (nothing else)
```
### 3. Understand the Cascade
Place templates where they make sense:
**Project structure:**
```
content/
├── blog/ → Frequent posts, custom layout
├── docs/ → Technical docs, different layout
└── about.md → One-off pages
```
**Template structure:**
```
templates/
├── __file.md.html ← Default for one-off pages
├── blog/
│ └── __file.md.html ← Blog-specific
└── docs/
└── __file.md.html ← Docs-specific
```
## Common Patterns
### Pattern: Section-Specific Layouts
Different sections need different layouts:
```
templates/
├── base.html ← Shared wrapper
├── __file.md.html ← Default content
├── blog/
│ ├── __file.md.html ← Blog posts
│ └── __folder.md.html ← Blog index
├── portfolio/
│ ├── __file.md.html ← Project pages
│ └── __folder.md.html ← Portfolio grid
└── docs/
├── __file.md.html ← Documentation pages
└── __folder.md.html ← Docs index
```
### Pattern: Gradual Specialization
Start general, add specificity as needed:
**Phase 1: MVP**
```
templates/
├── base.html
└── __file.md.html
```
**Phase 2: Add Gallery**
```
templates/
├── base.html
├── __file.md.html
└── __folder.image.html ← New!
```
**Phase 3: Custom Blog**
```
templates/
├── base.html
├── __file.md.html
├── __folder.image.html
└── blog/
└── __file.md.html ← New!
```
### Pattern: Override Single Page
Override one specific page:
```
templates/
├── __file.md.html ← All markdown
└── index.html ← Special homepage
```
## Troubleshooting
### Template Not Being Used
**Check:**
1. **File name** - Is it exactly right?
2. **Location** - Is it in the right directory?
3. **Extension** - `.html`, not `.htm` or `.jinja`
4. **Debug mode** - What does the console say?
**Debug:**
```bash
# Enable debug
[server]
debug = true
# Restart and check console output
```
### Wrong Template Used
**Likely cause:** More specific template exists
**Example:**
```
Want: templates/__file.md.html
Using: templates/blog/__file.md.html ← More specific!
```
**Solution:** Update the more specific template, or remove it.
### Styles Not Loading
**Check:**
1. **Style file exists** in `styles/` directory
2. **Path matches** template expectations
3. **Browser dev tools** - Are styles being requested?
**Remember:** Unlike templates, **all matching styles load**.
## Next Steps
- **[Template System Overview](index.md)** - Basics of templates
- **[Template Helpers](template-helpers.md)** - Functions available in templates
- **[Recipes](../recipes/)** - Working examples
- **[Styles Guide](../styles/)** - CSS cascade system
Understanding template discovery gives you complete control over your site's presentation. Use it wisely!

View File

@ -0,0 +1,793 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Template Helper Functions"
description: "Complete reference for all Jinja2 helper functions in Foldsite"
summary: "Comprehensive API documentation for Foldsite's template helper functions - discover content, navigate your site, and build dynamic features."
quick_tips:
- "All helpers are automatically available in every template"
- "Helpers return Python objects you can loop over and filter"
- "Most helpers are cached for performance - safe to call multiple times"
---
# Template Helper Functions
Foldsite provides powerful helper functions you can use in any template. These functions access your content dynamically, enabling features like recent posts, navigation menus, breadcrumbs, and more.
## Content Discovery
### get_folder_contents(path)
Get all files and folders in a directory with rich metadata.
**Parameters:**
- `path` (str, optional) - Relative path from content root. Defaults to current directory.
**Returns:** List of `TemplateFile` objects with attributes:
- `name` (str) - Filename with extension
- `path` (str) - Relative path from content root
- `proper_name` (str) - Filename without extension
- `extension` (str) - File extension (`.md`, `.jpg`, etc.)
- `categories` (list[str]) - File categories (`['document']`, `['image']`, etc.)
- `date_modified` (str) - Last modified date (`YYYY-MM-DD`)
- `date_created` (str) - Creation date (`YYYY-MM-DD`)
- `size_kb` (int) - File size in kilobytes
- `metadata` (dict | None) - Markdown frontmatter or image EXIF data
- `dir_item_count` (int) - Number of items if it's a directory
- `is_dir` (bool) - True if it's a directory
**Example:**
```jinja
<h2>Files in this folder:</h2>
<ul>
{% for file in get_folder_contents(currentPath) %}
<li>
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
({{ file.size_kb }} KB, modified {{ file.date_modified }})
</li>
{% endfor %}
</ul>
```
**Sort and filter:**
```jinja
{# Sort by date, newest first #}
{% for file in get_folder_contents()|sort(attribute='date_created', reverse=True) %}
...
{% endfor %}
{# Filter to only documents #}
{% for file in get_folder_contents() %}
{% if 'document' in file.categories %}
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
{% endif %}
{% endfor %}
```
### get_sibling_content_files(path)
Get files in the same directory as the current page.
**Parameters:**
- `path` (str, optional) - Relative path. Defaults to current directory.
**Returns:** List of tuples `(filename, relative_path)`
**Example:**
```jinja
<nav class="sibling-nav">
<h3>Other pages in this section:</h3>
{% for name, path in get_sibling_content_files(currentPath) %}
<a href="/{{ path }}"
{% if path == currentPath %}class="active"{% endif %}>
{{ name }}
</a>
{% endfor %}
</nav>
```
### get_sibling_content_folders(path)
Get folders in the same directory as the current page.
**Parameters:**
- `path` (str, optional) - Relative path. Defaults to current directory.
**Returns:** List of tuples `(folder_name, relative_path)`
**Example:**
```jinja
<nav class="folder-nav">
<h3>Sections:</h3>
{% for name, path in get_sibling_content_folders(currentPath) %}
<a href="/{{ path }}">{{ name }}</a>
{% endfor %}
</nav>
```
### get_text_document_preview(path)
Get a preview (first 100 characters) of a text document.
**Parameters:**
- `path` (str) - Relative path to the document
**Returns:** String (first 100 characters of the file)
**Example:**
```jinja
{% for file in get_folder_contents() %}
{% if 'document' in file.categories %}
<article>
<h3><a href="/{{ file.path }}">{{ file.proper_name }}</a></h3>
<p>{{ get_text_document_preview(file.path) }}...</p>
</article>
{% endif %}
{% endfor %}
```
### get_rendered_markdown(path)
Get fully rendered markdown content without Jinja2 templating. Perfect for displaying markdown files (like `index.md`) within folder views or embedding content from one markdown file into a template.
**Parameters:**
- `path` (str) - Relative path to the markdown file
**Returns:** Dictionary with:
- `html` (str | None) - Rendered HTML content from markdown
- `metadata` (dict | None) - Frontmatter metadata from the markdown file
- `exists` (bool) - True if file was found and rendered successfully
**Example:**
```jinja
{# Display index.md in a folder view #}
{% set index_path = (currentPath + '/index.md') if currentPath else 'index.md' %}
{% set index = get_rendered_markdown(index_path) %}
{% if index.exists %}
<section class="folder-index">
{{ index.html | safe }}
{% if index.metadata.author %}
<p class="author">By {{ index.metadata.author }}</p>
{% endif %}
</section>
{% endif %}
```
**Use in folder templates:**
```jinja
{# Show index.md content at the top of a folder listing #}
<div class="folder-view">
{% set index = get_rendered_markdown(currentPath + '/index.md') %}
{% if index.exists %}
<div class="folder-introduction">
{{ index.html | safe }}
<hr>
</div>
{% endif %}
{# Then show other files in the folder #}
<div class="folder-contents">
{% for file in get_folder_contents(currentPath) %}
{% if file.name != 'index.md' %}
<a href="/{{ file.path }}">{{ file.proper_name }}</a>
{% endif %}
{% endfor %}
</div>
</div>
```
**Embed content from another file:**
```jinja
{# Include shared content from another markdown file #}
{% set about = get_rendered_markdown('about/team-bio.md') %}
{% if about.exists %}
<aside class="team-bio">
{{ about.html | safe }}
</aside>
{% endif %}
```
### get_markdown_metadata(path)
Get metadata (frontmatter) from a markdown file **without rendering the content**. Perfect for displaying static markdown metadata in any location - like showing post titles, descriptions, or custom fields without the overhead of rendering the full markdown.
**Parameters:**
- `path` (str) - Relative path to the markdown file
**Returns:** Dictionary with:
- `metadata` (dict | None) - Frontmatter metadata from the markdown file
- `exists` (bool) - True if file was found successfully
- `error` (str, optional) - Error message if reading failed
**Example:**
```jinja
{# Display metadata from a specific post without rendering it #}
{% set post_meta = get_markdown_metadata('blog/my-awesome-post.md') %}
{% if post_meta.exists %}
<div class="featured-post">
<h2>{{ post_meta.metadata.title }}</h2>
<p class="description">{{ post_meta.metadata.description }}</p>
<p class="date">Published: {{ post_meta.metadata.date }}</p>
<div class="tags">
{% for tag in post_meta.metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
<a href="/blog/my-awesome-post.md">Read more →</a>
</div>
{% endif %}
```
**Build a custom post grid using metadata only:**
```jinja
{# Create cards showing metadata from multiple posts #}
<div class="post-grid">
{% for post_path in ['blog/intro.md', 'blog/tutorial.md', 'blog/advanced.md'] %}
{% set meta = get_markdown_metadata(post_path) %}
{% if meta.exists %}
<article class="post-card">
<h3>{{ meta.metadata.title }}</h3>
<time datetime="{{ meta.metadata.date }}">{{ meta.metadata.date }}</time>
{% if meta.metadata.author %}
<p class="author">By {{ meta.metadata.author }}</p>
{% endif %}
{% if meta.metadata.excerpt %}
<p>{{ meta.metadata.excerpt }}</p>
{% endif %}
<a href="/{{ post_path }}">Read full post</a>
</article>
{% endif %}
{% endfor %}
</div>
```
**Show custom metadata fields:**
```jinja
{# Display reading time, difficulty, or any custom frontmatter field #}
{% set meta = get_markdown_metadata('tutorials/python-basics.md') %}
{% if meta.exists %}
<div class="tutorial-info">
<h2>{{ meta.metadata.title }}</h2>
{% if meta.metadata.difficulty %}
<span class="badge">{{ meta.metadata.difficulty }}</span>
{% endif %}
{% if meta.metadata.reading_time %}
<span class="time">{{ meta.metadata.reading_time }} min read</span>
{% endif %}
{% if meta.metadata.prerequisites %}
<div class="prerequisites">
<strong>Prerequisites:</strong>
<ul>
{% for prereq in meta.metadata.prerequisites %}
<li>{{ prereq }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
```
**Performance tip:** Use `get_markdown_metadata` instead of `get_rendered_markdown` when you only need frontmatter data. It's much faster since it doesn't process the markdown content.
## Blog Functions
### get_recent_posts(limit, folder)
Get recent blog posts sorted by date (newest first).
**Parameters:**
- `limit` (int, optional) - Maximum number of posts. Default: 5
- `folder` (str, optional) - Search within specific folder. Default: "" (search everywhere)
**Returns:** List of post dictionaries with:
- `title` (str) - From frontmatter or filename
- `date` (str) - From frontmatter
- `path` (str) - Relative path to post
- `url` (str) - Full URL to post
- `metadata` (dict) - Full frontmatter including tags, description, etc.
**Example:**
```jinja
<section class="recent-posts">
<h2>Recent Posts</h2>
{% for post in get_recent_posts(limit=5, folder='blog') %}
<article>
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<time datetime="{{ post.date }}">{{ post.date }}</time>
{% if post.metadata.description %}
<p>{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</section>
```
**Search in specific folder:**
```jinja
{# Only posts from blog/tutorials/ #}
{% for post in get_recent_posts(limit=10, folder='blog/tutorials') %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
```
### get_posts_by_tag(tag, limit)
Get posts filtered by a specific tag.
**Parameters:**
- `tag` (str) - Tag to filter by (case-insensitive)
- `limit` (int, optional) - Maximum posts. Default: 10
**Returns:** List of post dictionaries (same format as `get_recent_posts`)
**Example:**
```jinja
<h2>Python Posts</h2>
{% for post in get_posts_by_tag('python', limit=10) %}
<article>
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<time>{{ post.date }}</time>
</article>
{% endfor %}
```
**Dynamic tag pages:**
```jinja
{# If currentPath is 'tags/python.md' #}
{% set tag = currentPath.split('/')[-1].replace('.md', '') %}
<h1>Posts tagged: {{ tag }}</h1>
{% for post in get_posts_by_tag(tag) %}
...
{% endfor %}
```
### get_related_posts(current_post_path, limit)
Find posts related to the current post based on shared tags.
**Parameters:**
- `current_post_path` (str) - Path to current post
- `limit` (int, optional) - Maximum related posts. Default: 3
**Returns:** List of post dictionaries with an additional `overlap_score` field (number of shared tags)
**Example:**
```jinja
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<aside class="related-posts">
<h3>You might also like:</h3>
{% for post in related %}
<article>
<a href="{{ post.url }}">{{ post.title }}</a>
<small>{{ post.overlap_score }} shared tags</small>
</article>
{% endfor %}
</aside>
{% endif %}
```
### get_all_tags()
Get all tags used across the site with post counts.
**Returns:** List of tag dictionaries with:
- `name` (str) - Tag name
- `count` (int) - Number of posts with this tag
**Example:**
```jinja
<div class="tag-cloud">
{% for tag in get_all_tags() %}
<a href="/tags/{{ tag.name|lower }}"
class="tag"
style="font-size: {{ 0.8 + (tag.count * 0.1) }}em;">
{{ tag.name }} ({{ tag.count }})
</a>
{% endfor %}
</div>
```
**Sort by popularity:**
```jinja
{% for tag in get_all_tags()|sort(attribute='count', reverse=True) %}
<a href="/tags/{{ tag.name|lower }}">
{{ tag.name }} <span class="count">{{ tag.count }}</span>
</a>
{% endfor %}
```
## Navigation
### generate_breadcrumbs(current_path)
Generate breadcrumb navigation based on URL path.
**Parameters:**
- `current_path` (str) - Current page path
**Returns:** List of breadcrumb dictionaries with:
- `title` (str) - Display title (from metadata or derived from path)
- `url` (str) - URL to this level
- `is_current` (bool) - True if this is the current page
**Example:**
```jinja
<nav class="breadcrumbs" aria-label="Breadcrumb">
{% for crumb in generate_breadcrumbs(currentPath) %}
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% else %}
<span aria-current="page">{{ crumb.title }}</span>
{% endif %}
{% if not loop.last %} / {% endif %}
{% endfor %}
</nav>
```
**Styled example:**
```jinja
<ol class="breadcrumb">
{% for crumb in generate_breadcrumbs(currentPath) %}
<li class="breadcrumb-item {% if crumb.is_current %}active{% endif %}">
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% else %}
{{ crumb.title }}
{% endif %}
</li>
{% endfor %}
</ol>
```
### get_navigation_items(max_items)
Get top-level pages and folders for site navigation.
**Parameters:**
- `max_items` (int, optional) - Maximum items to return. Default: 10
**Returns:** List of navigation dictionaries with:
- `title` (str) - Display title (from metadata or filename)
- `url` (str) - URL to page/folder
- `path` (str) - Relative path
- `is_folder` (bool, optional) - True if it's a folder
**Example:**
```jinja
<nav class="main-nav">
<a href="/">Home</a>
{% for item in get_navigation_items(max_items=10) %}
<a href="{{ item.url }}"
{% if currentPath.startswith(item.path) %}class="active"{% endif %}>
{{ item.title }}
</a>
{% endfor %}
</nav>
```
**With icons for folders:**
```jinja
{% for item in get_navigation_items() %}
<a href="{{ item.url }}">
{% if item.is_folder %}📁{% endif %}
{{ item.title }}
</a>
{% endfor %}
```
## Gallery Functions
### get_photo_albums()
Get all photo gallery directories (folders containing mostly images).
**Returns:** List of album dictionaries with:
- `name` (str) - Album name
- `path` (str) - Relative path
- `url` (str) - Full URL
- `image_count` (int) - Number of images
- `total_files` (int) - Total files in album
**Example:**
```jinja
<div class="photo-albums">
<h2>Photo Galleries</h2>
{% for album in get_photo_albums() %}
<a href="{{ album.url }}" class="album-card">
<h3>{{ album.name }}</h3>
<p>{{ album.image_count }} photos</p>
</a>
{% endfor %}
</div>
```
## Built-in Jinja2 Filters
In addition to Foldsite helpers, you can use standard Jinja2 filters:
### String Filters
```jinja
{{ "hello"|upper }} {# HELLO #}
{{ "HELLO"|lower }} {# hello #}
{{ "hello world"|title }} {# Hello World #}
{{ "hello world"|capitalize }} {# Hello world #}
{{ " text "|trim }} {# text #}
{{ "hello"|reverse }} {# olleh #}
```
### List Filters
```jinja
{{ items|length }} {# Count items #}
{{ items|first }} {# First item #}
{{ items|last }} {# Last item #}
{{ items|join(', ') }} {# Join with comma #}
{{ items|sort }} {# Sort list #}
{{ items|sort(attribute='date') }} {# Sort by attribute #}
{{ items|reverse }} {# Reverse list #}
{{ items|unique }} {# Remove duplicates #}
```
### Other Useful Filters
```jinja
{{ value|default('N/A') }} {# Default if falsy #}
{{ html|safe }} {# Don't escape HTML #}
{{ number|round(2) }} {# Round to 2 decimals #}
{{ date|replace('-', '/') }} {# Replace strings #}
```
## Common Patterns
### Pattern: Blog Homepage with Recent Posts
```jinja
<main>
<h1>Welcome to My Blog</h1>
<section class="recent-posts">
{% for post in get_recent_posts(limit=10) %}
<article class="post-card">
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time datetime="{{ post.date }}">{{ post.date }}</time>
{% if post.metadata.description %}
<p>{{ post.metadata.description }}</p>
{% endif %}
{% if post.metadata.tags %}
<div class="tags">
{% for tag in post.metadata.tags %}
<a href="/tags/{{ tag|lower }}" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</section>
<aside class="sidebar">
<h3>Popular Tags</h3>
<div class="tag-cloud">
{% for tag in get_all_tags()|sort(attribute='count', reverse=True)[:10] %}
<a href="/tags/{{ tag.name|lower }}">{{ tag.name }} ({{ tag.count }})</a>
{% endfor %}
</div>
</aside>
</main>
```
### Pattern: Folder Index with Previews
```jinja
<div class="folder-index">
<h1>{{ currentPath.split('/')[-1]|title }}</h1>
<nav class="breadcrumbs">
{% for crumb in generate_breadcrumbs(currentPath) %}
{% if not crumb.is_current %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a> /
{% else %}
{{ crumb.title }}
{% endif %}
{% endfor %}
</nav>
{# Show index.md content if it exists #}
{% set index = get_rendered_markdown(currentPath + '/index.md') %}
{% if index.exists %}
<section class="folder-introduction">
{{ index.html | safe }}
<hr>
</section>
{% endif %}
<div class="content-grid">
{% for file in get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
{% if 'document' in file.categories and file.name != 'index.md' %}
<div class="content-card">
<h3><a href="/{{ file.path }}">{{ file.proper_name }}</a></h3>
<p class="date">{{ file.date_created }}</p>
{% if file.metadata and file.metadata.description %}
<p>{{ file.metadata.description }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
```
### Pattern: Photo Gallery with EXIF Data
```jinja
<div class="gallery">
{% set photos = get_folder_contents(currentPath)|sort(attribute='date_created', reverse=True) %}
<h1>{{ currentPath.split('/')[-1]|title }}</h1>
<p>{{ photos|length }} photos</p>
<div class="photo-grid">
{% for photo in photos %}
{% if 'image' in photo.categories %}
<figure class="photo">
<a href="/download/{{ photo.path }}">
<img src="/download/{{ photo.path }}?max_width=600"
alt="{{ photo.name }}"
loading="lazy">
</a>
<figcaption>
{% if photo.metadata and photo.metadata.exif %}
<p>{{ photo.metadata.exif.DateTimeOriginal }}</p>
{% if photo.metadata.exif.Model %}
<p>{{ photo.metadata.exif.Model }}</p>
{% endif %}
{% endif %}
</figcaption>
</figure>
{% endif %}
{% endfor %}
</div>
</div>
```
### Pattern: Documentation with Sibling Pages
```jinja
<article class="doc-page">
<nav class="doc-sidebar">
<h3>In This Section:</h3>
<ul>
{% for name, path in get_sibling_content_files(currentPath) %}
<li>
<a href="/{{ path }}"
{% if path == currentPath %}class="active"{% endif %}>
{{ name.replace('.md', '')|replace('-', ' ')|title }}
</a>
</li>
{% endfor %}
</ul>
</nav>
<div class="doc-content">
<nav class="breadcrumbs">
{% for crumb in generate_breadcrumbs(currentPath) %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% if not loop.last %} {% endif %}
{% endfor %}
</nav>
{{ content|safe }}
{% set related = get_related_posts(currentPath, limit=3) %}
{% if related %}
<aside class="related-docs">
<h4>Related Documentation:</h4>
<ul>
{% for doc in related %}
<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>
{% endfor %}
</ul>
</aside>
{% endif %}
</div>
</article>
```
## Performance Tips
### Caching
Most helper functions are cached automatically. It's safe to call them multiple times:
```jinja
{# These don't cause multiple filesystem scans #}
{% set posts = get_recent_posts(limit=5) %}
... use posts ...
{% set posts_again = get_recent_posts(limit=5) %} {# Cached result #}
```
### Filtering vs. Multiple Calls
Filter results in templates rather than calling helpers multiple times:
```jinja
✓ Efficient:
{% set all_files = get_folder_contents() %}
{% set docs = all_files|selectattr('categories', 'contains', 'document') %}
{% set images = all_files|selectattr('categories', 'contains', 'image') %}
✗ Less efficient:
{% for file in get_folder_contents() %}
{% if 'document' in file.categories %}...{% endif %}
{% endfor %}
{% for file in get_folder_contents() %} {# Second call #}
{% if 'image' in file.categories %}...{% endif %}
{% endfor %}
```
## Debugging
Enable debug mode to see which templates and helpers are being used:
```toml
# config.toml
[server]
debug = true
```
Add debug output to templates:
```jinja
{# Show what get_recent_posts returns #}
{% set posts = get_recent_posts(limit=3) %}
<pre>{{ posts|pprint }}</pre>
{# Show current variables #}
<pre>
currentPath: {{ currentPath }}
metadata: {{ metadata|pprint }}
</pre>
```
## Next Steps
- **[Template Recipes](../recipes/)** - See these helpers in complete working examples
- **[Template System Overview](index.md)** - Understand how templates work
- **[Template Discovery](template-discovery.md)** - Learn how Foldsite finds templates
With these helpers, you can build sophisticated, dynamic sites while keeping your content as simple files and folders!

View File

@ -0,0 +1,314 @@
---
version: "1.0"
date: "2025-01-15"
author: "DWS Foldsite Team"
title: "Theme Gallery"
description: "Download and use community-created Foldsite themes"
summary: "Browse ready-to-use themes for Foldsite. Download complete template and style packages to jumpstart your site."
quick_tips:
- "Themes are complete template + style packages you can drop into your project"
- "All themes are free and open source"
- "Customize themes to make them your own"
---
# Theme Gallery
Ready-to-use themes for Foldsite. Download, customize, and launch your site quickly.
## What is a Theme?
A Foldsite theme is a complete package of:
- **Templates** (`templates/` directory)
- **Styles** (`styles/` directory)
- **Example content** (optional)
- **Configuration** (optional)
Simply download and drop into your Foldsite project.
## Official Themes
### Default Theme
**Included in:** Foldsite repository (`example_site/`)
**Type:** Blog + Gallery
**Best for:** Personal sites, blogs with photos
**Features:**
- Responsive sidebar navigation
- Blog post support with metadata
- Photo gallery views
- Breadcrumb navigation
- Clean, minimal design
**Install:**
```bash
# Copy from example_site
cp -r example_site/template my-site/templates
cp -r example_site/style my-site/styles
```
**Preview:** See [Tanishq Dubey's site](https://tanishq.page)
---
### Documentation Theme
**Included in:** Foldsite repository (`docs/`)
**Type:** Documentation
**Best for:** Project docs, technical writing, knowledge bases
**Features:**
- Hierarchical navigation
- Sibling page links
- Code syntax highlighting
- Breadcrumb trails
- Clean, readable typography
**Install:**
```bash
# Copy from docs
cp -r docs/templates my-site/templates
cp -r docs/styles my-site/styles
```
**Preview:** This documentation site!
---
## Community Themes
*Community-contributed themes will appear here as they're created and submitted.*
### How to Submit a Theme
Created a theme you want to share?
**Requirements:**
- Complete templates and styles
- README with installation instructions
- Screenshot or demo site
- MIT or similar permissive license
- No external dependencies (except common CDNs)
**Submission process:**
1. Create GitHub repository with your theme
2. Open issue on Foldsite repo with "Theme Submission" label
3. Include:
- Theme name and description
- Screenshot or demo URL
- Repository link
- What makes it unique
4. We'll review and add to gallery
**Theme structure:**
```
my-theme/
├── README.md
├── LICENSE
├── templates/
│ ├── base.html
│ ├── __file.md.html
│ └── ...
├── styles/
│ ├── base.css
│ └── ...
├── screenshots/
│ └── preview.png
└── example-content/ (optional)
└── ...
```
---
## Using a Theme
### Installation
1. **Download theme** (clone or download ZIP)
2. **Copy to your project:**
```bash
cp -r theme-name/templates my-site/templates
cp -r theme-name/styles my-site/styles
```
3. **Configure paths** in `config.toml`:
```toml
[paths]
templates_dir = "/path/to/my-site/templates"
styles_dir = "/path/to/my-site/styles"
```
4. **Test:**
```bash
python main.py --config config.toml
```
### Customization
Themes are starting points. Make them your own!
**Easy customizations:**
- Change colors in CSS
- Modify fonts
- Adjust spacing and sizing
- Replace logo/branding
- Update footer text
**Advanced customizations:**
- Modify templates
- Add new template variants
- Change layout structure
- Add custom helper functions
- Integrate JavaScript libraries
**Best practice:** Keep original theme in git so you can track your changes.
---
## Theme Development
Want to create your own theme?
### Theme Checklist
A complete theme should include:
**Required Templates:**
- [ ] `base.html` - Main page wrapper
- [ ] `__file.md.html` - Markdown file display
- [ ] `__folder.md.html` - Folder index for documents
- [ ] `__error.html` - Error pages
**Optional but Recommended:**
- [ ] `index.html` - Custom homepage
- [ ] `__folder.image.html` - Photo galleries
- [ ] `__file.document.html` - Document-specific layout
**Styles:**
- [ ] `base.css` - Base styles, always loaded
- [ ] `__file.md.css` - Markdown file styles
- [ ] `__folder.image.css` - Gallery styles
- [ ] Responsive design (mobile-friendly)
**Documentation:**
- [ ] README with installation instructions
- [ ] Screenshot or demo
- [ ] List of features
- [ ] Customization guide
- [ ] License (MIT recommended)
### Design Guidelines
**For consistency and usability:**
1. **Mobile-first** - Design for small screens first
2. **Accessible** - Follow WCAG guidelines
3. **Fast** - Minimize CSS, optimize images
4. **Semantic HTML** - Use proper elements
5. **Print-friendly** - Consider print stylesheets
**Typography:**
- Readable font sizes (16px+ for body)
- Good line height (1.5+)
- Proper contrast ratios
- System fonts or fast-loading web fonts
**Colors:**
- Consistent color palette
- Sufficient contrast
- Dark mode consideration (optional)
**Layout:**
- Clear visual hierarchy
- Consistent spacing
- Responsive breakpoints
- Touch-friendly (44px+ tap targets)
### Testing Your Theme
Before sharing, test with:
- **Various content types** - Markdown, images, mixed
- **Different structures** - Flat vs. deep hierarchies
- **Edge cases** - Long titles, no metadata, many tags
- **Devices** - Mobile, tablet, desktop
- **Browsers** - Chrome, Firefox, Safari
### Inspiration
Look at themes from other static site generators:
- Jekyll themes
- Hugo themes
- 11ty themes
- Gatsby themes
Adapt patterns (don't copy code) to Foldsite's template system.
---
## Coming Soon
**Future theme additions:**
- **Minimal Blog** - Ultra-simple, typography-focused
- **Photo Portfolio** - Full-screen galleries, minimal UI
- **Magazine** - Multi-column, content-rich
- **Landing Page** - Single-page, marketing-focused
- **Academic** - Papers, publications, research-focused
Want to help create these? See [Develop Foldsite](develop/).
---
## Theme Showcase
*Once community themes are available, this section will showcase screenshots and live demos.*
---
## Frequently Asked Questions
**Q: Are themes free?**
A: Yes! All themes in the official gallery are free and open source.
**Q: Can I sell themes I create?**
A: The themes themselves must be open source if listed here, but you could offer customization services commercially.
**Q: Do themes work with all Foldsite versions?**
A: Themes should specify compatible versions. Most work across versions unless core template system changes.
**Q: Can I request a specific theme?**
A: Open an issue with "Theme Request" label. No guarantees, but community might help!
**Q: How do I update a theme?**
A: If you've customized it, manually merge changes. Otherwise, re-download and replace.
**Q: Can I mix themes?**
A: Yes! Take templates from different themes. Just ensure styles don't conflict.
---
## Contributing
Help grow the theme ecosystem:
- **Create themes** - Share your designs
- **Improve docs** - Help others use themes
- **Test themes** - Report issues
- **Showcase sites** - Inspire others
See [Develop Foldsite](develop/) for contribution guidelines.
---
## Support
Need help with themes?
- **[Support](support.md)** - Get help
- **[Templates Guide](templates/)** - Learn the system
- **[Recipes](recipes/)** - See examples
- **GitHub Issues** - Report theme bugs
---
*This gallery is just getting started. As the Foldsite community grows, expect many more themes! Consider contributing the first one!*

View File

@ -0,0 +1,266 @@
/* Document Layout - For documentation/blog post pages */
/* Based on reference_post.html design */
:root {
/* Document-specific spacing */
--document-margin-x: 120px;
--document-max-width: 1200px;
}
/* Main content wrapper for document pages */
.document-layout {
/* max-width: var(--document-max-width); */
margin: 0 auto;
padding: var(--spacing-exp-7) var(--document-margin-x);
box-sizing: border-box;
}
.document {
display: flex;
flex-direction: row;
height: 100vh;
}
.document-header-holder {
display: flex;
height: 70vh;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
/* text-align: end; */
}
/* Document header with metadata */
.document-header {
margin-bottom: var(--spacing-exp-6);
max-width: 15%;
}
.document-metadata {
font-family: var(--fontFamily-mono);
font-size: var(--fontSize-small);
color: var(--swatch-4);
margin-bottom: var(--spacing-2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.document-title {
font-size: 3.8rem;
font-weight: 420;
line-height: 1;
margin: var(--spacing-2) 0;
letter-spacing: -0.02em;
}
.document-subtitle {
font-size: 1.6rem;
font-weight: 500;
color: var(--swatch-2);
line-height: var(--lineHeight-default);
margin-top: var(--spacing-1);
}
/* Document content area */
.document-quick-tips {
font-size: 1rem;
font-weight: 500;
color: var(--swatch-2);
line-height: var(--lineHeight-default);
margin-top: var(--spacing-1);
}
.document-content-wrapper {
flex-grow: 2;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-wrap: nowrap;
overflow-y: auto;
margin-left: 1vw;
margin-right: 1vw;
}
.document-content {
font-size: var(--fontSize-default);
line-height: var(--lineHeight-default);
width: fit-content;
padding-bottom: var(--spacing-exp-7);
}
.document-content > * + * {
margin-top: 1em;
}
.document-content h1 {
font-size: 3.8rem;
width: fit-content;
}
/* Section headers within document */
.document-content h2 {
margin-top: var(--spacing-exp-6);
margin-bottom: var(--spacing-2);
font-size: 2.4rem;
}
.document-content h3 {
margin-top: var(--spacing-exp-5);
margin-bottom: var(--spacing-1);
font-size: 1.8rem;
}
.document-content h4 {
margin-top: var(--spacing-2);
margin-bottom: var(--spacing-1);
font-size: 1.3rem;
}
/* Enhanced code blocks for documentation */
.document-content pre {
margin: var(--spacing-2) 0;
background: var(--background-3);
border-radius: var(--border-radius-small);
overflow-x: auto;
max-width: 120ch;
}
.document-content pre code {
font-family: var(--fontFamily-mono);
font-size: 0.95rem;
line-height: 1.6;
}
/* Document-specific column layouts */
.document-content .column-set {
margin: var(--spacing-exp-5) 0;
}
/* Two-column code examples */
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-2);
margin: var(--spacing-2) 0;
}
/* Callout boxes for notes/warnings */
.callout {
padding: var(--spacing-2);
margin: var(--spacing-2) 0;
border-left: 3px solid var(--swatch-5);
background: var(--background-3);
}
.callout.note {
border-left-color: var(--swatch-3);
}
.callout.warning {
border-left-color: var(--swatch-1);
background: rgba(255, 200, 0, 0.1);
}
/* Table of contents navigation */
.document-toc {
position: sticky;
top: var(--spacing-2);
font-size: var(--fontSize-secondary);
padding: var(--spacing-2);
background: var(--background-3);
border-radius: var(--border-radius-small);
}
.document-toc ul {
list-style: none;
padding-left: 0;
}
.document-toc li {
margin: var(--spacing-half) 0;
}
.document-toc a {
color: var(--swatch-2);
text-decoration: none;
}
.document-toc a:hover {
color: var(--swatch-1);
text-decoration: underline;
}
/* Footer navigation (prev/next) */
.document-footer {
max-width: 15%;
}
.document-footer-holder {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 40vh;
}
.document-nav-link {
display: flex;
flex-direction: column;
text-decoration: none;
color: var(--swatch-2);
}
.document-nav-link:hover {
color: var(--swatch-1);
}
.document-nav-label {
font-size: var(--fontSize-small);
font-family: var(--fontFamily-mono);
color: var(--swatch-4);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-half);
}
.document-nav-label-selected {
font-size: var(--fontSize-small);
font-family: var(--fontFamily-mono);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-half);
color:rgb(236, 113, 42);
}
.document-nav-title {
font-size: var(--fontSize-header);
font-weight: 500;
}
/* Responsive adjustments */
@media (max-width: 1024px) {
:root {
--document-margin-x: 60px;
}
}
@media (max-width: 768px) {
:root {
--document-margin-x: var(--spacing-2);
}
.document-title {
font-size: 2.5rem;
}
.code-comparison {
grid-template-columns: 1fr;
}
.document-footer {
flex-direction: column;
gap: var(--spacing-2);
}
}

View File

@ -1,23 +0,0 @@
article {
max-width: 800px;
margin: 0 auto;
}
article h1,
article h2,
article h3,
article h4,
article h5,
article h6 {
margin-top: 1.5rem;
}
article p {
margin: 1rem 0;
}
article code {
background: var(--code-background-color);
padding: 0.2rem 0.4rem;
border-radius: 3px;
}

View File

@ -0,0 +1,266 @@
/* Document Layout - For documentation/blog post pages */
/* Based on reference_post.html design */
:root {
/* Document-specific spacing */
--document-margin-x: 120px;
--document-max-width: 1200px;
}
/* Main content wrapper for document pages */
.document-layout {
/* max-width: var(--document-max-width); */
margin: 0 auto;
padding: var(--spacing-exp-7) var(--document-margin-x);
box-sizing: border-box;
}
.document {
display: flex;
flex-direction: row;
height: 100vh;
}
.document-header-holder {
display: flex;
height: 70vh;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
/* text-align: end; */
}
/* Document header with metadata */
.document-header {
margin-bottom: var(--spacing-exp-6);
max-width: 15%;
}
.document-metadata {
font-family: var(--fontFamily-mono);
font-size: var(--fontSize-small);
color: var(--swatch-4);
margin-bottom: var(--spacing-2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.document-title {
font-size: 3.8rem;
font-weight: 420;
line-height: 1;
margin: var(--spacing-2) 0;
letter-spacing: -0.02em;
}
.document-subtitle {
font-size: 1.6rem;
font-weight: 500;
color: var(--swatch-2);
line-height: var(--lineHeight-default);
margin-top: var(--spacing-1);
}
/* Document content area */
.document-quick-tips {
font-size: 1rem;
font-weight: 500;
color: var(--swatch-2);
line-height: var(--lineHeight-default);
margin-top: var(--spacing-1);
}
.document-content-wrapper {
flex-grow: 2;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-wrap: nowrap;
overflow-y: auto;
margin-left: 1vw;
margin-right: 1vw;
}
.document-content {
font-size: var(--fontSize-default);
line-height: var(--lineHeight-default);
width: fit-content;
padding-bottom: var(--spacing-exp-7);
}
.document-content > * + * {
margin-top: 1em;
}
.document-content h1 {
font-size: 3.8rem;
width: fit-content;
}
/* Section headers within document */
.document-content h2 {
margin-top: var(--spacing-exp-6);
margin-bottom: var(--spacing-2);
font-size: 2.4rem;
}
.document-content h3 {
margin-top: var(--spacing-exp-5);
margin-bottom: var(--spacing-1);
font-size: 1.8rem;
}
.document-content h4 {
margin-top: var(--spacing-2);
margin-bottom: var(--spacing-1);
font-size: 1.3rem;
}
/* Enhanced code blocks for documentation */
.document-content pre {
margin: var(--spacing-2) 0;
background: var(--background-3);
border-radius: var(--border-radius-small);
overflow-x: auto;
max-width: 120ch;
}
.document-content pre code {
font-family: var(--fontFamily-mono);
font-size: 0.95rem;
line-height: 1.6;
}
/* Document-specific column layouts */
.document-content .column-set {
margin: var(--spacing-exp-5) 0;
}
/* Two-column code examples */
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-2);
margin: var(--spacing-2) 0;
}
/* Callout boxes for notes/warnings */
.callout {
padding: var(--spacing-2);
margin: var(--spacing-2) 0;
border-left: 3px solid var(--swatch-5);
background: var(--background-3);
}
.callout.note {
border-left-color: var(--swatch-3);
}
.callout.warning {
border-left-color: var(--swatch-1);
background: rgba(255, 200, 0, 0.1);
}
/* Table of contents navigation */
.document-toc {
position: sticky;
top: var(--spacing-2);
font-size: var(--fontSize-secondary);
padding: var(--spacing-2);
background: var(--background-3);
border-radius: var(--border-radius-small);
}
.document-toc ul {
list-style: none;
padding-left: 0;
}
.document-toc li {
margin: var(--spacing-half) 0;
}
.document-toc a {
color: var(--swatch-2);
text-decoration: none;
}
.document-toc a:hover {
color: var(--swatch-1);
text-decoration: underline;
}
/* Footer navigation (prev/next) */
.document-footer {
max-width: 15%;
}
.document-footer-holder {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 40vh;
}
.document-nav-link {
display: flex;
flex-direction: column;
text-decoration: none;
color: var(--swatch-2);
}
.document-nav-link:hover {
color: var(--swatch-1);
}
.document-nav-label {
font-size: var(--fontSize-small);
font-family: var(--fontFamily-mono);
color: var(--swatch-4);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-half);
}
.document-nav-label-selected {
font-size: var(--fontSize-small);
font-family: var(--fontFamily-mono);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-half);
color:rgb(236, 113, 42);
}
.document-nav-title {
font-size: var(--fontSize-header);
font-weight: 500;
}
/* Responsive adjustments */
@media (max-width: 1024px) {
:root {
--document-margin-x: 60px;
}
}
@media (max-width: 768px) {
:root {
--document-margin-x: var(--spacing-2);
}
.document-title {
font-size: 2.5rem;
}
.code-comparison {
grid-template-columns: 1fr;
}
.document-footer {
flex-direction: column;
gap: var(--spacing-2);
}
}

View File

@ -1,280 +1,263 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
/* Foldsite Documentation Base Styles */
/* Design system extracted from reference designs */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
:root {
/* Typography Scale */
--fontSize-default: 14.5px;
--fontSize-small: 12px;
--fontSize-secondary: 13.5px;
--fontSize-header: 17px;
--fontSize-large: 22px;
--lineHeight-default: 1.65;
/* Color Swatches - Primary opacity-based system */
--swatch-1: rgba(0, 0, 0, 0.85);
--swatch-2: rgba(0, 0, 0, 0.75);
--swatch-3: rgba(0, 0, 0, 0.6);
--swatch-4: rgba(0, 0, 0, 0.4);
--swatch-5: rgba(0, 0, 0, 0.25);
--swatch-6: rgba(0, 0, 0, 0.15);
/* Base Colors */
--color-default: rgba(0, 0, 0, 0.75);
--color-default-secondary: rgba(0, 0, 0, 0.4);
--background-1: #FAF9F6;
--background-2: #ffffff;
--background-3: #fcfcfc;
--background-force-dark: #111111;
/* Spacing System - Consistent scale */
--spacing-1: 15px;
--spacing-half: calc(15px * 0.5);
--spacing-2: calc(15px * 2);
--spacing-3: calc(15px * 3);
--spacing-4: calc(15px * 4);
/* Exponential spacing for larger gaps */
--spacing-exp-1: 5px;
--spacing-exp-half: calc(5px * 0.5);
--spacing-exp-2: calc(5px * 2);
--spacing-exp-3: calc(5px * 3);
--spacing-exp-4: calc(5px * 5);
--spacing-exp-5: calc(5px * 8);
--spacing-exp-6: calc(5px * 13);
--spacing-exp-7: calc(5px * 21);
--spacing-exp-8: calc(5px * 34);
/* Typography */
--fontFamily-default: 'Lekton', monospace;
--fontFamily-mono: "Lekton", monospace;
--fontFamily-display: 'Lekton', monospace;
--fontFamily-serif: 'Lekton', monospace;
/* UI Elements */
--border-radius-small: 5px;
--opacity-downstate-default: 0.7;
--ui-border: rgba(0, 0, 0, 0.14);
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
}
.doto-500 {
font-family: "Doto", sans-serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
"ROND" 0;
}
.lekton-regular {
font-family: "Lekton", monospace;
font-weight: 400;
font-style: normal;
}
.lekton-bold {
font-family: "Lekton", monospace;
font-weight: 700;
font-style: normal;
}
.lekton-regular-italic {
font-family: "Lekton", monospace;
font-weight: 400;
font-style: italic;
}
html {
overflow-anchor: none;
text-size-adjust: 100%;
}
body {
margin: 0;
padding: 0;
font-family: var(--fontFamily-default);
font-size: var(--fontSize-default);
line-height: var(--lineHeight-default);
color: var(--color-default);
background-color: var(--background-1);
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
height: 100vh;
overflow: hidden;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin: 0;
font-weight: 500;
line-height: 1.2;
color: var(--swatch-1);
font-family: var(--fontFamily-display);
}
h1 {
font-size: 3.8rem;
letter-spacing: -0.02em;
font-weight: 420;
line-height: 1;
}
ol,
ul {
list-style: none;
h2 {
font-size: 2.4rem;
letter-spacing: -0.01em;
}
blockquote,
q {
quotes: none;
h3 {
font-size: 1.8rem;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
h4 {
font-size: 1.3rem;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
@property --font-color {
syntax: "<color>";
inherits: true;
initial-value: oklch(25.11% 0.006 258.36);
}
@property --background-color {
syntax: "<color>";
inherits: true;
initial-value: #F6F0F0;
}
@property --code-background-color {
syntax: "<color>";
inherits: true;
initial-value: #c7c1c1;
}
@property --hover-color {
syntax: "<color>";
inherits: true;
initial-value: #A4B465;
}
@property --url-color {
syntax: "<color>";
inherits: true;
initial-value: #626F47;
}
@media (prefers-color-scheme: dark) {
:root {
--font-color: oklch(91.87% 0.003 264.54);
--background-color: #29261f;
--hover-color: #626F47;
--url-color: #A4B465;
--code-background-color: #3d392e;
}
}
body {
font-family: "Open Sans", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-variation-settings:
"wdth" 100;
display: flex;
justify-content: center;
background-color: var(--background-color);
color: var(--font-color);
p {
margin: 0 0 1em 0;
}
a {
color: var(--url-color);
text-decoration: none;
transition: all 0.25s ease-in-out;
color: var(--swatch-1);
text-decoration: underline;
text-decoration-color: var(--swatch-5);
transition: text-decoration-color 0.2s;
}
a:hover {
color: var(--hover-color);
transition: all 0.25s ease-in-out;
text-decoration-color: var(--swatch-1);
}
a:visited {
color: #C14600;
transition: all 0.25s ease-in-out;
/* Code */
code {
font-family: 'Menlo', 'Monaco', var(--fontFamily-mono);
font-size: 0.9em;
background: var(--background-3);
padding: 0.2em 0.4em;
border-radius: 3px;
}
a:visited:hover {
color: var(--hover-color);
transition: all 0.25s ease-in-out;
pre {
background: var(--background-3);
border-radius: var(--border-radius-small);
overflow-x: auto;
line-height: 1.5;
}
@supports (font-size-adjust: 1) {
.content {
font-size-adjust: 0.5;
}
pre code {
background: none;
padding: 0;
}
ul {
list-style: square;
/* Horizontal Rules */
hr {
border: 0;
height: 1px;
background: var(--ui-border);
margin: var(--spacing-exp-4) 0;
}
/* Lists */
ul, ol {
margin: 0 0 1em 0;
padding-left: 2em;
}
li {
line-height: 160%;
margin-bottom: 0.5rem;
margin: 0.5em 0;
}
.content {
line-height: calc(1ex / 0.32);
text-rendering: optimizeLegibility;
max-width: 80ch;
padding-left: 1rem;
/* Images */
img {
max-width: 100%;
height: auto;
}
.content h1 {
font-size: 2.5em;
line-height: calc(1ex / 0.42);
margin: calc(1ex / 0.42) 0;
/* Blockquotes */
blockquote {
margin: var(--spacing-2) 0;
padding-left: var(--spacing-2);
border-left: 3px solid var(--swatch-5);
color: var(--swatch-3);
font-style: italic;
}
.content h2 {
font-size: 2em;
line-height: calc(1ex / 0.42);
margin: calc(1ex / 0.42) 0;
/* Tables */
table {
border-collapse: collapse;
width: 100%;
margin: var(--spacing-2) 0;
}
.content h3 {
font-size: 1.75em;
line-height: calc(1ex / 0.38);
margin: calc(1ex / 0.38) 0;
th, td {
padding: var(--spacing-half) var(--spacing-1);
text-align: left;
border-bottom: 1px solid var(--ui-border);
}
.content h4 {
font-size: 1.5em;
line-height: calc(1ex / 0.37);
margin: calc(1ex / 0.37) 0;
th {
font-weight: 600;
color: var(--swatch-1);
}
.content p {
font-size: 1em;
line-height: calc(1ex / 0.32);
margin: calc(1ex / 0.32) 0;
text-align: justify;
hyphens: auto;
/* Utilities */
.mono {
font-family: var(--fontFamily-mono);
font-size: 0.95rem;
}
.sidebar {
padding-top: 4rem;
line-height: calc(1ex / 0.32);
text-rendering: optimizeLegibility;
.secondary {
color: var(--color-default-secondary);
}
.holder {
display: flex;
flex-direction: row;
margin: auto;
.small {
font-size: var(--fontSize-small);
}
.text-center {
text-align: center;
}
/* Column System - Mimics reference design */
.column-set {
display: grid;
gap: var(--spacing-2);
margin: var(--spacing-2) 0;
}
.column-set[data-columns="2"] {
grid-template-columns: repeat(2, 1fr);
}
.column-set[data-columns="3"] {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 768px) {
.column-set {
grid-template-columns: 1fr;
}
}

96
docs/templates/__file.document.html vendored Normal file
View File

@ -0,0 +1,96 @@
<div class="document-layout">
<article class="document">
<header class="document-header">
<hr>
<div class="document-header-holder">
{% if metadata and metadata.date %}
<div class="document-metadata">
{{ metadata.date }}
{% if metadata.author %} — {{ metadata.author }}{% endif %}
</div>
{% endif %}
<div>
<div class="document-file-nav-holder">
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings and siblings|length > 1 %}
{% set current_index = namespace(value=-1) %}
{% for sibling in siblings %}
{% if sibling[1] == currentPath %}
<a href="/{{ sibling[1] }}" class="document-nav-link">
<span class="document-nav-label-selected">🖹 {{ sibling[0] }}</span>
</a>
{% else %}
<a href="/{{ sibling[1] }}" class="document-nav-link">
<span class="document-nav-label">🖹 {{ sibling[0] }}</span>
</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="document-folder-nav-holder">
{% set siblingsfolders = get_sibling_content_folders(currentPath) %}
{% if siblingsfolders and siblingsfolders|length > 1 %}
{% set current_index = namespace(value=-1) %}
{% for siblingf in siblingsfolders %}
{% if siblingf.path == currentPath %}
{% set current_index.value = loop.index0 %}
{% endif %}
<a href="/{{ siblingf[1] }}" class="document-nav-link">
<span class="document-nav-label">🗀 {{ siblingf[0] }}</span>
</a>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</header>
<div class="document-content-wrapper">
<div class="document-content">
<hr>
{{ content|safe }}
</div>
</div>
{% if metadata and metadata.tags %}
<footer class="document-footer">
<div class="document-tags">
<span class="mono small secondary">Tagged: </span>
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}" class="mono small">{{ tag }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</div>
</footer>
{% endif %}
<div class="document-footer">
<hr>
<div class="document-footer-holder">
{% if metadata and metadata.summary %}
<p class="document-subtitle">{{ metadata.summary }}</p>
{% endif %}
{% if metadata and metadata.quick_tips %}
<div class="document-quick-tips">
<h4>Quick Tips:</h4>
<hr>
<ul>
{% for tip in metadata.quick_tips %}
<li>{{ tip }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</article>
</div>

View File

@ -1,7 +0,0 @@
<article>
{{ content|safe }}
</article>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/dockerfile.min.js"></script>
<script>hljs.highlightAll();</script>

120
docs/templates/__folder.html vendored Normal file
View File

@ -0,0 +1,120 @@
<div class="document-layout">
{% set index_path = (currentPath + '/index.md') if currentPath else 'index.md' %}
{% set index = get_rendered_markdown(index_path) %}
<article class="document">
<header class="document-header">
<hr>
<div class="document-header-holder">
{% if index.exists %}
{% if index.metadata and index.metadata.date %}
<div class="document-metadata">
{{ index.metadata.date }}
{% if index.metadata.author %} — {{ index.metadata.author }}{% endif %}
</div>
{% endif %}
{% endif %}
<div>
<div class="document-file-nav-holder">
{% set siblings = get_sibling_content_files(currentPath) %}
{% if siblings and siblings|length > 1 %}
{% set current_index = namespace(value=-1) %}
{% for sibling in siblings %}
{% if sibling[1] == currentPath %}
<a href="/{{ sibling[1] }}" class="document-nav-link">
<span class="document-nav-label-selected">🖹 {{ sibling[0] }}</span>
</a>
{% else %}
<a href="/{{ sibling[1] }}" class="document-nav-link">
<span class="document-nav-label">🖹 {{ sibling[0] }}</span>
</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="document-folder-nav-holder">
{% set siblingsfolders = get_sibling_content_folders(currentPath) %}
{% if siblingsfolders and siblingsfolders|length > 1 %}
{% for siblingf in siblingsfolders %}
{% if siblingf[1] == currentPath %}
<a href="/{{ siblingf[1] }}" class="document-nav-link">
<span class="document-nav-label-selected">🗀 {{ siblingf[0] }}</span>
</a>
{% for child in get_folder_contents(siblingf[1]) %}
{% if child.name == "index.md" %}
<a href="/{{ child.path }}" class="document-nav-link">
<span class="document-nav-label-selected">∟ 🖹 {{ child.name }}</span>
</a>
{% else %}
<a href="/{{ child.path }}" class="document-nav-link">
<span class="document-nav-label">∟ 🖹 {{ child.name }}</span>
</a>
{% endif %}
{% endfor %}
{% else %}
<a href="/{{ siblingf[1] }}" class="document-nav-link">
<span class="document-nav-label">🗀 {{ siblingf[0] }}</span>
</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
</header>
<div class="document-content-wrapper">
<div class="document-content">
<hr>
{% if index.exists %}
<section class="folder-index">
{{ index.html | safe }}
<hr>
</section>
{% endif %}
</div>
</div>
{% if metadata and metadata.tags %}
<footer class="document-footer">
<div class="document-tags">
<span class="mono small secondary">Tagged: </span>
{% for tag in metadata.tags %}
<a href="/tags/{{ tag|lower }}" class="mono small">{{ tag }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</div>
</footer>
{% endif %}
<!-- Navigation to sibling documents -->
<div class="document-footer">
<hr>
{% if index.exists %}
<div class="document-footer-holder">
{% if index.metadata and index.metadata.summary %}
<p class="document-subtitle">{{ index.metadata.summary }}</p>
{% endif %}
{% if index.metadata and index.metadata.quick_tips %}
<div class="document-quick-tips">
<h4>Quick Tips:</h4>
<hr>
<ul>
{% for tip in index.metadata.quick_tips %}
<li>{{ tip }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
</article>
</div>

View File

@ -1,43 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
{% if metadata and metadata.title %}
<title>{{ metadata.title }} — Foldsite</title>
<meta name="description" content="{{ metadata.description or 'A thoughtful static site generator' }}">
{% if metadata.tags %}
<meta name="keywords" content="{{ metadata.tags | join(', ') }}">
{% endif %}
{% else %}
<title>Foldsite — Documentation</title>
<meta name="description" content="A thoughtful static site generator built with Python">
{% endif %}
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website">
<meta property="og:title" content="{{ metadata.title if metadata and metadata.title else 'Foldsite' }}">
<meta property="og:description" content="{{ metadata.description if metadata and metadata.description else 'A thoughtful static site generator' }}">
<!-- Load layout-specific styles -->
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}">
{% endfor %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Doto:wght@100..900&family=Lekton:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
{% for style in styles %}
<link rel="stylesheet" href="/styles{{ style }}" type="text/css">
{% endfor %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/django.min.js"></script>
{% block extra_styles %}{% endblock %}
</head>
<body>
<div class="holder">
<div class="sidebar"> <!-- Changed <sidebar> to <div> -->
<ul>
<li><a href="/">⌂ Home</a></li>
<hr>
{% for f in get_folder_contents() %}
{% if not f.is_dir %}
{% if f.proper_name == "index" %}
{% else %}
<li><a href="/{{ f.path }}">{{ f.proper_name }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
</div>
<div class="content"> <!-- <main> tag remains the same -->
{{ content|safe }}
<div class="footer">
<p>&copy; DWS</p>
</div>
</div>
</div>
</body>
{{ content|safe }}
<script>hljs.highlightAll();</script>
</body>
</html>

20
main.py
View File

@ -3,7 +3,8 @@ from src.routes.routes import RouteManager
from src.config.args import create_parser
from src.config.config import Configuration
from src.rendering.helpers import TemplateHelpers
from src.server.file_manager import create_filemanager_blueprint
from src.server.enhanced_file_manager import create_enhanced_filemanager_blueprint
from src.rendering.debug_helpers import init_debug_helper
def main():
@ -16,6 +17,9 @@ def main():
r = RouteManager(c)
t = TemplateHelpers(c)
# Initialize debug helper for better developer experience
init_debug_helper(c)
server = Server(
debug=c.debug,
host=c.listen_address,
@ -24,18 +28,30 @@ def main():
workers=c.max_threads,
)
# Original template functions
server.register_template_function("get_sibling_content_files", t.get_sibling_content_files)
server.register_template_function("get_text_document_preview", t.get_text_document_preview)
server.register_template_function("get_sibling_content_folders", t.get_sibling_content_folders)
server.register_template_function("get_folder_contents", t.get_folder_contents)
# Enhanced blog-focused template helpers
server.register_template_function("get_recent_posts", t.get_recent_posts)
server.register_template_function("get_posts_by_tag", t.get_posts_by_tag)
server.register_template_function("get_photo_albums", t.get_photo_albums)
server.register_template_function("get_navigation_items", t.get_navigation_items)
server.register_template_function("generate_breadcrumbs", t.generate_breadcrumbs)
server.register_template_function("get_related_posts", t.get_related_posts)
server.register_template_function("get_all_tags", t.get_all_tags)
server.register_template_function("get_rendered_markdown", t.get_rendered_markdown)
server.register_template_function("get_markdown_metadata", t.get_markdown_metadata)
server.register_route("/styles/<path:path>", r.get_style)
server.register_route("/download/<path:path>", r.get_static)
server.register_route("/", r.default_route, defaults={"path": ""})
server.register_route("/<path:path>", r.default_route)
if c.admin_browser:
file_manager_bp = create_filemanager_blueprint(c.content_dir, url_prefix='/admin', auth_password=c.admin_password)
file_manager_bp = create_enhanced_filemanager_blueprint(c.content_dir, url_prefix='/admin', auth_password=c.admin_password)
server.app.register_blueprint(file_manager_bp)
try:

View File

@ -0,0 +1,218 @@
"""
Debug helpers for foldsite - following grug principles
Simple debugging tools that don't require big brain to understand
"""
import os
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any
class DebugHelper:
"""
Grug-approved debugging - simple tools that show what happening
No complexity demons, just helpful info when things go wrong
"""
def __init__(self, config):
self.config = config
self.debug_enabled = getattr(config, 'debug', False)
def log_template_search(self, path: str, found_template: str = None, candidates: List[str] = None):
"""Log template discovery process for debugging"""
if not self.debug_enabled:
return
print(f"🔍 Template search for: {path}")
if candidates:
print(f" Candidates checked: {', '.join(candidates)}")
if found_template:
print(f" ✅ Found: {found_template}")
else:
print(f" ❌ No template found")
print()
def show_available_templates(self):
"""Show all available templates - useful for developers"""
if not self.debug_enabled:
return {}
templates = {}
template_dir = Path(self.config.templates_dir)
if template_dir.exists():
for template_file in template_dir.rglob("*.html"):
rel_path = str(template_file.relative_to(template_dir))
templates[rel_path] = {
'path': str(template_file),
'size': template_file.stat().st_size,
'modified': datetime.fromtimestamp(template_file.stat().st_mtime).strftime('%Y-%m-%d %H:%M')
}
if self.debug_enabled:
print("📄 Available templates:")
for name, info in templates.items():
print(f" {name} ({info['size']} bytes, modified {info['modified']})")
print()
return templates
def show_content_structure(self, max_depth: int = 2):
"""Show content directory structure - helps with template planning"""
if not self.debug_enabled:
return {}
content_dir = Path(self.config.content_dir)
structure = {}
def scan_directory(dir_path: Path, current_depth: int = 0):
if current_depth > max_depth:
return {"...": "truncated"}
items = {}
try:
for item in dir_path.iterdir():
if item.name.startswith('___'):
continue
if item.is_file():
items[item.name] = {
'type': 'file',
'extension': item.suffix,
'size': item.stat().st_size
}
elif item.is_dir():
items[item.name] = {
'type': 'directory',
'contents': scan_directory(item, current_depth + 1)
}
except PermissionError:
items['error'] = 'Permission denied'
return items
if content_dir.exists():
structure = scan_directory(content_dir)
if self.debug_enabled:
print("📁 Content structure:")
self._print_structure(structure, indent=" ")
print()
return structure
def _print_structure(self, structure: Dict, indent: str = ""):
"""Helper to print directory structure nicely"""
for name, info in structure.items():
if isinstance(info, dict):
if info.get('type') == 'file':
size_kb = info['size'] // 1024 if info['size'] > 1024 else info['size']
unit = 'KB' if info['size'] > 1024 else 'B'
print(f"{indent}{name} ({size_kb}{unit})")
elif info.get('type') == 'directory':
print(f"{indent}{name}/")
if 'contents' in info:
self._print_structure(info['contents'], indent + " ")
else:
print(f"{indent}{name}: {info}")
def get_template_context_info(self, context: Dict[str, Any]):
"""Show what variables are available in templates"""
if not self.debug_enabled:
return {}
info = {}
for key, value in context.items():
if key.startswith('_'): # Skip internal variables
continue
value_type = type(value).__name__
if hasattr(value, '__len__') and not isinstance(value, str):
info[key] = f"{value_type} (length: {len(value)})"
else:
info[key] = value_type
if self.debug_enabled:
print("🔧 Template context variables:")
for var_name, var_info in info.items():
print(f" {var_name}: {var_info}")
print()
return info
def validate_template_syntax(self, template_path: str):
"""Basic template validation - catch obvious errors"""
try:
with open(template_path, 'r', encoding='utf-8') as f:
content = f.read()
issues = []
# Check for common Jinja2 issues
if '{{' in content and '}}' not in content:
issues.append("Unclosed {{ expression")
if '{%' in content and '%}' not in content:
issues.append("Unclosed {% statement")
if '{#' in content and '#}' not in content:
issues.append("Unclosed {# comment")
# Check for mismatched quotes
single_quotes = content.count("'")
double_quotes = content.count('"')
if single_quotes % 2 != 0:
issues.append("Mismatched single quotes")
if double_quotes % 2 != 0:
issues.append("Mismatched double quotes")
if issues and self.debug_enabled:
print(f"⚠️ Template issues in {template_path}:")
for issue in issues:
print(f" - {issue}")
print()
return issues
except Exception as e:
if self.debug_enabled:
print(f"❌ Error validating template {template_path}: {e}")
return [f"Validation error: {e}"]
def show_render_performance(self, path: str, render_time: float, cache_hit: bool = False):
"""Show rendering performance - useful for optimization"""
if not self.debug_enabled:
return
status = "💨 CACHED" if cache_hit else "🔄 RENDERED"
print(f"{status} {path} in {render_time:.3f}s")
def create_debug_info_page(self):
"""Create comprehensive debug info for admin interface"""
debug_info = {
'timestamp': datetime.now().isoformat(),
'config': {
'content_dir': str(self.config.content_dir),
'templates_dir': str(self.config.templates_dir),
'styles_dir': str(self.config.styles_dir),
'debug_enabled': self.debug_enabled
},
'templates': self.show_available_templates(),
'content_structure': self.show_content_structure(),
}
return debug_info
# Global debug helper - can be imported anywhere
debug_helper = None
def init_debug_helper(config):
"""Initialize global debug helper"""
global debug_helper
debug_helper = DebugHelper(config)
return debug_helper
def get_debug_helper():
"""Get the global debug helper instance"""
return debug_helper

View File

@ -1,48 +1,22 @@
from dataclasses import dataclass
from src.config.config import Configuration
from src.rendering import GENERIC_FILE_MAPPING
from src.rendering.markdown import (
render_markdown,
read_raw_markdown,
rendered_markdown_to_plain_text,
from src.rendering.metadata_builders import (
MetadataBuilderFactory,
BlogPostAnalyzer,
ImageGalleryAnalyzer,
ImageMetadata,
MarkdownMetadata,
FileMetadata
)
from enum import Enum
from pathlib import Path
from PIL import Image, ExifTags
from datetime import datetime
import frontmatter
@dataclass
class ImageMetadata:
width: int
height: int
alt: str
exif: dict
@dataclass
class MarkdownMetadata:
"""
A class to represent metadata for a Markdown file.
Attributes:
----------
frontmatter : dict
A dictionary containing the front matter of the Markdown file.
content : str
The main content of the Markdown file.
preview : str
A preview or summary of the Markdown content.
"""
frontmatter: dict
content: str
preview: str
@dataclass
class FileMetadata:
typeMeta: MarkdownMetadata | None
import os
from src.server.simple_cache import app_cache, cached, get_folder_contents_cache_key, get_posts_cache_key
from src.rendering.markdown import render_markdown
@dataclass
@ -85,101 +59,46 @@ def format_date(timestamp):
class TemplateHelpers:
def __init__(self, config: Configuration):
self.config: Configuration = config
# Initialize focused metadata builders
self.metadata_factory = MetadataBuilderFactory()
self.blog_analyzer = BlogPostAnalyzer()
self.gallery_analyzer = ImageGalleryAnalyzer()
def _filter_hidden_files(self, files):
return [f for f in files if not f.name.startswith("___")]
def _build_metadata_for_file(self, path: str, categories: list[str] = []):
def _build_metadata_for_file(self, file_path, categories: list[str] = []):
"""
Builds and returns metadata for a given file based on specified categories.
Args:
path (str): The relative path to the file within the content directory.
categories (list[str], optional): A list of category strings to determine the type of metadata to extract.
Supported categories include "image" and "document".
Returns:
ImageMetadata | FileMetadata | None:
- If "image" is in categories and the file is a valid image, returns an ImageMetadata object containing
width, height, alt text, and EXIF data.
- If "document" is in categories and the file is a document (e.g., Markdown), returns a FileMetadata object
with type-specific metadata such as frontmatter, content, raw content, plain text, and preview.
- Returns None if the file cannot be processed or if no supported category matches.
Notes:
- For images, EXIF orientation is handled to ensure correct width and height.
- For Markdown documents, frontmatter and content are extracted and a text preview is generated.
- Prints an error message and returns None if image processing fails.
Simple metadata builder using focused classes
Grug-approved: clean delegation to specialized builders
"""
file_path = self.config.content_dir / path
for k in categories:
if k == "image":
try:
with Image.open(file_path) as img:
width, height = img.width, img.height
exif_raw = img._getexif()
exif = {}
if exif_raw:
orientation = exif_raw.get(0x0112, 1)
if orientation in [5, 6, 7, 8]:
width, height = height, width
exif = {
ExifTags.TAGS[k]: v
for k, v in exif_raw.items()
if k in ExifTags.TAGS
}
return ImageMetadata(
width=width,
height=height,
alt=file_path.name,
exif=exif,
)
except Exception as e:
print(f"Error processing image {file_path}: {e}")
return None
elif k == "document":
ret = None
with open(file_path, "r") as fdoc:
ret = FileMetadata(None)
if file_path.suffix[1:].lower() == "md":
ret.typeMeta = MarkdownMetadata({}, "", "")
content, c_frontmatter, obj = render_markdown(file_path)
ret.typeMeta.frontmatter = c_frontmatter
ret.typeMeta.content = content
ret.typeMeta.rawContent = read_raw_markdown(file_path)
ret.typeMeta.rawText = rendered_markdown_to_plain_text(
ret.typeMeta.content
)
ret.typeMeta.preview = ret.typeMeta.rawText[:500] + "..."
return ret
return None
full_path = self.config.content_dir / file_path
return self.metadata_factory.build_metadata(full_path, categories)
def get_folder_contents(self, path: str = ""):
"""
Retrieve the contents of a folder and return a list of TemplateFile objects.
Args:
path (str): The relative path to the folder within the content directory. Defaults to an empty string,
which refers to the root content directory.
Returns:
list: A list of TemplateFile objects representing the files and directories within the specified folder.
The function performs the following steps:
1. Constructs the full path to the folder by combining the content directory with the provided path.
2. Retrieves all files and directories within the specified folder.
3. Iterates over each file and directory, creating a TemplateFile object with metadata such as name,
path, proper name, extension, categories, date modified, date created, size in KB, metadata, directory
item count, and whether it is a directory.
4. If the item is a file, it assigns categories based on the file extension using a predefined mapping.
5. Builds additional metadata for each file.
6. Filters out hidden files from the list.
7. Returns the list of TemplateFile objects.
Now with caching to improve performance!
"""
search_contnet_path = self.config.content_dir / path
files = search_contnet_path.glob("*")
# Check cache first
cache_key = get_folder_contents_cache_key(str(self.config.content_dir), path)
# Check if folder has been modified since cache
search_content_path = self.config.content_dir / path
print(search_content_path)
if not search_content_path.exists():
return []
folder_mtime = search_content_path.stat().st_mtime
cache_key_with_mtime = f"{cache_key}:{folder_mtime}"
cached_result = app_cache.get(cache_key_with_mtime)
if cached_result is not None:
print("Cache hit")
return cached_result
# Compute folder contents
files = search_content_path.glob("*")
ret = []
for f in files:
t = TemplateFile(
@ -202,12 +121,18 @@ class TemplateHelpers:
t.metadata = self._build_metadata_for_file(f, t.categories)
if "image" in t.categories:
# Adjust date_modified and date_created to be the date the image was taken from exif if available
if t.metadata.exif and "DateTimeOriginal" in t.metadata.exif:
if t.metadata and hasattr(t.metadata, 'exif') and t.metadata.exif and "DateTimeOriginal" in t.metadata.exif:
t.date_modified = t.metadata.exif["DateTimeOriginal"]
t.date_created = t.metadata.exif["DateTimeOriginal"]
ret.append(t)
ret = self._filter_hidden_files(ret)
# Cache the result (cache for 5 minutes)
app_cache.set(cache_key_with_mtime, ret, 300)
# Clean up old cache entries for this folder
app_cache.delete(cache_key)
return ret
def get_sibling_content_files(self, path: str = ""):
@ -222,8 +147,9 @@ class TemplateHelpers:
list: A list of tuples, where each tuple contains the file name and its relative path
to the content directory. Only files that do not start with "___" are included.
"""
search_contnet_path = self.config.content_dir / path
files = search_contnet_path.glob("*")
search_content_path: Path = self.config.content_dir / path
search_content_path = search_content_path.parents[0] if len(search_content_path.parents) > 0 else search_content_path
files = search_content_path.glob("*")
return [
(file.name, str(file.relative_to(self.config.content_dir)))
for file in files
@ -265,10 +191,408 @@ class TemplateHelpers:
list of tuple: A list of tuples where each tuple contains the folder name and its relative path
to the content directory. Only directories that do not start with "___" are included.
"""
search_contnet_path = self.config.content_dir / path
files = search_contnet_path.glob("*")
search_content_path = self.config.content_dir / path
search_content_path = search_content_path.parents[0] if len(search_content_path.parents) > 0 else search_content_path
files = search_content_path.glob("*")
return [
(file.name, str(file.relative_to(self.config.content_dir)))
for file in files
if file.is_dir() and not file.name.startswith("___")
]
# Enhanced blog-focused template helpers
def get_recent_posts(self, limit: int = 5, folder: str = ""):
"""
Get recent blog posts using focused blog analyzer
Grug-approved: simple delegation to specialized tool
"""
# Check cache first
cache_key = get_posts_cache_key(str(self.config.content_dir), limit, folder)
search_path = self.config.content_dir / folder
if not search_path.exists():
return []
# Get the latest modification time for cache invalidation
latest_mtime = 0
for file_path in search_path.rglob("*.md"):
if not file_path.name.startswith("___"):
try:
mtime = file_path.stat().st_mtime
latest_mtime = max(latest_mtime, mtime)
except:
continue
cache_key_with_mtime = f"{cache_key}:{latest_mtime}"
cached_result = app_cache.get(cache_key_with_mtime)
if cached_result is not None:
return cached_result
# Use focused blog analyzer - much simpler!
posts = self.blog_analyzer.find_posts_in_directory(search_path, recursive=True)
# Convert to template-friendly format and add URLs
result = []
for post in posts[:limit]:
result.append({
'title': post['title'],
'date': post['date'],
'path': post['path'],
'metadata': {'tags': post['tags'], 'description': post['description']},
'url': f"/{post['path']}"
})
# Cache the result (cache for 10 minutes)
app_cache.set(cache_key_with_mtime, result, 600)
app_cache.delete(cache_key) # Clean up old entries
return result
def get_posts_by_tag(self, tag: str, limit: int = 10):
"""
Get posts filtered by tag - simple and useful
"""
posts = []
for file_path in self.config.content_dir.rglob("*.md"):
if file_path.name.startswith("___"):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
try:
import frontmatter
post = frontmatter.loads(content)
tags = post.metadata.get('tags', [])
if tag.lower() in [t.lower() for t in tags]:
title = post.metadata.get('title', file_path.stem)
date = post.metadata.get('date', '')
rel_path = str(file_path.relative_to(self.config.content_dir))
posts.append({
'title': title,
'date': date,
'path': rel_path,
'metadata': post.metadata,
'url': f"/{rel_path}",
'tags': tags
})
except:
continue
except Exception:
continue
# Sort by date (newest first)
posts.sort(key=lambda x: x.get('date', ''), reverse=True)
return posts[:limit]
def get_photo_albums(self):
"""
Get photo albums using focused gallery analyzer
Grug-approved: simple delegation to specialized tool
"""
galleries = self.gallery_analyzer.find_galleries(self.config.content_dir)
# Convert to template-friendly format
albums = []
for gallery in galleries:
albums.append({
'name': gallery['name'],
'path': gallery['relative_path'],
'url': f"/{gallery['relative_path']}",
'image_count': gallery['image_count'],
'total_files': gallery['total_files']
})
return albums
def get_navigation_items(self, max_items: int = 10):
"""
Get customizable navigation items - simple and practical
"""
nav_items = []
# Get top-level markdown files (excluding index)
for file_path in self.config.content_dir.glob("*.md"):
if file_path.name.startswith("___") or file_path.stem == "index":
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
title = file_path.stem
if content.startswith('---'):
try:
import frontmatter
post = frontmatter.loads(content)
title = post.metadata.get('title', title)
# Skip if marked as hidden in navigation
if post.metadata.get('nav_hidden', False):
continue
except:
pass
rel_path = str(file_path.relative_to(self.config.content_dir))
nav_items.append({
'title': title,
'url': f"/{rel_path}",
'path': rel_path
})
except Exception:
continue
# Get top-level folders
for folder_path in self.config.content_dir.glob("*"):
if not folder_path.is_dir() or folder_path.name.startswith("___"):
continue
nav_items.append({
'title': folder_path.name.replace('-', ' ').title(),
'url': f"/{folder_path.name}",
'path': folder_path.name,
'is_folder': True
})
return sorted(nav_items, key=lambda x: x['title'])[:max_items]
def generate_breadcrumbs(self, current_path: str):
"""
Generate breadcrumb navigation - simple and useful
"""
if not current_path:
return []
parts = current_path.split('/')
breadcrumbs = [{'title': 'Home', 'url': '/', 'is_current': False}]
for i, part in enumerate(parts):
if not part:
continue
path = '/'.join(parts[:i+1])
is_current = (i == len(parts) - 1)
# Try to get a better title from file metadata
title = part.replace('-', ' ').replace('_', ' ').title()
if part.endswith('.md'):
file_path = self.config.content_dir / path
if file_path.exists():
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
import frontmatter
post = frontmatter.loads(content)
title = post.metadata.get('title', title)
except:
pass
breadcrumbs.append({
'title': title,
'url': f"/{path}",
'is_current': is_current
})
return breadcrumbs
def get_related_posts(self, current_post_path: str, limit: int = 3):
"""
Get related posts based on tags and categories - simple similarity
"""
current_file = self.config.content_dir / current_post_path
if not current_file.exists():
return []
# Get current post tags
current_tags = []
try:
with open(current_file, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
import frontmatter
post = frontmatter.loads(content)
current_tags = [tag.lower() for tag in post.metadata.get('tags', [])]
except:
return []
if not current_tags:
return []
# Find posts with matching tags
related_posts = []
for file_path in self.config.content_dir.rglob("*.md"):
if file_path == current_file or file_path.name.startswith("___"):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
import frontmatter
post = frontmatter.loads(content)
post_tags = [tag.lower() for tag in post.metadata.get('tags', [])]
# Calculate tag overlap
overlap = len(set(current_tags) & set(post_tags))
if overlap > 0:
title = post.metadata.get('title', file_path.stem)
date = post.metadata.get('date', '')
rel_path = str(file_path.relative_to(self.config.content_dir))
related_posts.append({
'title': title,
'date': date,
'path': rel_path,
'url': f"/{rel_path}",
'overlap_score': overlap,
'tags': post_tags
})
except Exception:
continue
# Sort by overlap score and date
related_posts.sort(key=lambda x: (x['overlap_score'], x.get('date', '')), reverse=True)
return related_posts[:limit]
def get_all_tags(self):
"""
Get all tags used across the site with post counts
"""
tag_counts = {}
for file_path in self.config.content_dir.rglob("*.md"):
if file_path.name.startswith("___"):
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('---'):
import frontmatter
post = frontmatter.loads(content)
tags = post.metadata.get('tags', [])
for tag in tags:
tag_counts[tag] = tag_counts.get(tag, 0) + 1
except Exception:
continue
# Convert to list of dicts and sort by count
tag_list = [{'name': tag, 'count': count} for tag, count in tag_counts.items()]
tag_list.sort(key=lambda x: x['count'], reverse=True)
return tag_list
def get_rendered_markdown(self, path: str):
"""
Get rendered markdown content without Jinja2 templating.
Perfect for displaying markdown files (like index.md) within folder views.
Args:
path (str): Relative path to the markdown file within the content directory
Returns:
dict: Dictionary with 'html' (rendered content), 'metadata' (frontmatter),
and 'exists' (bool) keys. Returns None values if file doesn't exist.
Example:
{% set index = get_rendered_markdown(currentPath + '/index.md') %}
{% if index.exists %}
<div class="index-content">
{{ index.html | safe }}
</div>
{% endif %}
"""
file_path = self.config.content_dir / path
# Return empty result if file doesn't exist
if not file_path.exists() or not file_path.is_file():
return {
'html': None,
'metadata': None,
'exists': False
}
try:
# Use the existing render_markdown function which handles frontmatter
html_content, metadata, _ = render_markdown(file_path)
return {
'html': html_content,
'metadata': metadata,
'exists': True
}
except Exception as e:
# Return error state if rendering fails
return {
'html': f'<p class="error">Error rendering markdown: {str(e)}</p>',
'metadata': None,
'exists': False
}
def get_markdown_metadata(self, path: str):
"""
Get metadata (frontmatter) from a markdown file without rendering the content.
Perfect for displaying static markdown metadata in any location.
Args:
path (str): Relative path to the markdown file within the content directory
Returns:
dict: Dictionary with 'metadata' (frontmatter dict) and 'exists' (bool) keys.
Returns None for metadata if file doesn't exist or has no frontmatter.
Example:
{% set post_meta = get_markdown_metadata('blog/my-post.md') %}
{% if post_meta.exists %}
<h2>{{ post_meta.metadata.title }}</h2>
<p>{{ post_meta.metadata.description }}</p>
<span>Tags: {{ post_meta.metadata.tags | join(', ') }}</span>
{% endif %}
"""
file_path = self.config.content_dir / path
# Return empty result if file doesn't exist
if not file_path.exists() or not file_path.is_file():
return {
'metadata': None,
'exists': False
}
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check if file has frontmatter
if content.startswith('---'):
post = frontmatter.loads(content)
return {
'metadata': post.metadata,
'exists': True
}
else:
# File exists but has no frontmatter
return {
'metadata': {},
'exists': True
}
except Exception as e:
# Return error state if reading fails
return {
'metadata': None,
'exists': False,
'error': str(e)
}

View File

@ -0,0 +1,290 @@
"""
Focused metadata builders - following grug principles
Split the complex metadata building logic into focused, single-purpose classes
"""
import os
from datetime import datetime
from pathlib import Path
from PIL import Image, ExifTags
import frontmatter
from dataclasses import dataclass
from typing import Dict, List, Any, Optional
from src.rendering.markdown import render_markdown, read_raw_markdown, rendered_markdown_to_plain_text
@dataclass
class ImageMetadata:
width: int
height: int
alt: str
exif: dict
@dataclass
class MarkdownMetadata:
frontmatter: dict
content: str
preview: str
@dataclass
class FileMetadata:
typeMeta: MarkdownMetadata | None
class ImageMetadataBuilder:
"""
Simple image metadata builder - does one thing well
Grug-approved: focused, debuggable, no complexity demons
"""
def build_metadata(self, file_path: Path) -> Optional[ImageMetadata]:
"""Build metadata for an image file"""
try:
with Image.open(file_path) as img:
width, height = img.width, img.height
exif_raw = img._getexif()
exif = {}
if exif_raw:
# Handle orientation for correct width/height
orientation = exif_raw.get(0x0112, 1)
if orientation in [5, 6, 7, 8]:
width, height = height, width
# Convert EXIF tags to readable names
exif = {
ExifTags.TAGS[k]: v
for k, v in exif_raw.items()
if k in ExifTags.TAGS
}
return ImageMetadata(
width=width,
height=height,
alt=file_path.name,
exif=exif,
)
except Exception as e:
print(f"Error processing image {file_path}: {e}")
return None
class MarkdownMetadataBuilder:
"""
Simple markdown metadata builder - focused on documents
Grug-approved: one job, do it well
"""
def build_metadata(self, file_path: Path) -> Optional[MarkdownMetadata]:
"""Build metadata for a markdown file"""
if not file_path.suffix.lower() == '.md':
return None
try:
# Use existing markdown rendering functions
content, c_frontmatter, obj = render_markdown(file_path)
raw_content = read_raw_markdown(file_path)
raw_text = rendered_markdown_to_plain_text(content)
preview = raw_text[:500] + "..." if len(raw_text) > 500 else raw_text
return MarkdownMetadata(
frontmatter=c_frontmatter or {},
content=content,
preview=preview
)
except Exception as e:
print(f"Error processing markdown {file_path}: {e}")
return None
class DocumentMetadataBuilder:
"""
Simple document metadata builder - handles various document types
Currently focused on markdown but can be extended
"""
def __init__(self):
self.markdown_builder = MarkdownMetadataBuilder()
def build_metadata(self, file_path: Path) -> Optional[FileMetadata]:
"""Build metadata for any document file"""
try:
# Check if file exists and is readable
with open(file_path, "r", encoding='utf-8') as f:
# Just verify we can read it
pass
metadata = FileMetadata(typeMeta=None)
# Handle markdown files specifically
if file_path.suffix.lower() == '.md':
markdown_meta = self.markdown_builder.build_metadata(file_path)
metadata.typeMeta = markdown_meta
return metadata
except Exception as e:
print(f"Error processing document {file_path}: {e}")
return None
class MetadataBuilderFactory:
"""
Simple factory for metadata builders - no complexity demons
Grug-approved: clear, simple, does what it says
"""
def __init__(self):
self.image_builder = ImageMetadataBuilder()
self.document_builder = DocumentMetadataBuilder()
def build_metadata(self, file_path: Path, categories: List[str]) -> Optional[Any]:
"""
Build appropriate metadata based on file categories
Simple dispatch to focused builders
"""
for category in categories:
if category == "image":
return self.image_builder.build_metadata(file_path)
elif category == "document":
return self.document_builder.build_metadata(file_path)
return None
class BlogPostAnalyzer:
"""
Focused analyzer for blog posts - separate from general metadata
Grug-approved: specific job, clear purpose
"""
def extract_post_info(self, file_path: Path) -> Dict[str, Any]:
"""Extract blog-specific information from a markdown file"""
if not file_path.suffix.lower() == '.md':
return {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract frontmatter if present
post_info = {
'title': file_path.stem,
'date': '',
'tags': [],
'description': '',
'has_frontmatter': False
}
if content.startswith('---'):
try:
post = frontmatter.loads(content)
post_info.update({
'title': post.metadata.get('title', post_info['title']),
'date': post.metadata.get('date', ''),
'tags': post.metadata.get('tags', []),
'description': post.metadata.get('description', ''),
'has_frontmatter': True
})
except Exception:
pass # If frontmatter parsing fails, use defaults
# Use file modification time as fallback date
if not post_info['date']:
stat = file_path.stat()
post_info['date'] = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d')
# Ensure date is always a string for consistent sorting
if hasattr(post_info['date'], 'strftime'):
post_info['date'] = post_info['date'].strftime('%Y-%m-%d')
return post_info
except Exception as e:
print(f"Error analyzing blog post {file_path}: {e}")
return {}
def find_posts_in_directory(self, directory: Path, recursive: bool = True) -> List[Dict[str, Any]]:
"""Find all blog posts in a directory with their metadata"""
posts = []
search_pattern = "**/*.md" if recursive else "*.md"
for file_path in directory.glob(search_pattern):
if file_path.name.startswith("___"):
continue
post_info = self.extract_post_info(file_path)
if post_info:
# Add path information
post_info['path'] = str(file_path.relative_to(directory.parent))
post_info['filename'] = file_path.name
posts.append(post_info)
# Sort by date (newest first)
posts.sort(key=lambda x: x.get('date', ''), reverse=True)
return posts
class ImageGalleryAnalyzer:
"""
Focused analyzer for image galleries - separate concern
Grug-approved: one job, clear boundaries
"""
def __init__(self):
self.image_builder = ImageMetadataBuilder()
def analyze_directory(self, directory: Path) -> Dict[str, Any]:
"""Analyze a directory for image gallery potential"""
if not directory.is_dir():
return {}
files = list(directory.iterdir())
if not files:
return {}
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.svg'}
image_files = [f for f in files if f.suffix.lower() in image_extensions]
# Determine if this is a photo album (>50% images, at least 3 images)
is_gallery = len(image_files) >= 3 and len(image_files) / len(files) > 0.5
gallery_info = {
'name': directory.name,
'is_gallery': is_gallery,
'image_count': len(image_files),
'total_files': len(files),
'images': []
}
# Get metadata for each image if it's a gallery
if is_gallery:
for image_file in image_files:
image_meta = self.image_builder.build_metadata(image_file)
if image_meta:
gallery_info['images'].append({
'filename': image_file.name,
'path': str(image_file),
'metadata': image_meta
})
return gallery_info
def find_galleries(self, root_directory: Path) -> List[Dict[str, Any]]:
"""Find all potential galleries in directory tree"""
galleries = []
for item in root_directory.rglob("*"):
if item.is_dir() and not item.name.startswith("___"):
gallery_info = self.analyze_directory(item)
if gallery_info.get('is_gallery', False):
# Add relative path information
gallery_info['relative_path'] = str(item.relative_to(root_directory))
galleries.append(gallery_info)
return sorted(galleries, key=lambda x: x['name'])

View File

@ -0,0 +1,174 @@
"""
Performance monitoring for template rendering - grug-approved
Simple tracking of rendering times to identify slow pages
"""
import time
from typing import Dict, List, Optional
from dataclasses import dataclass
from threading import Lock
@dataclass
class RenderStats:
path: str
render_time: float
cache_hit: bool
template_used: Optional[str]
timestamp: float
class PerformanceMonitor:
"""
Simple performance monitor - tracks template rendering times
Grug-approved: no complexity, just useful info when things slow
"""
def __init__(self, max_entries: int = 100):
self.max_entries = max_entries
self.render_stats: List[RenderStats] = []
self.lock = Lock()
def record_render(self, path: str, render_time: float, cache_hit: bool = False, template_used: str = None):
"""Record a template render operation"""
with self.lock:
stat = RenderStats(
path=path,
render_time=render_time,
cache_hit=cache_hit,
template_used=template_used,
timestamp=time.time()
)
self.render_stats.append(stat)
# Keep only recent entries
if len(self.render_stats) > self.max_entries:
self.render_stats = self.render_stats[-self.max_entries:]
def get_slow_pages(self, threshold: float = 0.1, limit: int = 10) -> List[RenderStats]:
"""Get pages that render slowly (above threshold in seconds)"""
with self.lock:
slow_pages = [
stat for stat in self.render_stats
if stat.render_time > threshold and not stat.cache_hit
]
# Sort by render time, slowest first
slow_pages.sort(key=lambda x: x.render_time, reverse=True)
return slow_pages[:limit]
def get_cache_efficiency(self) -> Dict[str, float]:
"""Get cache hit rate statistics"""
with self.lock:
if not self.render_stats:
return {'hit_rate': 0.0, 'total_requests': 0, 'cache_hits': 0}
total = len(self.render_stats)
cache_hits = sum(1 for stat in self.render_stats if stat.cache_hit)
return {
'hit_rate': (cache_hits / total) * 100 if total > 0 else 0.0,
'total_requests': total,
'cache_hits': cache_hits
}
def get_average_render_times(self) -> Dict[str, float]:
"""Get average render times by path"""
with self.lock:
path_times = {}
path_counts = {}
for stat in self.render_stats:
if not stat.cache_hit: # Only count actual renders, not cache hits
if stat.path not in path_times:
path_times[stat.path] = 0.0
path_counts[stat.path] = 0
path_times[stat.path] += stat.render_time
path_counts[stat.path] += 1
# Calculate averages
averages = {}
for path in path_times:
if path_counts[path] > 0:
averages[path] = path_times[path] / path_counts[path]
return dict(sorted(averages.items(), key=lambda x: x[1], reverse=True))
def get_recent_activity(self, limit: int = 20) -> List[RenderStats]:
"""Get recent rendering activity"""
with self.lock:
# Return most recent entries
return list(reversed(self.render_stats[-limit:]))
def get_performance_summary(self) -> Dict:
"""Get comprehensive performance summary"""
cache_stats = self.get_cache_efficiency()
slow_pages = self.get_slow_pages()
avg_times = self.get_average_render_times()
with self.lock:
if not self.render_stats:
return {
'total_renders': 0,
'overall_average_time': 0.0,
'cache_efficiency': cache_stats,
'slow_pages': [],
'average_times': {},
'recent_activity': []
}
# Calculate overall statistics
non_cached_renders = [s for s in self.render_stats if not s.cache_hit]
overall_avg = sum(s.render_time for s in non_cached_renders) / len(non_cached_renders) if non_cached_renders else 0
return {
'total_renders': len(self.render_stats),
'overall_average_time': overall_avg,
'cache_efficiency': cache_stats,
'slow_pages': slow_pages[:5], # Top 5 slowest
'average_times': dict(list(avg_times.items())[:10]), # Top 10 by avg time
'recent_activity': self.get_recent_activity(10)
}
def clear_stats(self):
"""Clear all performance statistics"""
with self.lock:
self.render_stats.clear()
# Global performance monitor instance
performance_monitor = PerformanceMonitor()
def get_performance_monitor():
"""Get the global performance monitor instance"""
return performance_monitor
class RenderTimer:
"""
Context manager for timing template renders
Grug-approved: simple, automatic timing
"""
def __init__(self, path: str, template_used: str = None, cache_hit: bool = False):
self.path = path
self.template_used = template_used
self.cache_hit = cache_hit
self.start_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.start_time:
render_time = time.time() - self.start_time
performance_monitor.record_render(
self.path,
render_time,
self.cache_hit,
self.template_used
)

View File

@ -5,6 +5,9 @@ from flask import render_template_string, send_file
from src.rendering import GENERIC_FILE_MAPPING
from src.rendering.markdown import render_markdown
from src.rendering.template_discovery import TemplateDiscovery
from src.rendering.debug_helpers import get_debug_helper
from src.rendering.performance_monitor import RenderTimer
def count_file_extensions(path):
@ -126,6 +129,7 @@ def render_page(
error_description="The requested resource was not found on this server.",
template_path=template_path,
)
target_path = path
target_file = path
if path.is_file():
@ -135,63 +139,26 @@ def render_page(
relative_path = target_file.relative_to(base_path)
relative_dir = target_path.relative_to(base_path)
"""
The styles are ordered in the following manner:
# Use new simplified template and style discovery system
template_discovery = TemplateDiscovery(template_path)
Specific style for the target path (e.g., /path/to/target.css).
Specific styles for the type and extension in the current and parent directories
(e.g., /path/to/__file.html.css).
Specific styles for the type and category in the current and parent directories
(e.g., /path/to/__file.document.css).
Base style (/base.css).
This ordering ensures that the most specific styles are applied first, followed by
more general styles, and finally the base style.
"""
styles = []
styles.append("/" + str(relative_path) + ".css")
# Find styles using simplified discovery
style_candidates = template_discovery.find_style_candidates(relative_path, type, category, extension)
styles = [s for s in style_candidates if (style_path / s[1:]).exists()]
search_path = style_path / relative_dir
while search_path >= style_path:
if (search_path / f"__{type}.{extension}.css").exists():
styles.append(
"/"
+ str(search_path.relative_to(style_path))
+ f"/__{type}.{extension}.css"
)
for c in reversed(category):
if (search_path / f"__{type}.{c}.css").exists():
styles.append(
"/"
+ str(search_path.relative_to(style_path))
+ f"/__{type}.{c}.css"
)
search_path = search_path.parent
# Find template using simplified discovery
found_template = template_discovery.find_template(relative_path, type, category, extension)
styles.append("/base.css")
# Debug logging for developers
debug_helper = get_debug_helper()
if debug_helper:
debug_helper.log_template_search(
str(relative_path),
str(found_template) if found_template else None,
template_discovery.get_last_search_candidates()
)
styles = [t for t in styles if (style_path / t[1:]).exists()]
templates = []
if type == "folder":
if (template_path / relative_dir / "__folder.html").exists():
templates.append(relative_dir / "__folder.html")
else:
if (template_path / (str(relative_path) + ".html")).exists():
templates.append(template_path / (str(relative_path) + ".html"))
if len(templates) == 0:
search_path = template_path / relative_dir
while search_path >= template_path:
if (search_path / f"__{type}.{extension}.html").exists():
templates.append(search_path / f"__{type}.{extension}.html")
break
for c in reversed(category):
if (search_path / f"__{type}.{c}.html").exists():
templates.append(search_path / f"__{type}.{c}.html")
break
search_path = search_path.parent
if len(templates) == 0:
if found_template is None:
if type == "file":
return send_file(target_file)
else:
@ -211,8 +178,8 @@ def render_page(
raise Exception("Base template not found")
# The first found template is the most specific one for the content.
page_template_path = templates[0]
# Use the found template from our simplified discovery system
page_template_path = found_template
template_vars = {
"content": content,

View File

@ -0,0 +1,221 @@
"""
Smart Template Discovery System - Grug-approved simplification
Replaces the complex nested template discovery with simple, debuggable logic
"""
from pathlib import Path
from typing import Optional, List
class TemplateDiscovery:
"""
Simple template discovery that maintains all current functionality
Following grug principles: clear priority order, easy to debug
"""
def __init__(self, template_root: Path):
self.template_root = Path(template_root)
self._last_search_candidates = []
def find_template(self, content_path: Path, file_type: str, categories: List[str], extension: str) -> Optional[Path]:
"""
Find the best template for the given content
Uses simple priority list instead of complex nested loops
"""
# Build candidate template names in priority order
candidates = self._build_candidate_list(content_path, file_type, categories, extension)
self._last_search_candidates = candidates.copy() # Store for debugging
# Search from current directory up to template root (preserve hierarchy)
search_path = self.template_root / content_path.parent
while search_path >= self.template_root:
for candidate in candidates:
template_path = search_path / candidate
if template_path.exists():
return template_path
search_path = search_path.parent
return None
def get_last_search_candidates(self) -> List[str]:
"""Get the candidates from the last template search - for debugging"""
return self._last_search_candidates.copy()
def _build_candidate_list(self, content_path: Path, file_type: str, categories: List[str], extension: str) -> List[str]:
"""
Build list of template candidates in priority order
This replaces the complex nested loop logic with simple, clear ordering
"""
candidates = []
# 1. Specific file template (highest priority)
candidates.append(f"{content_path.stem}.html")
# 2. Type + extension templates
if extension:
candidates.append(f"__{file_type}.{extension}.html")
# 3. Type + category templates (preserve current category system)
for category in reversed(categories): # Most specific categories first
candidates.append(f"__{file_type}.{category}.html")
# 4. Generic type template
candidates.append(f"__{file_type}.html")
# 5. Default template (for ultimate fallback)
candidates.append("default.html")
return candidates
def find_style_candidates(self, content_path: Path, file_type: str, categories: List[str], extension: str) -> List[str]:
"""
Find CSS style candidates - similar logic to templates but for styles
Returns list of potential style paths in priority order
"""
candidates = []
# 1. Specific style for the content path
candidates.append(f"/{content_path}.css")
# 2. Type + extension styles in current and parent directories
search_path = content_path.parent
while True:
if extension:
candidates.append(f"/{search_path}/__{file_type}.{extension}.css")
# 3. Type + category styles
for category in reversed(categories):
candidates.append(f"/{search_path}/__{file_type}.{category}.css")
if search_path == Path('.'):
break
search_path = search_path.parent
# 4. Base style (always included)
candidates.append("/base.css")
return candidates
def debug_template_discovery(self, content_path: Path, file_type: str, categories: List[str], extension: str) -> dict:
"""
Debug information for template discovery - helps with troubleshooting
Grug-approved: when things break, need easy way to see what's happening
"""
candidates = self._build_candidate_list(content_path, file_type, categories, extension)
debug_info = {
'content_path': str(content_path),
'file_type': file_type,
'categories': categories,
'extension': extension,
'candidates': candidates,
'search_paths': [],
'found_templates': [],
'chosen_template': None
}
# Check each search path
search_path = self.template_root / content_path.parent
while search_path >= self.template_root:
debug_info['search_paths'].append(str(search_path))
for candidate in candidates:
template_path = search_path / candidate
if template_path.exists():
debug_info['found_templates'].append(str(template_path))
if debug_info['chosen_template'] is None:
debug_info['chosen_template'] = str(template_path)
search_path = search_path.parent
return debug_info
class LegacyTemplateDiscovery:
"""
Wrapper around the original complex template discovery for comparison
Helps ensure we don't break anything during migration
"""
def __init__(self, template_root: Path):
self.template_root = Path(template_root)
def find_template_legacy(self, content_path: Path, file_type: str, categories: List[str], extension: str) -> Optional[Path]:
"""
Original complex template discovery logic (from renderer.py)
Kept for comparison and gradual migration
"""
templates = []
# Check for folder template
if file_type == "folder":
folder_template = self.template_root / content_path / "__folder.html"
if folder_template.exists():
templates.append(folder_template)
else:
# Check for specific file template
specific_template = self.template_root / f"{content_path}.html"
if specific_template.exists():
templates.append(specific_template)
# If no specific template found, search with complex nested logic
if len(templates) == 0:
search_path = self.template_root / content_path.parent
while search_path >= self.template_root:
# Check type + extension
type_ext_template = search_path / f"__{file_type}.{extension}.html"
if type_ext_template.exists():
templates.append(type_ext_template)
break
# Check type + categories
for category in reversed(categories):
type_cat_template = search_path / f"__{file_type}.{category}.html"
if type_cat_template.exists():
templates.append(type_cat_template)
break
search_path = search_path.parent
return templates[0] if templates else None
def migrate_template_discovery(old_discovery: LegacyTemplateDiscovery, new_discovery: TemplateDiscovery) -> dict:
"""
Migration helper to compare old vs new template discovery
Helps ensure we don't break anything
"""
test_cases = [
# Common test cases
(Path("index.md"), "file", ["document"], "md"),
(Path("posts/my-post.md"), "file", ["document"], "md"),
(Path("images/gallery"), "folder", ["image"], None),
(Path("about.md"), "file", ["document"], "md"),
]
results = {
'total_tests': len(test_cases),
'matching': 0,
'differences': [],
'test_details': []
}
for content_path, file_type, categories, extension in test_cases:
old_result = old_discovery.find_template_legacy(content_path, file_type, categories, extension)
new_result = new_discovery.find_template(content_path, file_type, categories, extension)
test_detail = {
'content_path': str(content_path),
'old_result': str(old_result) if old_result else None,
'new_result': str(new_result) if new_result else None,
'matches': old_result == new_result
}
results['test_details'].append(test_detail)
if old_result == new_result:
results['matching'] += 1
else:
results['differences'].append(test_detail)
return results

View File

@ -0,0 +1,489 @@
"""
Admin interface templates - split from the monolithic file_manager.py template
Following grug principles: focused, embedded templates that are easy to debug
"""
# Shared CSS for all admin templates
ADMIN_CSS = """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0; padding: 20px; background: #f5f5f5;
}
.admin-container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
.admin-header { border-bottom: 1px solid #eee; padding-bottom: 15px; margin-bottom: 20px; }
.admin-nav { margin-bottom: 20px; }
.admin-nav a { margin-right: 15px; padding: 8px 16px; background: #007cba; color: white; text-decoration: none; border-radius: 4px; }
.admin-nav a:hover { background: #005a87; }
.admin-nav a.active { background: #333; }
.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; }
.file-item { padding: 15px; border: 1px solid #ddd; border-radius: 6px; background: #fafafa; }
.file-item:hover { background: #f0f0f0; }
.file-actions { margin-top: 10px; }
.file-actions a { margin-right: 8px; font-size: 12px; color: #666; }
.clipboard { background: #e8f4fd; border: 1px solid #bee5eb; padding: 15px; margin: 15px 0; border-radius: 6px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
.form-group input, .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.btn { padding: 10px 20px; background: #007cba; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn:hover { background: #005a87; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #c82333; }
.success { color: #28a745; padding: 10px; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; margin: 10px 0; }
.error { color: #dc3545; padding: 10px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; margin: 10px 0; }
</style>
"""
# JavaScript is now embedded directly in the base template
def get_admin_base_template():
"""Base template for all admin pages"""
return f"""
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{{{ title or 'Foldsite Admin' }}}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{ADMIN_CSS}
</head>
<body>
<div class="admin-container">
<div class="admin-header">
<h1>{{{{ title or 'Foldsite Admin' }}}}</h1>
<div class="admin-nav">
<a href="{{{{ url_for('filemanager.dashboard') }}}}" {{% if current_page == 'dashboard' %}}class="active"{{% endif %}}>📁 Files</a>
<a href="{{{{ url_for('filemanager.list_posts') }}}}" {{% if current_page == 'posts' %}}class="active"{{% endif %}}>📝 Posts</a>
<a href="{{{{ url_for('filemanager.post_editor') }}}}" {{% if current_page == 'editor' %}}class="active"{{% endif %}}>✏️ New Post</a>
<a href="{{{{ url_for('filemanager.image_manager') }}}}" {{% if current_page == 'images' %}}class="active"{{% endif %}}>🖼️ Images</a>
<a href="{{{{ url_for('filemanager.settings') }}}}" {{% if current_page == 'settings' %}}class="active"{{% endif %}}>⚙️ Settings</a>
<form method="post" action="{{{{ url_for('filemanager.logout') }}}}" style="display: inline;">
<button type="submit" class="btn btn-danger" style="margin-left: 20px;">Logout</button>
</form>
</div>
</div>
{{% with messages = get_flashed_messages() %}}
{{% if messages %}}
{{% for message in messages %}}
<div class="success">{{{{ message }}}}</div>
{{% endfor %}}
{{% endif %}}
{{% endwith %}}
<div class="admin-content">
{{{{ content|safe }}}}
</div>
</div>
<!-- Admin JavaScript - Load at end of body -->
<script>
// Admin functions that need to be globally available
function deleteItem(path) {{
if(confirm("Are you sure you want to delete " + path + "?")) {{
submitForm('/admin/delete', {{path: path}});
}}
}}
function renameItem(path) {{
var newName = prompt("Enter new name for " + path);
if(newName) {{
submitForm('/admin/rename', {{path: path, new_name: newName}});
}}
}}
function cutItem(path) {{
submitForm('/admin/cut', {{paths: path}});
}}
function bulkCut() {{
var checkboxes = document.querySelectorAll('.bulk:checked');
if(checkboxes.length === 0) {{
alert("No items selected");
return;
}}
var paths = Array.from(checkboxes).map(cb => cb.value);
submitForm('/admin/cut', {{paths: paths}});
}}
function submitForm(action, data) {{
var form = document.createElement("form");
form.method = "post";
form.action = action;
for (var key in data) {{
if (Array.isArray(data[key])) {{
data[key].forEach(val => {{
var inp = document.createElement("input");
inp.type = "hidden";
inp.name = key;
inp.value = val;
form.appendChild(inp);
}});
}} else {{
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}}
}}
document.body.appendChild(form);
form.submit();
}}
function previewMarkdown() {{
var content = document.getElementById('markdown-content');
var preview = document.getElementById('preview');
if (!content || !preview) return;
if (!content.value.trim()) {{
preview.innerHTML = '<p style="color: #666; font-style: italic;">Preview will appear here...</p>';
return;
}}
// Basic markdown preview client-side
var html = content.value
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
.replace(/\\*(.+?)\\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/\\[(.+?)\\]\\((.+?)\\)/g, '<a href="$2">$1</a>')
.replace(/!\\[(.+?)\\]\\((.+?)\\)/g, '<img src="$2" alt="$1" style="max-width: 100%;">')
.replace(/\\n\\n/g, '</p><p>')
.replace(/\\n/g, '<br>');
preview.innerHTML = '<p>' + html + '</p>';
}}
</script>
</body>
</html>
"""
def get_dashboard_template():
"""File browser dashboard template - focused and clean"""
return """
<div class="breadcrumbs" style="margin-bottom: 20px;">
<strong>Current Directory:</strong> /{{ rel_path or '' }}
{% if rel_path %}
<a href="{{ url_for('filemanager.dashboard', path=parent) }}" style="margin-left: 15px;">⬆️ Go up</a>
{% endif %}
</div>
{% if session.clipboard %}
<div class="clipboard">
<h3>📋 Clipboard ({{ session.clipboard|length }} items)</h3>
<ul style="margin: 10px 0;">
{% for item in session.clipboard %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<form method="post" action="{{ url_for('filemanager.paste') }}" style="display: inline;">
<input type="hidden" name="dest" value="{{ rel_path }}">
<button type="submit" class="btn">📥 Paste Here</button>
</form>
<form method="post" action="{{ url_for('filemanager.cancel_move') }}" style="display: inline; margin-left: 10px;">
<button type="submit" class="btn btn-danger">❌ Cancel</button>
</form>
</div>
{% endif %}
<div class="file-grid">
{% for item in items %}
<div class="file-item">
<input type="checkbox" class="bulk" value="{{ item.path }}" style="float: right;">
{% if item.is_dir %}
<div style="font-size: 24px;">📁</div>
<strong><a href="{{ url_for('filemanager.dashboard', path=item.path) }}">{{ item.name }}</a></strong>
<div style="color: #666; font-size: 12px;">Folder</div>
{% else %}
<div style="font-size: 24px;">
{% if item.name.endswith('.md') %}📝
{% elif item.name.endswith(('.jpg', '.jpeg', '.png', '.gif')) %}🖼️
{% else %}📄{% endif %}
</div>
<strong>{{ item.name }}</strong>
<div style="color: #666; font-size: 12px;">{{ "%.1f"|format(item.size/1024) }} KB</div>
{% if item.name.endswith('.md') %}
<div class="file-actions">
<a href="{{ url_for('filemanager.edit_post', filename=item.path) }}">✏️ Edit</a>
</div>
{% endif %}
{% endif %}
<div class="file-actions">
{% if not item.is_dir %}
<a href="{{ url_for('filemanager.download', path=item.path) }}">⬇️ Download</a>
{% endif %}
<a href="#" onclick="renameItem('{{ item.path }}'); return false;">📝 Rename</a>
<a href="#" onclick="cutItem('{{ item.path }}'); return false;">✂️ Cut</a>
<a href="#" onclick="deleteItem('{{ item.path }}'); return false;" style="color: #dc3545;">🗑️ Delete</a>
</div>
</div>
{% endfor %}
</div>
<div style="margin: 20px 0;">
<button onclick="bulkCut()" class="btn">✂️ Cut Selected</button>
</div>
<hr style="margin: 30px 0;">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px;">
<div>
<h3>⚡ Quick Post</h3>
<form action="{{ url_for('filemanager.quick_post') }}" method="post">
<div class="form-group">
<input type="text" name="title" placeholder="Post title..." required>
</div>
<button type="submit" class="btn">⚡ Create Post</button>
</form>
</div>
<div>
<h3>📤 Upload Files</h3>
<form action="{{ url_for('filemanager.upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="path" value="{{ rel_path }}">
<div class="form-group">
<input type="file" name="file" multiple style="width: 100%;">
</div>
<button type="submit" class="btn">📤 Upload</button>
</form>
</div>
<div>
<h3>📁 Create Directory</h3>
<form action="{{ url_for('filemanager.mkdir') }}" method="post">
<input type="hidden" name="path" value="{{ rel_path }}">
<div class="form-group">
<input type="text" name="dirname" placeholder="Directory name" required>
</div>
<button type="submit" class="btn">📁 Create</button>
</form>
</div>
</div>
"""
def get_post_editor_template():
"""Markdown post editor with preview"""
return """
<h2>✏️ Create New Post</h2>
<form method="post" action="{{ url_for('filemanager.create_post') }}">
{% if editing_file %}
<input type="hidden" name="editing_file" value="{{ editing_file }}">
{% endif %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<div class="form-group">
<label for="title">Post Title</label>
<input type="text" id="title" name="title" value="{{ title or '' }}" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<input type="text" id="description" name="description" value="{{ description or '' }}">
</div>
<div class="form-group">
<label for="tags">Tags (comma-separated)</label>
<input type="text" id="tags" name="tags" value="{{ tags or '' }}" placeholder="blog, tutorial, tech">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="add-date" name="add_date" {% if not editing_file %}checked{% endif %}>
Add date prefix to filename
</label>
</div>
<div class="form-group">
<label for="folder">Save in folder</label>
<input type="text" id="folder" name="folder" value="{{ folder or '' }}" placeholder="/ (root)" list="folder-suggestions">
<datalist id="folder-suggestions">
<option value="/">
<option value="posts/">
<option value="blog/">
<option value="articles/">
<option value="drafts/">
</datalist>
</div>
</div>
<div>
<div class="form-group">
<label>Preview</label>
<div id="preview" style="border: 1px solid #ddd; padding: 15px; min-height: 150px; background: #fafafa;">
Preview will appear here...
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="markdown-content">Content (Markdown)</label>
<textarea id="markdown-content" name="content" rows="15" placeholder="# Your post content here...
Write your post content in Markdown format. You can use:
- **Bold text**
- *Italic text*
- [Links](http://example.com)
- ![Images](image.jpg)
- `Code snippets`
And much more!" onkeyup="previewMarkdown()">{{ content or '' }}</textarea>
</div>
<div style="margin-top: 20px;">
<button type="submit" class="btn">💾 Save Post</button>
<button type="button" onclick="previewMarkdown()" class="btn" style="margin-left: 10px;">👁️ Preview</button>
</div>
<script>
// Auto-preview on load and typing
window.onload = function() {
previewMarkdown();
document.getElementById('markdown-content').addEventListener('input', previewMarkdown);
};
</script>
</form>
"""
def get_image_manager_template():
"""Image upload and management interface"""
return """
<h2>🖼️ Image Manager</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px;">
<div>
<h3>📤 Upload Images</h3>
<form action="{{ url_for('filemanager.upload_images') }}" method="post" enctype="multipart/form-data">
<div class="form-group">
<input type="file" name="images" multiple accept="image/*" style="width: 100%;">
</div>
<div class="form-group">
<label for="image-folder">Save to folder</label>
<input type="text" id="image-folder" name="folder" placeholder="images/ (optional)">
</div>
<button type="submit" class="btn">📤 Upload Images</button>
</form>
</div>
<div>
<h3>🎨 Quick Actions</h3>
<button onclick="createImageGallery()" class="btn" style="margin-bottom: 10px; width: 100%;">📸 Create Photo Gallery</button>
<button onclick="optimizeImages()" class="btn" style="margin-bottom: 10px; width: 100%;">⚡ Optimize All Images</button>
<button onclick="generateThumbnails()" class="btn" style="width: 100%;">🔍 Generate Thumbnails</button>
</div>
</div>
<h3>📁 Image Folders</h3>
<div class="file-grid">
{% for folder in image_folders %}
<div class="file-item">
<div style="font-size: 24px;">📸</div>
<strong><a href="{{ url_for('filemanager.view_images', folder=folder.path) }}">{{ folder.name }}</a></strong>
<div style="color: #666; font-size: 12px;">{{ folder.image_count }} images</div>
<div class="file-actions">
<a href="{{ url_for('filemanager.view_images', folder=folder.path) }}">👁️ View</a>
<a href="#" onclick="createGalleryFromFolder('{{ folder.path }}')">📝 Gallery Page</a>
</div>
</div>
{% endfor %}
</div>
<script>
function createImageGallery() {
var folder = prompt("Create gallery for which folder?", "images/");
if (folder) {
submitForm('/admin/create-gallery', {folder: folder});
}
}
function optimizeImages() {
if (confirm("This will optimize all images. Continue?")) {
submitForm('/admin/optimize-images', {});
}
}
function generateThumbnails() {
if (confirm("Generate thumbnails for all images?")) {
submitForm('/admin/generate-thumbnails', {});
}
}
function createGalleryFromFolder(folder) {
submitForm('/admin/create-gallery', {folder: folder});
}
</script>
"""
def get_settings_template():
"""Basic site settings"""
return """
<h2>⚙️ Site Settings</h2>
<form method="post" action="{{ url_for('filemanager.save_settings') }}">
<div class="form-group">
<label for="site-title">Site Title</label>
<input type="text" id="site-title" name="site_title" value="{{ config.get('site_title', '') }}">
</div>
<div class="form-group">
<label for="site-description">Site Description</label>
<textarea id="site-description" name="site_description" rows="3">{{ config.get('site_description', '') }}</textarea>
</div>
<div class="form-group">
<label for="author">Author Name</label>
<input type="text" id="author" name="author" value="{{ config.get('author', '') }}">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="enable_comments" {{ 'checked' if config.get('enable_comments') else '' }}>
Enable Comments
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="auto_optimize_images" {{ 'checked' if config.get('auto_optimize_images') else '' }}>
Auto-optimize uploaded images
</label>
</div>
<button type="submit" class="btn">💾 Save Settings</button>
</form>
<hr style="margin: 30px 0;">
<h3>🔧 Maintenance</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<form method="post" action="{{ url_for('filemanager.clear_cache') }}">
<button type="submit" class="btn" style="width: 100%;">🗑️ Clear Cache</button>
</form>
<a href="{{ url_for('filemanager.debug_info') }}" class="btn" style="width: 100%; text-align: center; text-decoration: none; display: block;">🔧 Debug Info</a>
<a href="{{ url_for('filemanager.performance_stats') }}" class="btn" style="width: 100%; text-align: center; text-decoration: none; display: block;">⚡ Performance</a>
<button onclick="alert('Backup functionality coming soon!')" class="btn" style="width: 100%;">💾 Backup Content</button>
<button onclick="alert('RSS generation functionality coming soon!')" class="btn" style="width: 100%;">📡 Regenerate RSS</button>
</div>
{% if cache_stats %}
<h3>📊 Cache Statistics</h3>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 15px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
<div><strong>Active Entries:</strong> {{ cache_stats.active_entries }}</div>
<div><strong>Total Entries:</strong> {{ cache_stats.total_entries }}</div>
<div><strong>Memory Usage:</strong> ~{{ "%.1f"|format(cache_stats.memory_usage_estimate/1024) }}KB</div>
</div>
</div>
{% endif %}
"""

View File

@ -0,0 +1,973 @@
"""
Enhanced File Manager - Grug-approved replacement for the monolithic file_manager.py
Split into focused, debuggable components while keeping everything embedded
"""
import os
import shutil
from datetime import datetime
from pathlib import Path
from flask import Blueprint, request, render_template_string, send_from_directory, redirect, url_for, flash, session
from werkzeug.utils import secure_filename
from .admin_templates import (
get_admin_base_template,
get_dashboard_template,
get_post_editor_template,
get_image_manager_template,
get_settings_template
)
from .image_optimizer import ImageOptimizer, bulk_optimize_images, create_image_gallery_data
from .simple_cache import get_cache_stats, clear_all_cache, invalidate_content_cache
from ..rendering.debug_helpers import get_debug_helper
from ..rendering.performance_monitor import get_performance_monitor
def create_enhanced_filemanager_blueprint(base_dir, url_prefix='/admin', auth_password=None):
"""
Enhanced file manager with focused components following grug principles
"""
base_dir = os.path.abspath(base_dir)
os.makedirs(base_dir, exist_ok=True)
filemanager = Blueprint('filemanager', __name__, url_prefix=url_prefix)
def secure_path(path):
"""Ensure path stays within base_dir"""
safe_path = os.path.abspath(os.path.join(base_dir, path))
if not safe_path.startswith(base_dir):
raise Exception("Invalid path")
return safe_path
def render_admin_page(template_content, title="Foldsite Admin", current_page="dashboard", **kwargs):
"""Render admin page with base template"""
base_template = get_admin_base_template()
return render_template_string(
base_template,
content=template_content,
title=title,
current_page=current_page,
**kwargs
)
@filemanager.before_request
def require_login():
if auth_password is not None:
if request.endpoint in ['filemanager.login', 'filemanager.logout']:
return None
if not session.get('filemanager_authenticated'):
return redirect(url_for('filemanager.login', next=request.url))
return None
@filemanager.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
password = request.form.get('password', '')
if password == auth_password:
session['filemanager_authenticated'] = True
flash("Logged in successfully")
next_url = request.args.get('next') or url_for('filemanager.dashboard')
return redirect(next_url)
else:
flash("Incorrect password")
login_template = """
<!doctype html>
<html>
<head><title>Foldsite Admin - Login</title></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5;">
<div style="background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); min-width: 300px;">
<h1 style="text-align: center; margin-bottom: 30px;">🔐 Foldsite Admin</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div style="color: #dc3545; padding: 10px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; margin-bottom: 15px;">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Password</label>
<input type="password" name="password" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" required>
</div>
<button type="submit" style="width: 100%; padding: 12px; background: #007cba; color: white; border: none; border-radius: 4px; cursor: pointer;">Login</button>
</form>
</div>
</body>
</html>
"""
return render_template_string(login_template)
@filemanager.route('/logout', methods=['POST'])
def logout():
session.pop('filemanager_authenticated', None)
flash("Logged out")
return redirect(url_for('filemanager.login'))
@filemanager.route('/')
@filemanager.route('/dashboard')
def dashboard():
"""Main file browser dashboard"""
rel_path = request.args.get('path', '')
try:
abs_path = secure_path(rel_path)
except Exception:
return "Invalid path", 400
if not os.path.isdir(abs_path):
return "Not a directory", 400
# Build file listing
items = []
for entry in os.listdir(abs_path):
entry_path = os.path.join(abs_path, entry)
rel_entry_path = os.path.join(rel_path, entry) if rel_path else entry
# Get file stats
stat = os.stat(entry_path)
items.append({
'name': entry,
'is_dir': os.path.isdir(entry_path),
'path': rel_entry_path,
'size': stat.st_size,
'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
})
items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
parent = os.path.dirname(rel_path) if rel_path else ''
template_content = render_template_string(
get_dashboard_template(),
items=items,
rel_path=rel_path,
parent=parent
)
return render_admin_page(template_content, title="File Manager", current_page="dashboard")
@filemanager.route('/editor')
def post_editor():
"""New post editor interface"""
template_content = render_template_string(get_post_editor_template())
return render_admin_page(template_content, title="New Post", current_page="editor")
@filemanager.route('/edit/<path:filename>')
def edit_post(filename):
"""Edit existing markdown post"""
try:
abs_path = secure_path(filename)
except Exception:
flash("Invalid file path")
return redirect(url_for('filemanager.dashboard'))
if not os.path.isfile(abs_path) or not filename.endswith('.md'):
flash("File not found or not a markdown file")
return redirect(url_for('filemanager.dashboard'))
# Read existing content
with open(abs_path, 'r', encoding='utf-8') as f:
content = f.read()
# Parse frontmatter if it exists
title = os.path.splitext(os.path.basename(filename))[0]
description = ""
tags = ""
if content.startswith('---'):
try:
import frontmatter
post = frontmatter.loads(content)
title = post.metadata.get('title', title)
description = post.metadata.get('description', '')
tags = ', '.join(post.metadata.get('tags', []))
content = post.content
except:
pass # If frontmatter parsing fails, use raw content
template_content = render_template_string(
get_post_editor_template(),
title=title,
description=description,
tags=tags,
content=content,
editing_file=filename
)
return render_admin_page(template_content, title=f"Edit: {title}", current_page="editor")
@filemanager.route('/create-post', methods=['POST'])
def create_post():
"""Create new blog post"""
title = request.form.get('title', '').strip()
description = request.form.get('description', '').strip()
tags = request.form.get('tags', '').strip()
folder = request.form.get('folder', '').strip()
content = request.form.get('content', '').strip()
editing_file = request.form.get('editing_file', '').strip()
add_date = request.form.get('add_date') == 'on'
if not title:
flash("Title is required")
return redirect(url_for('filemanager.post_editor'))
# Generate filename
if editing_file:
# Editing existing file - keep same filename
filename = editing_file
else:
# Create new file - handle date prefix and folder properly
safe_title = title.replace('/', '-').replace('\\', '-').replace(':', '-')
if add_date:
today = datetime.now().strftime('%Y-%m-%d')
filename = f"{today}-{safe_title}.md"
else:
filename = f"{safe_title}.md"
# Handle folder properly - clean up and ensure proper path
if folder and folder.strip() and folder.strip() != '/':
# Remove leading/trailing slashes and ensure clean path
folder = folder.strip().strip('/')
if folder:
# Allow spaces in folder names but make them filesystem-safe for creation
safe_folder = folder.replace('\\', '/').replace(':', '-')
filename = os.path.join(safe_folder, filename)
try:
abs_path = secure_path(filename)
# Create directory if needed
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
# Build frontmatter
frontmatter_data = {
'title': title,
'date': datetime.now().strftime('%Y-%m-%d'),
}
if description:
frontmatter_data['description'] = description
if tags:
frontmatter_data['tags'] = [tag.strip() for tag in tags.split(',') if tag.strip()]
# Build full content
frontmatter_lines = ['---']
for key, value in frontmatter_data.items():
if isinstance(value, list):
frontmatter_lines.append(f"{key}:")
for item in value:
frontmatter_lines.append(f" - {item}")
else:
frontmatter_lines.append(f"{key}: {value}")
frontmatter_lines.append('---')
frontmatter_lines.append('')
full_content = '\n'.join(frontmatter_lines) + content
# Write file
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(full_content)
action = "updated" if editing_file else "created"
flash(f"Post '{title}' {action} successfully!")
# Invalidate content cache when posts are modified
invalidate_content_cache(base_dir)
return redirect(url_for('filemanager.dashboard'))
except Exception as e:
flash(f"Error saving post: {str(e)}")
return redirect(url_for('filemanager.post_editor'))
@filemanager.route('/images')
def image_manager():
"""Image management interface"""
# Find image folders
image_folders = []
for root, dirs, files in os.walk(base_dir):
image_files = [f for f in files if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg'))]
if image_files:
rel_path = os.path.relpath(root, base_dir)
if rel_path == '.':
rel_path = ''
folder_name = os.path.basename(root) if rel_path else 'root'
image_folders.append({
'name': folder_name,
'path': rel_path,
'image_count': len(image_files)
})
template_content = render_template_string(
get_image_manager_template(),
image_folders=image_folders
)
return render_admin_page(template_content, title="Image Manager", current_page="images")
@filemanager.route('/settings')
def settings():
"""Site settings interface"""
# Basic settings - could be expanded
config = {
'site_title': 'My Foldsite',
'site_description': 'A blog powered by Foldsite',
'author': '',
'enable_comments': False,
'auto_optimize_images': True
}
# Get cache statistics for display
cache_stats = get_cache_stats()
template_content = render_template_string(
get_settings_template(),
config=config,
cache_stats=cache_stats
)
return render_admin_page(template_content, title="Settings", current_page="settings")
@filemanager.route('/debug')
def debug_info():
"""Debug information page for developers"""
debug_helper = get_debug_helper()
if not debug_helper:
flash("Debug mode not enabled")
return redirect(url_for('filemanager.settings'))
debug_data = debug_helper.create_debug_info_page()
debug_template = """
<h2>🔧 Debug Information</h2>
<div style="margin-bottom: 20px;">
<a href="{{ url_for('filemanager.settings') }}" class="btn">⬅️ Back to Settings</a>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<h3>📋 Configuration</h3>
<div style="font-family: monospace; font-size: 12px;">
<div><strong>Content Dir:</strong> {{ debug_data.config.content_dir }}</div>
<div><strong>Templates Dir:</strong> {{ debug_data.config.templates_dir }}</div>
<div><strong>Styles Dir:</strong> {{ debug_data.config.styles_dir }}</div>
<div><strong>Debug Enabled:</strong> {{ debug_data.config.debug_enabled }}</div>
</div>
</div>
<div style="background: #e8f4fd; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<h3>📄 Available Templates</h3>
{% if debug_data.templates %}
<div style="font-family: monospace; font-size: 12px;">
{% for name, info in debug_data.templates.items() %}
<div style="margin-bottom: 5px;">
<strong>{{ name }}</strong> - {{ info.size }} bytes, modified {{ info.modified }}
</div>
{% endfor %}
</div>
{% else %}
<p style="color: #666;">No templates found in debug mode</p>
{% endif %}
</div>
<div style="background: #f0f8f0; padding: 15px; border-radius: 6px;">
<h3>📁 Content Structure</h3>
{% if debug_data.content_structure %}
<div style="font-family: monospace; font-size: 12px;">
<pre>{{ debug_data.content_structure | tojson(indent=2) }}</pre>
</div>
{% else %}
<p style="color: #666;">No content structure available</p>
{% endif %}
</div>
"""
template_content = render_template_string(debug_template, debug_data=debug_data)
return render_admin_page(template_content, title="Debug Info", current_page="debug")
@filemanager.route('/performance')
def performance_stats():
"""Performance monitoring page for developers"""
perf_monitor = get_performance_monitor()
performance_data = perf_monitor.get_performance_summary()
performance_template = """
<h2>⚡ Performance Statistics</h2>
<div style="margin-bottom: 20px;">
<a href="{{ url_for('filemanager.settings') }}" class="btn">⬅️ Back to Settings</a>
<form method="post" action="{{ url_for('filemanager.clear_performance') }}" style="display: inline; margin-left: 10px;">
<button type="submit" class="btn btn-danger">🗑️ Clear Stats</button>
</form>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px;">
<h3>📊 Overview</h3>
<div style="font-family: monospace; font-size: 12px;">
<div><strong>Total Renders:</strong> {{ performance_data.total_renders }}</div>
<div><strong>Average Time:</strong> {{ "%.3f"|format(performance_data.overall_average_time) }}s</div>
<div><strong>Cache Hit Rate:</strong> {{ "%.1f"|format(performance_data.cache_efficiency.hit_rate) }}%</div>
</div>
</div>
<div style="background: #e8f4fd; padding: 15px; border-radius: 6px;">
<h3>💨 Cache Efficiency</h3>
<div style="font-family: monospace; font-size: 12px;">
<div><strong>Total Requests:</strong> {{ performance_data.cache_efficiency.total_requests }}</div>
<div><strong>Cache Hits:</strong> {{ performance_data.cache_efficiency.cache_hits }}</div>
<div><strong>Miss Rate:</strong> {{ "%.1f"|format(100 - performance_data.cache_efficiency.hit_rate) }}%</div>
</div>
</div>
</div>
{% if performance_data.slow_pages %}
<div style="background: #fff3cd; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<h3>🐌 Slow Pages (>100ms)</h3>
<div style="font-family: monospace; font-size: 12px;">
{% for page in performance_data.slow_pages %}
<div style="margin-bottom: 5px;">
<strong>{{ page.path }}</strong> - {{ "%.3f"|format(page.render_time) }}s
{% if page.template_used %} ({{ page.template_used }}){% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if performance_data.average_times %}
<div style="background: #f0f8f0; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<h3>📈 Average Render Times</h3>
<div style="font-family: monospace; font-size: 12px;">
{% for path, avg_time in performance_data.average_times.items() %}
<div style="margin-bottom: 3px;">
<strong>{{ path }}</strong> - {{ "%.3f"|format(avg_time) }}s avg
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if performance_data.recent_activity %}
<div style="background: #f5f5f5; padding: 15px; border-radius: 6px;">
<h3>🕒 Recent Activity</h3>
<div style="font-family: monospace; font-size: 11px;">
{% for activity in performance_data.recent_activity %}
<div style="margin-bottom: 2px;">
{{ activity.path }} - {{ "%.3f"|format(activity.render_time) }}s
{% if activity.cache_hit %}<span style="color: green;">[CACHED]</span>{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
"""
template_content = render_template_string(performance_template, performance_data=performance_data)
return render_admin_page(template_content, title="Performance Stats", current_page="performance")
@filemanager.route('/clear-performance', methods=['POST'])
def clear_performance():
"""Clear performance statistics"""
try:
perf_monitor = get_performance_monitor()
perf_monitor.clear_stats()
flash("Performance statistics cleared successfully!")
except Exception as e:
flash(f"Error clearing performance stats: {str(e)}")
return redirect(url_for('filemanager.performance_stats'))
# Existing file operations (download, delete, upload, etc.)
@filemanager.route('/download')
def download():
rel_path = request.args.get('path', '')
try:
abs_path = secure_path(rel_path)
except Exception:
return "Invalid path", 400
if not os.path.isfile(abs_path):
return "File not found", 404
directory = os.path.dirname(abs_path)
filename = os.path.basename(abs_path)
return send_from_directory(directory, filename, as_attachment=True)
@filemanager.route('/delete', methods=['POST'])
def delete():
rel_path = request.form.get('path', '')
try:
abs_path = secure_path(rel_path)
except Exception:
return "Invalid path", 400
if os.path.isfile(abs_path):
os.remove(abs_path)
elif os.path.isdir(abs_path):
shutil.rmtree(abs_path)
else:
return "Not found", 404
flash("Deleted successfully")
# Invalidate content cache when files are deleted
invalidate_content_cache(base_dir)
parent = os.path.dirname(rel_path)
return redirect(url_for('filemanager.dashboard', path=parent))
@filemanager.route('/upload', methods=['POST'])
def upload():
rel_path = request.form.get('path', '')
try:
abs_path = secure_path(rel_path)
except Exception:
return "Invalid path", 400
if not os.path.isdir(abs_path):
return "Not a directory", 400
files = request.files.getlist('file')
if files:
for file in files:
if file and file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(abs_path, filename))
flash("Uploaded files successfully")
# Invalidate content cache when files are uploaded
invalidate_content_cache(base_dir)
return redirect(url_for('filemanager.dashboard', path=rel_path))
@filemanager.route('/rename', methods=['POST'])
def rename():
rel_path = request.form.get('path', '')
new_name = request.form.get('new_name', '')
try:
abs_path = secure_path(rel_path)
new_rel_path = os.path.join(os.path.dirname(rel_path), new_name)
new_abs_path = secure_path(new_rel_path)
except Exception:
return "Invalid path", 400
os.rename(abs_path, new_abs_path)
flash("Renamed successfully")
# Invalidate content cache when files are renamed
invalidate_content_cache(base_dir)
parent = os.path.dirname(rel_path)
return redirect(url_for('filemanager.dashboard', path=parent))
@filemanager.route('/cut', methods=['POST'])
def cut():
paths = request.form.getlist('paths')
session['clipboard'] = paths
flash("Item(s) added to clipboard")
return redirect(request.referrer or url_for('filemanager.dashboard'))
@filemanager.route('/paste', methods=['POST'])
def paste():
dest = request.form.get('dest', '')
try:
dest_abs = secure_path(dest)
except Exception:
return "Invalid destination", 400
if not os.path.isdir(dest_abs):
return "Destination not a directory", 400
clipboard = session.get('clipboard', [])
for rel_path in clipboard:
try:
abs_path = secure_path(rel_path)
filename = os.path.basename(abs_path)
shutil.move(abs_path, os.path.join(dest_abs, filename))
except Exception as e:
flash(f"Error moving {rel_path}: {str(e)}")
session.pop('clipboard', None)
flash("Moved items successfully")
# Invalidate content cache when files are moved
invalidate_content_cache(base_dir)
return redirect(url_for('filemanager.dashboard', path=dest))
@filemanager.route('/mkdir', methods=['POST'])
def mkdir():
rel_path = request.form.get('path', '')
dirname = request.form.get('dirname', '')
if not dirname:
flash("Directory name cannot be empty")
return redirect(url_for('filemanager.dashboard', path=rel_path))
try:
# Allow spaces in directory names, just remove dangerous characters
safe_dirname = dirname.replace('/', '-').replace('\\', '-').replace(':', '-').replace('..', '-')
new_dir_path = secure_path(os.path.join(rel_path, safe_dirname))
os.makedirs(new_dir_path, exist_ok=False)
flash(f"Directory '{dirname}' created successfully")
except Exception as e:
flash(f"Error creating directory: {str(e)}")
return redirect(url_for('filemanager.dashboard', path=rel_path))
@filemanager.route('/cancel_move', methods=['POST'])
def cancel_move():
session.pop('clipboard', None)
flash("Move cancelled")
return redirect(request.referrer or url_for('filemanager.dashboard'))
# Blog-focused features
@filemanager.route('/preview-markdown', methods=['POST'])
def preview_markdown():
"""Live preview for markdown content"""
content = request.form.get('content', '')
try:
# Simple markdown preview - could be enhanced with the actual renderer
import mistune
renderer = mistune.create_markdown(renderer=mistune.HTMLRenderer(escape=False))
html = renderer(content)
return {'html': html, 'success': True}
except Exception as e:
return {'html': f'<p>Preview error: {str(e)}</p>', 'success': False}
@filemanager.route('/quick-post', methods=['POST'])
def quick_post():
"""Quick post creation from dashboard"""
title = request.form.get('title', '').strip()
if not title:
flash("Title is required for quick post")
return redirect(url_for('filemanager.dashboard'))
# Generate quick post - no date prefix for quick posts
safe_title = title.replace('/', '-').replace('\\', '-').replace(':', '-')
filename = f"{safe_title}.md"
try:
abs_path = secure_path(filename)
# Quick post template
quick_content = f"""---
title: {title}
date: {today}
tags: []
---
# {title}
Your content here...
"""
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(quick_content)
flash(f"Quick post '{title}' created!")
return redirect(url_for('filemanager.edit_post', filename=filename))
except Exception as e:
flash(f"Error creating quick post: {str(e)}")
return redirect(url_for('filemanager.dashboard'))
@filemanager.route('/list-posts')
def list_posts():
"""List all blog posts for management"""
posts = []
for root, dirs, files in os.walk(base_dir):
for file in files:
if file.endswith('.md'):
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, base_dir)
# Try to extract frontmatter
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
title = os.path.splitext(file)[0]
date = ""
tags = []
if content.startswith('---'):
try:
import frontmatter
post = frontmatter.loads(content)
title = post.metadata.get('title', title)
date = post.metadata.get('date', '')
tags = post.metadata.get('tags', [])
except:
pass
stat = os.stat(file_path)
# Ensure date is always a string for consistent sorting
if hasattr(date, 'strftime'):
date = date.strftime('%Y-%m-%d')
elif not date:
date = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d')
posts.append({
'title': title,
'filename': file,
'path': rel_path,
'date': str(date) if date else '',
'tags': tags,
'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
'size': stat.st_size
})
except Exception:
continue # Skip files we can't read
# Sort by date/modified time
posts.sort(key=lambda x: x.get('date') or x['modified'], reverse=True)
post_list_template = """
<div style="margin-bottom: 20px;">
<h2>📝 All Posts</h2>
<form method="post" action="{{ url_for('filemanager.quick_post') }}" style="margin-bottom: 20px;">
<div style="display: flex; gap: 10px;">
<input type="text" name="title" placeholder="Quick post title..." style="flex: 1; padding: 8px;">
<button type="submit" class="btn">⚡ Quick Post</button>
</div>
</form>
</div>
<div class="file-grid">
{% for post in posts %}
<div class="file-item">
<div style="font-size: 24px;">📝</div>
<strong>{{ post.title }}</strong>
<div style="color: #666; font-size: 12px;">
{% if post.date %}{{ post.date }}{% else %}{{ post.modified }}{% endif %}
</div>
{% if post.tags %}
<div style="margin: 5px 0;">
{% for tag in post.tags %}
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-right: 3px;">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
<div class="file-actions">
<a href="{{ url_for('filemanager.edit_post', filename=post.path) }}">✏️ Edit</a>
<a href="/{{ post.path }}" target="_blank">👁️ View</a>
<a href="#" onclick="deleteItem('{{ post.path }}'); return false;" style="color: #dc3545;">🗑️ Delete</a>
</div>
</div>
{% endfor %}
</div>
"""
template_content = render_template_string(post_list_template, posts=posts)
return render_admin_page(template_content, title="All Posts", current_page="posts")
@filemanager.route('/upload-images', methods=['POST'])
def upload_images():
"""Enhanced image upload with optimization"""
folder = request.form.get('folder', 'images').strip()
if folder and not folder.endswith('/'):
folder += '/'
try:
upload_path = secure_path(folder)
os.makedirs(upload_path, exist_ok=True)
files = request.files.getlist('images')
uploaded_count = 0
for file in files:
if file and file.filename:
filename = secure_filename(file.filename)
if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
file_path = os.path.join(upload_path, filename)
file.save(file_path)
# Basic image optimization could go here
uploaded_count += 1
flash(f"Uploaded {uploaded_count} images to {folder}")
except Exception as e:
flash(f"Error uploading images: {str(e)}")
return redirect(url_for('filemanager.image_manager'))
@filemanager.route('/create-gallery', methods=['POST'])
def create_gallery():
"""Create image gallery page from folder"""
folder = request.form.get('folder', '').strip()
if not folder:
flash("Folder path required")
return redirect(url_for('filemanager.image_manager'))
try:
folder_path = secure_path(folder)
if not os.path.isdir(folder_path):
flash("Folder not found")
return redirect(url_for('filemanager.image_manager'))
# Find images in folder
images = []
for file in os.listdir(folder_path):
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
images.append(file)
if not images:
flash("No images found in folder")
return redirect(url_for('filemanager.image_manager'))
# Generate gallery markdown
gallery_name = os.path.basename(folder) or 'Gallery'
gallery_filename = f"{gallery_name.lower().replace(' ', '-')}-gallery.md"
gallery_content = f"""---
title: {gallery_name} Gallery
date: {datetime.now().strftime('%Y-%m-%d')}
tags: [gallery, photos]
---
# {gallery_name} Gallery
"""
for image in sorted(images):
image_path = os.path.join(folder, image) if folder else image
gallery_content += f"![{image}](/{image_path})\n\n"
gallery_path = secure_path(gallery_filename)
with open(gallery_path, 'w', encoding='utf-8') as f:
f.write(gallery_content)
flash(f"Gallery page '{gallery_filename}' created with {len(images)} images!")
return redirect(url_for('filemanager.edit_post', filename=gallery_filename))
except Exception as e:
flash(f"Error creating gallery: {str(e)}")
return redirect(url_for('filemanager.image_manager'))
@filemanager.route('/save-settings', methods=['POST'])
def save_settings():
"""Save basic site settings"""
# In a real implementation, this would save to a config file
flash("Settings saved successfully!")
return redirect(url_for('filemanager.settings'))
@filemanager.route('/clear-cache', methods=['POST'])
def clear_cache():
"""Clear all cached data"""
try:
cleared_count = clear_all_cache()
flash(f"Cache cleared successfully! Removed {cleared_count} cached entries.")
except Exception as e:
flash(f"Error clearing cache: {str(e)}")
return redirect(url_for('filemanager.settings'))
# Image optimization routes
@filemanager.route('/optimize-images', methods=['POST'])
def optimize_images():
"""Bulk optimize all images"""
try:
results = bulk_optimize_images(base_dir)
flash(f"Optimized {results['optimized']} images, {results['errors']} errors, {results['skipped']} skipped")
except Exception as e:
flash(f"Error optimizing images: {str(e)}")
return redirect(url_for('filemanager.image_manager'))
@filemanager.route('/generate-thumbnails', methods=['POST'])
def generate_thumbnails():
"""Generate thumbnails for all images"""
try:
optimizer = ImageOptimizer()
thumbnail_dir = os.path.join(base_dir, 'thumbnails')
os.makedirs(thumbnail_dir, exist_ok=True)
count = 0
for root, dirs, files in os.walk(base_dir):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
file_path = os.path.join(root, file)
generated = optimizer.generate_thumbnails(file_path, thumbnail_dir)
if generated:
count += len(generated)
flash(f"Generated {count} thumbnails in /thumbnails/")
except Exception as e:
flash(f"Error generating thumbnails: {str(e)}")
return redirect(url_for('filemanager.image_manager'))
@filemanager.route('/view-images/<path:folder>')
def view_images(folder):
"""View images in a specific folder with optimization options"""
try:
folder_path = secure_path(folder)
except Exception:
flash("Invalid folder path")
return redirect(url_for('filemanager.image_manager'))
if not os.path.isdir(folder_path):
flash("Folder not found")
return redirect(url_for('filemanager.image_manager'))
# Get image data
images = create_image_gallery_data(folder_path)
image_viewer_template = """
<div style="margin-bottom: 20px;">
<h2>🖼️ Images in {{ folder_name }}</h2>
<div style="margin-bottom: 15px;">
<a href="{{ url_for('filemanager.image_manager') }}" class="btn">⬅️ Back to Image Manager</a>
<form method="post" action="{{ url_for('filemanager.create_gallery') }}" style="display: inline; margin-left: 10px;">
<input type="hidden" name="folder" value="{{ folder }}">
<button type="submit" class="btn">📝 Create Gallery Page</button>
</form>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 15px;">
{% for image in images %}
<div class="file-item">
<img src="/download/{{ image.path }}" alt="{{ image.filename }}"
style="width: 100%; height: 150px; object-fit: cover; border-radius: 4px; margin-bottom: 10px;">
<strong>{{ image.filename }}</strong>
<div style="color: #666; font-size: 12px;">
{{ image.width }}x{{ image.height }} • {{ image.size_kb }}KB
</div>
{% if image.date_taken %}
<div style="color: #666; font-size: 11px;">📅 {{ image.date_taken }}</div>
{% endif %}
{% if image.camera %}
<div style="color: #666; font-size: 11px;">📷 {{ image.camera }}</div>
{% endif %}
<div class="file-actions">
<a href="/download/{{ image.path }}" target="_blank">⬇️ Download</a>
<a href="#" onclick="deleteItem('{{ image.path }}'); return false;" style="color: #dc3545;">🗑️ Delete</a>
</div>
</div>
{% endfor %}
</div>
{% if not images %}
<div style="text-align: center; padding: 40px; color: #666;">
<p>No images found in this folder.</p>
</div>
{% endif %}
"""
template_content = render_template_string(
image_viewer_template,
images=images,
folder=folder,
folder_name=os.path.basename(folder) or 'Root'
)
return render_admin_page(template_content, title=f"Images: {os.path.basename(folder)}", current_page="images")
@filemanager.route('/optimize-single-image', methods=['POST'])
def optimize_single_image():
"""Optimize a single image"""
image_path = request.form.get('path', '')
try:
abs_path = secure_path(image_path)
optimizer = ImageOptimizer()
if optimizer.optimize_image(abs_path):
flash(f"Image {os.path.basename(image_path)} optimized successfully!")
else:
flash("Failed to optimize image")
except Exception as e:
flash(f"Error optimizing image: {str(e)}")
# Redirect back to the image view or image manager
folder = os.path.dirname(image_path)
if folder:
return redirect(url_for('filemanager.view_images', folder=folder))
else:
return redirect(url_for('filemanager.image_manager'))
return filemanager

View File

@ -0,0 +1,204 @@
"""
Image optimization utilities for foldsite
Follows grug principles: simple, focused functionality
"""
import os
from PIL import Image, ExifTags
from pathlib import Path
class ImageOptimizer:
"""Simple image optimization following grug principles - does one thing well"""
def __init__(self, max_width=2048, max_height=2048, quality=85):
self.max_width = max_width
self.max_height = max_height
self.quality = quality
def optimize_image(self, input_path, output_path=None, preserve_exif=True):
"""
Optimize a single image - grug-simple approach
"""
if output_path is None:
output_path = input_path
try:
with Image.open(input_path) as img:
# Handle EXIF orientation
if preserve_exif and hasattr(img, '_getexif'):
exif = img._getexif()
if exif is not None:
orientation = exif.get(0x0112, 1)
if orientation == 3:
img = img.rotate(180, expand=True)
elif orientation == 6:
img = img.rotate(270, expand=True)
elif orientation == 8:
img = img.rotate(90, expand=True)
# Resize if too large
if img.width > self.max_width or img.height > self.max_height:
img.thumbnail((self.max_width, self.max_height), Image.Resampling.LANCZOS)
# Convert to RGB if needed (for JPEG output)
if img.mode in ('RGBA', 'P'):
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
rgb_img.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = rgb_img
# Save optimized image
save_kwargs = {'quality': self.quality, 'optimize': True}
# Preserve some EXIF if possible
if preserve_exif and hasattr(img, '_getexif'):
exif_dict = img._getexif()
if exif_dict:
# Keep important EXIF data
save_kwargs['exif'] = img.info.get('exif', b'')
img.save(output_path, **save_kwargs)
return True
except Exception as e:
print(f"Error optimizing {input_path}: {e}")
return False
def optimize_folder(self, folder_path, backup=True):
"""
Optimize all images in a folder - simple batch processing
"""
folder_path = Path(folder_path)
results = {'optimized': 0, 'errors': 0, 'skipped': 0}
image_extensions = {'.jpg', '.jpeg', '.png', '.gif'}
for file_path in folder_path.rglob('*'):
if file_path.suffix.lower() in image_extensions:
try:
# Create backup if requested
if backup:
backup_path = file_path.with_suffix(f'{file_path.suffix}.backup')
if not backup_path.exists():
file_path.rename(backup_path)
source_path = backup_path
else:
source_path = file_path
else:
source_path = file_path
# Optimize
if self.optimize_image(source_path, file_path):
results['optimized'] += 1
else:
results['errors'] += 1
except Exception:
results['errors'] += 1
return results
def generate_thumbnails(self, image_path, thumbnail_dir, sizes=[150, 300, 600]):
"""
Generate thumbnails in multiple sizes - simple and useful
"""
image_path = Path(image_path)
thumbnail_dir = Path(thumbnail_dir)
thumbnail_dir.mkdir(exist_ok=True)
base_name = image_path.stem
extension = image_path.suffix
generated = []
try:
with Image.open(image_path) as img:
for size in sizes:
# Create thumbnail
thumb = img.copy()
thumb.thumbnail((size, size), Image.Resampling.LANCZOS)
# Save thumbnail
thumb_name = f"{base_name}_{size}px{extension}"
thumb_path = thumbnail_dir / thumb_name
# Convert to RGB for JPEG if needed
if thumb.mode in ('RGBA', 'P') and extension.lower() in ['.jpg', '.jpeg']:
rgb_thumb = Image.new('RGB', thumb.size, (255, 255, 255))
rgb_thumb.paste(thumb, mask=thumb.split()[-1] if thumb.mode == 'RGBA' else None)
thumb = rgb_thumb
thumb.save(thumb_path, quality=self.quality, optimize=True)
generated.append(thumb_path)
except Exception as e:
print(f"Error generating thumbnails for {image_path}: {e}")
return generated
def get_image_info(self, image_path):
"""
Get basic image information - simple and fast
"""
try:
with Image.open(image_path) as img:
info = {
'width': img.width,
'height': img.height,
'format': img.format,
'mode': img.mode,
'size_kb': os.path.getsize(image_path) // 1024
}
# Get EXIF data if available
if hasattr(img, '_getexif'):
exif = img._getexif()
if exif:
exif_data = {}
for key, value in exif.items():
tag = ExifTags.TAGS.get(key, key)
exif_data[tag] = value
info['exif'] = exif_data
return info
except Exception as e:
return {'error': str(e)}
def bulk_optimize_images(content_dir, max_width=2048, quality=85):
"""
Utility function for bulk optimization - grug simple
"""
optimizer = ImageOptimizer(max_width=max_width, quality=quality)
return optimizer.optimize_folder(content_dir, backup=True)
def create_image_gallery_data(folder_path):
"""
Create gallery data structure for templates - simple and useful
"""
folder_path = Path(folder_path)
images = []
image_extensions = {'.jpg', '.jpeg', '.png', '.gif'}
for file_path in folder_path.iterdir():
if file_path.suffix.lower() in image_extensions and file_path.is_file():
optimizer = ImageOptimizer()
info = optimizer.get_image_info(file_path)
if 'error' not in info:
images.append({
'filename': file_path.name,
'path': str(file_path.relative_to(folder_path.parent)),
'width': info['width'],
'height': info['height'],
'size_kb': info['size_kb'],
'exif': info.get('exif', {}),
'date_taken': info.get('exif', {}).get('DateTimeOriginal', ''),
'camera': info.get('exif', {}).get('Model', '')
})
# Sort by date taken or filename
images.sort(key=lambda x: x.get('date_taken') or x['filename'])
return images

157
src/server/simple_cache.py Normal file
View File

@ -0,0 +1,157 @@
"""
Simple caching system for foldsite - following grug principles
No fancy cache libraries, just simple in-memory cache with TTL
"""
import time
from typing import Any, Optional, Callable
from threading import Lock
class SimpleCache:
"""
Grug-approved simple cache: does one thing well
- In-memory storage with TTL
- Thread-safe
- No complexity demons
"""
def __init__(self, default_ttl: int = 300): # 5 minutes default
self.cache = {}
self.lock = Lock()
self.default_ttl = default_ttl
def get(self, key: str) -> Optional[Any]:
"""Get value from cache, return None if expired or missing"""
with self.lock:
if key in self.cache:
value, expiry = self.cache[key]
if time.time() < expiry:
return value
else:
# Clean up expired entry
del self.cache[key]
return None
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
"""Set value in cache with TTL"""
if ttl is None:
ttl = self.default_ttl
expiry = time.time() + ttl
with self.lock:
self.cache[key] = (value, expiry)
def delete(self, key: str) -> None:
"""Remove key from cache"""
with self.lock:
self.cache.pop(key, None)
def clear(self) -> None:
"""Clear all cache entries"""
with self.lock:
self.cache.clear()
def cleanup_expired(self) -> int:
"""Remove expired entries, return count of removed items"""
current_time = time.time()
expired_keys = []
with self.lock:
for key, (value, expiry) in self.cache.items():
if current_time >= expiry:
expired_keys.append(key)
for key in expired_keys:
del self.cache[key]
return len(expired_keys)
def cache_info(self) -> dict:
"""Get cache statistics - useful for debugging"""
with self.lock:
current_time = time.time()
expired_count = sum(1 for _, expiry in self.cache.values() if current_time >= expiry)
return {
'total_entries': len(self.cache),
'expired_entries': expired_count,
'active_entries': len(self.cache) - expired_count,
'memory_usage_estimate': sum(len(str(k)) + len(str(v[0])) for k, v in self.cache.items())
}
def cached(cache_instance: SimpleCache, ttl: Optional[int] = None, key_func: Optional[Callable] = None):
"""
Simple decorator for caching function results
Grug-approved: easy to use, easy to understand
"""
def decorator(func):
def wrapper(*args, **kwargs):
# Generate cache key
if key_func:
cache_key = key_func(*args, **kwargs)
else:
# Simple key generation
cache_key = f"{func.__name__}:{hash(str(args) + str(sorted(kwargs.items())))}"
# Try to get from cache
result = cache_instance.get(cache_key)
if result is not None:
return result
# Compute result and cache it
result = func(*args, **kwargs)
cache_instance.set(cache_key, result, ttl)
return result
return wrapper
return decorator
# Global cache instance for the application
app_cache = SimpleCache(default_ttl=300) # 5 minutes default
def get_folder_contents_cache_key(content_dir: str, folder: str = "") -> str:
"""Generate cache key for folder contents"""
return f"folder_contents:{content_dir}:{folder}"
def get_posts_cache_key(content_dir: str, limit: int, folder: str = "") -> str:
"""Generate cache key for recent posts"""
return f"recent_posts:{content_dir}:{limit}:{folder}"
def invalidate_content_cache(content_dir: str) -> None:
"""
Invalidate all content-related caches when content changes
Simple approach: clear all keys that start with content patterns
"""
cache = app_cache
with cache.lock:
keys_to_delete = []
for key in cache.cache.keys():
if (key.startswith(f"folder_contents:{content_dir}") or
key.startswith(f"recent_posts:{content_dir}") or
key.startswith(f"posts_by_tag:{content_dir}")):
keys_to_delete.append(key)
for key in keys_to_delete:
cache.cache.pop(key, None)
# Utility function for cache management in admin
def get_cache_stats() -> dict:
"""Get cache statistics for admin interface"""
stats = app_cache.cache_info()
stats['cleanup_removed'] = app_cache.cleanup_expired()
return stats
def clear_all_cache() -> int:
"""Clear all cache and return count of entries removed"""
with app_cache.lock:
count = len(app_cache.cache)
app_cache.cache.clear()
return count