I'm not sure if this is a bug, a security feature, or something to do with Twig 2; but since Craft 3 (RC3) I'm no longer able to access hidden twig files via ajax.

This works fine:

      console.log("SUCCESS", data);
      console.log("ERROR", data);

However, when I point the url to a hidden file with this:


I get a 404 (Not Found) error. For my example, I have both '_test.twig' and 'test.twig' files in the 'blog' directory.

Am I missing a breaking change?


I didn't really know what it was for, but 'enableCsrfProtection' => true has always been set in my config/general.php file. (In both Craft 2 and 3). I've tried setting this to false too, and I'm getting the same results.

Following Robins comment, I've attempted to follow these instructions: Enabling CSRF Protection.

I've set the global variables window.csrfTokenName and window.csrfTokenValue. And now I'm stuck.

I'm pretty new to ajax, and I'm not sure how to extrapolate the example in a way where I can add the CSRF Token correctly in my 'GET' method.

This is the response I'm getting:

{readyState: 4, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}
abort : (statusText)
always : ()
catch : (fn)
done : ()
fail : ()
getAllResponseHeaders : ()
getResponseHeader : (key)
overrideMimeType : (type)
pipe : ()
progress : ()
promise : (obj)
readyState : 4
responseText : "<pre>Not Found: Page not found.</pre>"
setRequestHeader : (name,value)
state : ()
status : 404
statusCode : (map)
statusText : "Not Found"
then : (onFulfilled,onRejected,onProgress)
__proto__ : Object
  • 1
    Did you disable the CSRF validation in your config? If not, it's not going to work since you need to include it and I'll post how to get it in my answer. If not maybe you could show us your complete response? Dec 31, 2017 at 14:18

1 Answer 1


My guess is still: you did not define a route correct route for your template. You can check if you can access the file when you call the route normally via url www.example.com/blog/test if you can see the template, then it should usually work, if not you did not define any routing rules see here

However: when you do it that way everyone with the link may see this template and I expect it to look not very good without content since it's probably only a partial, so I would highly recommend you to use the proper way via controller.

public function actionRenderTemplate(){
    $response = [
        'success'   => false,
        'message'   => 'something went wrong'
    if($template = Craft::$app->getRequest()->getBodyParam('template')){
            $response['success'] = true;
            $response['html'] = Craft::$app->getView()->renderTemplate($template, [
                'foo' => Craft::$app->getRequest()->getBodyParam('just an example to pass values');
        }catch(\Twig_Error_Loader $e){
            $response['message'] = $e->getMessage(); 
        }catch(\yii\base\Exception $e){
            $response['message'] = $e->getMessage();
    return $this->asJson($response);

And your javascript

    type: "post",
    data: {
        action: 'plugin-handle/controller/render-template',
        template: 'path/to/template'
    url: '',
    success: function (data) {
        if (data.success === true) {
            // do your stuff
        } else {
            // something went wrong
            // show a message or whatever

Please make sure you are in the correct folder for rendering your template, there is a difference between frontend and cp requests docs that's why we always build a custom template service that checks if there is a template in the normal path first and only takes the plugin template if there is none so we can overwrite plugin templates if needed and don't need to check/overwrite the paths every time

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.