-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow Serving "Virtual" Routes from Plugins #1737
Comments
Hey guys, is there anyone I can tag in order to get this proposal reviewed? Thanks in advance for the feedback :) |
cc @docsifyjs/reviewers |
Thanks for taking the time to document the request, @illBeRoy. This looks interesting. I haven't had time to think this through, but here are a few thoughts off the top of my head and in no particular order:
While outside the scope of this proposal, it's easy to imagine how creating dynamic site content would also be useful for other docsify content such as the top navigation, sidebar, and embeds. Multiple feature requests have been made here and in other repos to allow markdown features like helpers, plugins, and Vue rendering to just work everywhere rather than only in the main content area. Obviously, docsify isn't setup to do that today and doing so would require significant changes, but it's worth considering as we explore how to integrate the proposed feature. For example, how could a plugin author use Exciting stuff. Looking forward to hearing everyone's thoughts. :) |
I think I propose we not introduce new names that follow naming of existing hooks, and make new hooks have the most meaningful name they can have. Also I don't think Naming ideas:
Note that someone doesn't even have to fetch anything, they could just provide a hard coded markdown string for example.
I propose let's not worry about this yet. A first version can allow a plugin to provide markdown for the top level route only, not all other requests. I believe this is what the OP is about too. After that, embedded files would work the same. Later on, as a separate feature, onRequest or onResponse hooks could be used to intercept and, if desired by a plugin author, replace the responses or requests with alternates. |
Or maybe we just need onResponse and onRequest? That would be more powerful, and we can include metadata as to whether a request or response is for a route or not. Or if it is for an embedded file, etc |
Running before a request is the proposed behavior. From the OP:
Instead of focusing on serving virtual routes, I think we're better served by focusing on the "intercept fetch requests before they are made" part of the proposal. There are many reasons why a site owner or plugin author may want to intercept a request. The OP's use case -- requesting data from an API and generating content from it -- is one of many possible use cases. Other use cases include displaying a UI element, prompting for user input, interacting with a Vue component, changing the state of a remote application, or modifying request headers. This is why I believe a name based on the event/stage (like
To clarify, the OP mentioned modifying the existing My intention in mentioning features "outside the scope of this proposal" was to encourage @illBeRoy to think beyond the original use case. My gut tells me that if someone wants to dynamically generate pages from an API, it won't be long before they want to do the same for navigation (sidebar) or portions of a page (embedded files). Considering this while building v1 can make the road to v2 shorter and less bumpy for everyone. If the decision is "we don't care about now", that's okay, too.
Yep! I think
Yep! Both a request- and a response-related hook would be useful. Let's do both. An An As far as naming goes, the Also worth mentioning: In the future we can determine if we need both
If by "top level route only" you mean the route associated with the main content area then I agree. Handling the top nav, sidebar, and embeds is a separate effort that I assume would be rolled into a larger plugin system project. |
Hey guys! Following the discussion, the way I understand it we have two main options for implementation, each (imo) has a slightly different meaning: The fetchRoute OptionThis was my original proposal: let plugin writers provide virtual routes. It doesn't have a lot to do with request and response, and more like a replacement for the network request altogether. The product here is not to intercept requests; intercepting them is just an implementation details. Instead, the API here is to let you provide custom markdown based on the route, instead of using network requests altogether. This is a higher level API to allow users to provide custom content for routes.
The onRequest and onResponse OptionThis option is a different product than my original proposal. Instead of telling users "you can provide custom pages", we're telling them: "you can intercept requests and responses and alter them". Any request and response - not just routes. This allows for my original use case, but solves it from a different perspective: this time, you let plugins intercept requests and alter them, similar to what Puppeteer's setRequestInterception API does. This also opens up followup questions:
In addition, if we decide to go down this path, we should probably define a full request interface here, similar to what they do in the aforementioned example from Puppeteer: a request object that has To summarize:
Bottom LineOption one lets you integrate with "content providers". Option two lets you declare interceptors. Option one provides a higher level API that would be easier to use, but is more limited than option two in what you can achieve. The way I see it, both options aren't contradicting each other, and can live side by side. Much like If we go down that path, we can strategically decide how and when to implement each option. We can start by delivering All that said and done, the final say here is yours and I would be more than happy to contribute either option according to your decision :) Looking forward for your feedback! |
Hey @illBeRoy. Thanks for the thoughtful breakdown and your willingness to work through the details. I agree we're talking about two different approaches, both of which address the route-focused use case you've outlined. My concern is how this all plays out if/when request/response hooks are implemented. If we agree that these hooks are useful, I think it makes sense to assume they will be so we can work through potential issues and make better short- and long-term decisions. I also think the pros and cons can easily be made to favor either approach, as can the the rational for implementing one hook before the other. The good news is that I have a proposal that I believe works well for everyone and addresses the issues discussed so far. Let's dive in... 1. Add
|
@illBeRoy -- Checking in. 😄 |
Hey! Sorry for the delay, seems like I completely missed the previous update! This looks like a great idea. Gonna start working on the first PR asap. |
@illBeRoy -- Sounds great. Looking forward to it. Happy to lend a hand if needed. |
Hey again! I know it's been a while (had some stuff going on and almost no free time on my hands), but I finally got to work on the PR and I'm almost ready to submit! Before I do, though, I've a few questions about the spec, and would love your feedback: Matches in string routesRegarding the following: {
// RegEx match w/ return string and match string
'/foo/(.*)': '# Route match: $1',
} While it seems like an extremely convenient way to format routes without having to write too much code, I'm a little worried that people might unknowingly trigger this mechanism. Now, I thought about possible solutions (e.g. more distinct delimiter; a standard templating language like ejs or vm) but IMO that inherent problem is that people who do not expect it might still fall for it. Bottom LineI already implemented this feature, but maybe we should drop it altogether? If someone wants to format the match into their pages, let them use a function and not a string. The "next" functionI was thinking that maybe we can rethink how we handle async functions? That is, if a function is sync, we just use its return value as the contents of the page. How about we do the same with async functions, instead of making you invoke a Thanks for your time! LMK what you think and I'll adjust the PR accordingly. |
@illBeRoy -- Very excited to hear about your progress! Thanks for devoting the time and energy on this. Very much appreciated. :)
If I understand the concern correctly, the issue is that a string-based route value my contain
This is how it works today. The existing documentation explaining how to write a plugin doesn't make this very clear. This (among other reasons) is why I rewrote those docs which you can see on the develop branch preview: https://docsify-preview.vercel.app/#/write-a-plugin
No argument from me about the need to revisit out plugin system. How async functions are handled is one of the items on that todo list. The one thing to keep in mind is that the current version of Docsify still supports legacy browsers, changing how async plugins are handled would require a major version bump (I assume), and bumping to the next major version is not as simple as it may seem due to Docsify's history. I'm very much trying to avoid opening up a can of worms and diving into that history here, so apologies if that seems vague. I think the best approach for now is to separate the functionality proposed in this PR from tasks like rethinking how we handle async plugins, then revisit the plugin system and decide if/how/when we want to move forward there in a separate issue. I'd love to have that discussion if it's something you're interested in. Hope this helps. |
Thanks for the reply! It does help :) So as far as I understand, our action items are:
Will do. One more question, though: how do you propose that we determine whether the function is sync or async? since with sync functions we don't wait for anything (just use the returned string), but with async functions we need to await Thanks! By the way, you can see my draft here (need to add docs and modify the impl to match what we discussed here) Edit: I see that in |
Yep. The plugin system implementation predates any of the current maintainers. I just worked with the existing patterns when possible to reduce risk and avoid requiring a major version bump. FYI, I also approved the draft so automated tests can run on the PR. You should see the results reflected on Github. |
Thanks! |
Hey! PR is ready for review 🙌 |
Hi, @illBeRoy @jhildenbiddle |
Feature request
Plugins are a great way to introduce dynamic content into existing pages. But, what if we took this a step further, and allowed plugins to actually generate the routes themselves on the fly?
The feature I want to contribute introduces an API for plugins to actually provide dynamically generated pages for routes, even if actual markdown files do not exist to begin with.
What problem does this feature solve?
Say that you have a list of APIs, which can change at any time. Each one should have its own page under
/apis/<METHOD_NAME>.md
.Today, you will have to create real markdown files and deploy them along with your site, as every route in a Docsify website requires that a file exist on the server. This means that you will have to create all the
/apis/*.md
files manually and deploy them, and do this every time that even a character changes in any of the API descriptions.With my proposed solution, you will be able to create a plugin that intercepts all fetches from
/apis/*
, and provide dynamically generated markdown as replacement from fetching real markdown files from the server.What does the proposed API look like?
Add a new hook to the Plugin API, called:
fetchRoute(cb)
, which will essentially allow plugins to intercept fetch requests before they are made by the underlying engine. If any plugin has a fetchRoute hook, and the callback provided any content for the route, use that content instead of fetching.The fetchRoute Hook Behavior
This hook receives a callback, possibly async, that implements the following signature:
(route: Route, next: fn) => void | Promise<void>
. The route object is the same one that can be found onvm.route
.The
next
function can either receive amarkdown
string, or nothing at all. If a non-empty string was provided, then this is the markdown that will be passed into the system, and nofetch
will be done by the Fetch class itself.If the
next
function was called with a falsy value, it will just call the next plugin that hasfetchRoute
. Finally, if all plugins were exhausted and none returned any markdown, the internalfetch
function will be run like today.Flow Diagram
How should this be implemented in your opinion?
From my brief introduction to the code, I would suggest that we edit the
_fetch
method (link to code), so instead of directly usingget
, it will run thefetchRoute
hooks first and only fall back to get if none return markdown.Are you willing to work on this yourself?
Yes :) Would love to.
The text was updated successfully, but these errors were encountered: